Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
106 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "1960697431/openclaw-mem0", "files": {"src/utils.ts": {"9d6c99b2dcd0e880213081cf959a65d63494dd3e": {"t": 1785746026.68918, "n": 9508, "h": "8ab30be823c849b7", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (match, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n console.warn(`[mem0] ⚠️ Environment variable ${envVar} is not set, leaving as-is`);\n return match;\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n // MiniMax v2 chat completion endpoint (required for JSON extraction)\n \"minimax\": \"https://api.minimaxi.com/v1/text/chatcompletion_v2\",\n };\n\n // Providers that are OpenAI-compatible and can be normalized to \"openai\"\n const OPENAI_COMPATIBLE_PROVIDERS = new Set([\"deepseek\", \"moonshot\", \"yi\", \"siliconflow\", \"dashscope\", \"zhipu\"]);\n \n if (KNOWN_PROVIDERS[prov] && OPENAI_COMPATIBLE_PROVIDERS.has(prov)) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n } else if (KNOWN_PROVIDERS[prov]) {\n // For non-OpenAI-compatible providers (like MiniMax), force set the correct baseURL\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n // Keep the original provider name (e.g., \"minimax\")\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n \n cfg.baseURL = url;\n }\n\n // 2.5. Fix MiniMax anthropic URL inherited from main config\n // Main config uses anthropic-messages API, but mem0 needs chatcompletion_v2\n if (cfg.baseURL && typeof cfg.baseURL === \"string\") {\n const url = cfg.baseURL.trim();\n // Detect MiniMax anthropic URL pattern from main config\n if (url.includes(\"api.minimaxi.com\") && url.includes(\"/anthropic\")) {\n // Convert to MiniMax v2 chat completion endpoint\n cfg.baseURL = \"https://api.minimaxi.com/v1/text/chatcompletion_v2\";\n // If provider is anthropic or openai, switch to minimax\n if (prov === \"anthropic\" || prov === \"openai\") {\n prov = \"minimax\";\n }\n }\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\n/**\n * Clean JSON response from LLMs that might wrap it in markdown or add commentary.\n */\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n // Strategy 1: Full-text code block\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n // Strategy 2: Last code block\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n // Strategy 3: Raw JSON object/array\n const jsonMatch = cleaned.match(/(\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])/);\n if (jsonMatch) {\n try {\n JSON.parse(jsonMatch[1]);\n return jsonMatch[1];\n } catch { /* ignore */ }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "31450d0faf9d193c0bfbd79b07187f7b6dac77f4": {"t": 1785758945.7680273, "n": 9462, "h": "43f23a683d2c6211", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n // MiniMax v2 chat completion endpoint (required for JSON extraction)\n \"minimax\": \"https://api.minimaxi.com/v1/text/chatcompletion_v2\",\n };\n\n // Providers that are OpenAI-compatible and can be normalized to \"openai\"\n const OPENAI_COMPATIBLE_PROVIDERS = new Set([\"deepseek\", \"moonshot\", \"yi\", \"siliconflow\", \"dashscope\", \"zhipu\"]);\n \n if (KNOWN_PROVIDERS[prov] && OPENAI_COMPATIBLE_PROVIDERS.has(prov)) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n } else if (KNOWN_PROVIDERS[prov]) {\n // For non-OpenAI-compatible providers (like MiniMax), force set the correct baseURL\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n // Keep the original provider name (e.g., \"minimax\")\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n \n cfg.baseURL = url;\n }\n\n // 2.5. Fix MiniMax anthropic URL inherited from main config\n // Main config uses anthropic-messages API, but mem0 needs chatcompletion_v2\n if (cfg.baseURL && typeof cfg.baseURL === \"string\") {\n const url = cfg.baseURL.trim();\n // Detect MiniMax anthropic URL pattern from main config\n if (url.includes(\"api.minimaxi.com\") && url.includes(\"/anthropic\")) {\n // Convert to MiniMax v2 chat completion endpoint\n cfg.baseURL = \"https://api.minimaxi.com/v1/text/chatcompletion_v2\";\n // If provider is anthropic or openai, switch to minimax\n if (prov === \"anthropic\" || prov === \"openai\") {\n prov = \"minimax\";\n }\n }\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\n/**\n * Clean JSON response from LLMs that might wrap it in markdown or add commentary.\n */\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n // Strategy 1: Full-text code block\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n // Strategy 2: Last code block\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n // Strategy 3: Raw JSON object/array\n const jsonMatch = cleaned.match(/(\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])/);\n if (jsonMatch) {\n try {\n JSON.parse(jsonMatch[1]);\n return jsonMatch[1];\n } catch { /* ignore */ }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "6bca5c0e795626494042601a6f2d7bfa2f1383f6": {"t": 1785758947.4797697, "n": 11313, "h": "f29882b1a7bd81ba", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"kimi\": \"https://api.moonshot.cn/v1\",\n \"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"glm\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n // MiniMax chat-completions endpoint (v2 format) is required for mem0 JSON extraction.\n \"minimax\": \"https://api.minimaxi.com/v1/text/chatcompletion_v2\",\n \"openrouter\": \"https://openrouter.ai/api/v1\",\n \"baichuan\": \"https://api.baichuan-ai.com/v1\",\n \"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"ark\": \"https://ark.cn-beijing.volces.com/api/v3\",\n };\n\n if (KNOWN_PROVIDERS[prov]) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n // Remove common endpoint-level mistakes\n url = url.replace(/\\/chat\\/completions\\/?$/i, \"\");\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Baichuan: https://api.baichuan-ai.com -> https://api.baichuan-ai.com/v1\n if (url.includes(\"api.baichuan-ai.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // OpenRouter: https://openrouter.ai/api -> https://openrouter.ai/api/v1\n if (url.includes(\"openrouter.ai/api\") && !url.includes(\"/api/v1\")) {\n url = url.replace(/\\/$/, \"\").replace(/\\/api$/, \"/api/v1\");\n }\n // Doubao/Ark: https://ark.cn-beijing.volces.com -> https://ark.cn-beijing.volces.com/api/v3\n if (url.includes(\"volces.com\") && !url.includes(\"/api/v3\")) {\n url = url.replace(/\\/$/, \"\") + \"/api/v3\";\n }\n \n cfg.baseURL = url;\n }\n\n // 2.5. Fix MiniMax anthropic URL inherited from main config\n // Main config uses anthropic-messages API, but mem0 needs chatcompletion_v2\n if (cfg.baseURL && typeof cfg.baseURL === \"string\") {\n const url = cfg.baseURL.trim();\n // Detect MiniMax anthropic URL pattern from main config\n if (url.includes(\"api.minimaxi.com\") && url.includes(\"/anthropic\")) {\n // Convert to MiniMax v2 chat completion endpoint\n cfg.baseURL = \"https://api.minimaxi.com/v1/text/chatcompletion_v2\";\n // If provider is anthropic or openai, switch to minimax\n if (prov === \"anthropic\" || prov === \"openai\") {\n prov = \"minimax\";\n }\n }\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n cleaned = cleaned\n .replace(/^preview=/i, \"\")\n .replace(/^response:/i, \"\")\n .replace(/^result:/i, \"\")\n .replace(/^output:/i, \"\")\n .trim();\n\n cleaned = cleaned\n .replace(/```(?:thinking|reasoning|analysis)\\s*[\\s\\S]*?```/gi, \"\")\n .replace(/<\\|begin_of_thought\\|>[\\s\\S]*?<\\|end_of_thought\\|>/gi, \"\")\n .replace(/<think>[\\s\\S]*?<\\/think>/gi, \"\")\n .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, \"\")\n .replace(/<reasoning>[\\s\\S]*?<\\/reasoning>/gi, \"\")\n .trim();\n\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n cleaned = cleaned\n .replace(/^```json\\s*/im, \"\")\n .replace(/^```\\s*/im, \"\")\n .trim();\n\n const jsonStart = cleaned.search(/[{\\[]/);\n if (jsonStart !== -1) {\n let depth = 0;\n let inString = false;\n let escapeNext = false;\n let jsonEnd = -1;\n\n for (let i = jsonStart; i < cleaned.length; i++) {\n const char = cleaned[i];\n \n if (escapeNext) {\n escapeNext = false;\n continue;\n }\n \n if (char === \"\\\\\") {\n escapeNext = true;\n continue;\n }\n \n if (char === '\"' && !inString) {\n inString = true;\n } else if (char === '\"' && inString) {\n inString = false;\n } else if (!inString) {\n if (char === \"{\" || char === \"[\") depth++;\n if (char === \"}\" || char === \"]\") depth--;\n if (depth === 0) {\n jsonEnd = i + 1;\n break;\n }\n }\n }\n\n if (jsonEnd !== -1) {\n const jsonStr = cleaned.slice(jsonStart, jsonEnd);\n try {\n JSON.parse(jsonStr);\n return jsonStr;\n } catch { /* ignore */ }\n }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "16db76005bb45dd75ef4d080b6920ee240c6aee7": {"t": 1785758948.852858, "n": 10651, "h": "772f214d77f51beb", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"kimi\": \"https://api.moonshot.cn/v1\",\n \"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"glm\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n // MiniMax chat-completions endpoint (v2 format) is required for mem0 JSON extraction.\n \"minimax\": \"https://api.minimaxi.com/v1/text/chatcompletion_v2\",\n \"openrouter\": \"https://openrouter.ai/api/v1\",\n \"baichuan\": \"https://api.baichuan-ai.com/v1\",\n \"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"ark\": \"https://ark.cn-beijing.volces.com/api/v3\",\n };\n\n if (KNOWN_PROVIDERS[prov]) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n // Remove common endpoint-level mistakes\n url = url.replace(/\\/chat\\/completions\\/?$/i, \"\");\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Baichuan: https://api.baichuan-ai.com -> https://api.baichuan-ai.com/v1\n if (url.includes(\"api.baichuan-ai.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // OpenRouter: https://openrouter.ai/api -> https://openrouter.ai/api/v1\n if (url.includes(\"openrouter.ai/api\") && !url.includes(\"/api/v1\")) {\n url = url.replace(/\\/$/, \"\").replace(/\\/api$/, \"/api/v1\");\n }\n // Doubao/Ark: https://ark.cn-beijing.volces.com -> https://ark.cn-beijing.volces.com/api/v3\n if (url.includes(\"volces.com\") && !url.includes(\"/api/v3\")) {\n url = url.replace(/\\/$/, \"\") + \"/api/v3\";\n }\n \n cfg.baseURL = url;\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n cleaned = cleaned\n .replace(/^preview=/i, \"\")\n .replace(/^response:/i, \"\")\n .replace(/^result:/i, \"\")\n .replace(/^output:/i, \"\")\n .trim();\n\n cleaned = cleaned\n .replace(/```(?:thinking|reasoning|analysis)\\s*[\\s\\S]*?```/gi, \"\")\n .replace(/<\\|begin_of_thought\\|>[\\s\\S]*?<\\|end_of_thought\\|>/gi, \"\")\n .replace(/<think>[\\s\\S]*?<\\/think>/gi, \"\")\n .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, \"\")\n .replace(/<reasoning>[\\s\\S]*?<\\/reasoning>/gi, \"\")\n .trim();\n\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n cleaned = cleaned\n .replace(/^```json\\s*/im, \"\")\n .replace(/^```\\s*/im, \"\")\n .trim();\n\n const jsonStart = cleaned.search(/[{\\[]/);\n if (jsonStart !== -1) {\n let depth = 0;\n let inString = false;\n let escapeNext = false;\n let jsonEnd = -1;\n\n for (let i = jsonStart; i < cleaned.length; i++) {\n const char = cleaned[i];\n \n if (escapeNext) {\n escapeNext = false;\n continue;\n }\n \n if (char === \"\\\\\") {\n escapeNext = true;\n continue;\n }\n \n if (char === '\"' && !inString) {\n inString = true;\n } else if (char === '\"' && inString) {\n inString = false;\n } else if (!inString) {\n if (char === \"{\" || char === \"[\") depth++;\n if (char === \"}\" || char === \"]\") depth--;\n if (depth === 0) {\n jsonEnd = i + 1;\n break;\n }\n }\n }\n\n if (jsonEnd !== -1) {\n const jsonStr = cleaned.slice(jsonStart, jsonEnd);\n try {\n JSON.parse(jsonStr);\n return jsonStr;\n } catch { /* ignore */ }\n }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "54a7916e0b6b727654afbe4d9b7555d5875e1872": {"t": 1785758950.0713143, "n": 10553, "h": "184c7f37687556af", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"kimi\": \"https://api.moonshot.cn/v1\",\n \"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"glm\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n // MiniMax chat-completions endpoint (v2 format) is required for mem0 JSON extraction.\n \"minimax\": \"https://api.minimaxi.com/v1/text/chatcompletion_v2\",\n \"openrouter\": \"https://openrouter.ai/api/v1\",\n \"baichuan\": \"https://api.baichuan-ai.com/v1\",\n \"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"ark\": \"https://ark.cn-beijing.volces.com/api/v3\",\n };\n\n if (KNOWN_PROVIDERS[prov]) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n // Remove common endpoint-level mistakes\n url = url.replace(/\\/chat\\/completions\\/?$/i, \"\");\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Baichuan: https://api.baichuan-ai.com -> https://api.baichuan-ai.com/v1\n if (url.includes(\"api.baichuan-ai.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // OpenRouter: https://openrouter.ai/api -> https://openrouter.ai/api/v1\n if (url.includes(\"openrouter.ai/api\") && !url.includes(\"/api/v1\")) {\n url = url.replace(/\\/$/, \"\").replace(/\\/api$/, \"/api/v1\");\n }\n // Doubao/Ark: https://ark.cn-beijing.volces.com -> https://ark.cn-beijing.volces.com/api/v3\n if (url.includes(\"volces.com\") && !url.includes(\"/api/v3\")) {\n url = url.replace(/\\/$/, \"\") + \"/api/v3\";\n }\n \n cfg.baseURL = url;\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n cleaned = cleaned\n .replace(/^preview=/i, \"\")\n .replace(/^response:/i, \"\")\n .replace(/^result:/i, \"\")\n .replace(/^output:/i, \"\")\n .trim();\n\n cleaned = cleaned\n .replace(/```(?:thinking|reasoning|analysis)\\s*[\\s\\S]*?```/gi, \"\")\n .replace(/<\\|begin_of_thought\\|>[\\s\\S]*?<\\|end_of_thought\\|>/gi, \"\")\n .replace(/<think>[\\s\\S]*?<\\/think>/gi, \"\")\n .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, \"\")\n .replace(/<reasoning>[\\s\\S]*?<\\/reasoning>/gi, \"\")\n .trim();\n\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n const jsonStart = cleaned.search(/[{\\[]/);\n if (jsonStart !== -1) {\n let depth = 0;\n let inString = false;\n let escapeNext = false;\n let jsonEnd = -1;\n\n for (let i = jsonStart; i < cleaned.length; i++) {\n const char = cleaned[i];\n \n if (escapeNext) {\n escapeNext = false;\n continue;\n }\n \n if (char === \"\\\\\") {\n escapeNext = true;\n continue;\n }\n \n if (char === '\"' && !inString) {\n inString = true;\n } else if (char === '\"' && inString) {\n inString = false;\n } else if (!inString) {\n if (char === \"{\" || char === \"[\") depth++;\n if (char === \"}\" || char === \"]\") depth--;\n if (depth === 0) {\n jsonEnd = i + 1;\n break;\n }\n }\n }\n\n if (jsonEnd !== -1) {\n const jsonStr = cleaned.slice(jsonStart, jsonEnd);\n try {\n JSON.parse(jsonStr);\n return jsonStr;\n } catch { /* ignore */ }\n }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "960ef094e2af947d8098f9802c956bab16684109": {"t": 1785758951.6136603, "n": 9797, "h": "b7985a92a54a17bf", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"kimi\": \"https://api.moonshot.cn/v1\",\n \"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"glm\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n // MiniMax chat-completions endpoint (v2 format) is required for mem0 JSON extraction.\n \"minimax\": \"https://api.minimaxi.com/v1/text/chatcompletion_v2\",\n \"openrouter\": \"https://openrouter.ai/api/v1\",\n \"baichuan\": \"https://api.baichuan-ai.com/v1\",\n \"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"ark\": \"https://ark.cn-beijing.volces.com/api/v3\",\n };\n\n if (KNOWN_PROVIDERS[prov]) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n // Remove common endpoint-level mistakes\n url = url.replace(/\\/chat\\/completions\\/?$/i, \"\");\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Baichuan: https://api.baichuan-ai.com -> https://api.baichuan-ai.com/v1\n if (url.includes(\"api.baichuan-ai.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // OpenRouter: https://openrouter.ai/api -> https://openrouter.ai/api/v1\n if (url.includes(\"openrouter.ai/api\") && !url.includes(\"/api/v1\")) {\n url = url.replace(/\\/$/, \"\").replace(/\\/api$/, \"/api/v1\");\n }\n // Doubao/Ark: https://ark.cn-beijing.volces.com -> https://ark.cn-beijing.volces.com/api/v3\n if (url.includes(\"volces.com\") && !url.includes(\"/api/v3\")) {\n url = url.replace(/\\/$/, \"\") + \"/api/v3\";\n }\n \n cfg.baseURL = url;\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\n/**\n * Clean JSON response from LLMs that might wrap it in markdown or add commentary.\n */\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n cleaned = cleaned\n .replace(/```(?:thinking|reasoning|analysis)\\s*[\\s\\S]*?```/gi, \"\")\n .replace(/<\\|begin_of_thought\\|>[\\s\\S]*?<\\|end_of_thought\\|>/gi, \"\")\n .replace(/<think>[\\s\\S]*?<\\/think>/gi, \"\")\n .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, \"\")\n .replace(/<reasoning>[\\s\\S]*?<\\/reasoning>/gi, \"\")\n .trim();\n\n // Strategy 1: Full-text code block\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n // Strategy 2: Last code block\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n // Strategy 3: Raw JSON object/array\n const jsonMatch = cleaned.match(/(\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])/);\n if (jsonMatch) {\n try {\n JSON.parse(jsonMatch[1]);\n return jsonMatch[1];\n } catch { /* ignore */ }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "abd3e0b292fa9f8c518772debcf016effa9fcf0f": {"t": 1785758952.7641058, "n": 9683, "h": "d686f5af7b4eddd7", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"kimi\": \"https://api.moonshot.cn/v1\",\n \"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"glm\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"minimax\": \"https://api.minimax.chat/v1\",\n \"openrouter\": \"https://openrouter.ai/api/v1\",\n \"baichuan\": \"https://api.baichuan-ai.com/v1\",\n \"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"ark\": \"https://ark.cn-beijing.volces.com/api/v3\",\n };\n\n if (KNOWN_PROVIDERS[prov]) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n // Remove common endpoint-level mistakes\n url = url.replace(/\\/chat\\/completions\\/?$/i, \"\");\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Baichuan: https://api.baichuan-ai.com -> https://api.baichuan-ai.com/v1\n if (url.includes(\"api.baichuan-ai.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // OpenRouter: https://openrouter.ai/api -> https://openrouter.ai/api/v1\n if (url.includes(\"openrouter.ai/api\") && !url.includes(\"/api/v1\")) {\n url = url.replace(/\\/$/, \"\").replace(/\\/api$/, \"/api/v1\");\n }\n // Doubao/Ark: https://ark.cn-beijing.volces.com -> https://ark.cn-beijing.volces.com/api/v3\n if (url.includes(\"volces.com\") && !url.includes(\"/api/v3\")) {\n url = url.replace(/\\/$/, \"\") + \"/api/v3\";\n }\n \n cfg.baseURL = url;\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\n/**\n * Clean JSON response from LLMs that might wrap it in markdown or add commentary.\n */\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n cleaned = cleaned\n .replace(/```(?:thinking|reasoning|analysis)\\s*[\\s\\S]*?```/gi, \"\")\n .replace(/<\\|begin_of_thought\\|>[\\s\\S]*?<\\|end_of_thought\\|>/gi, \"\")\n .replace(/<think>[\\s\\S]*?<\\/think>/gi, \"\")\n .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, \"\")\n .replace(/<reasoning>[\\s\\S]*?<\\/reasoning>/gi, \"\")\n .trim();\n\n // Strategy 1: Full-text code block\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n // Strategy 2: Last code block\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n // Strategy 3: Raw JSON object/array\n const jsonMatch = cleaned.match(/(\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])/);\n if (jsonMatch) {\n try {\n JSON.parse(jsonMatch[1]);\n return jsonMatch[1];\n } catch { /* ignore */ }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "01bc688b75dde8eca2623035938cfdcc4b675c9e": {"t": 1785758954.0184228, "n": 8242, "h": "cd7b3cdcf51bc4f1", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"minimax\": \"https://api.minimax.chat/v1\",\n };\n\n if (KNOWN_PROVIDERS[prov]) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n \n cfg.baseURL = url;\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\n/**\n * Clean JSON response from LLMs that might wrap it in markdown or add commentary.\n */\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n // Strategy 1: Full-text code block\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n // Strategy 2: Last code block\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n // Strategy 3: Raw JSON object/array\n const jsonMatch = cleaned.match(/(\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])/);\n if (jsonMatch) {\n try {\n JSON.parse(jsonMatch[1]);\n return jsonMatch[1];\n } catch { /* ignore */ }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to:\n * 1. Strip markdown code blocks from JSON responses\n * 2. Handle providers that don't support response_format (MiniMax, etc.)\n */\nexport class JsonCleaningLLM {\n private static JSON_PROMPT = `\\n\\nIMPORTANT: You must respond with valid JSON only. No markdown, no explanations, just the JSON object.`;\n \n constructor(\n private wrappedLLM: any, \n private logger?: OpenClawPluginApi[\"logger\"],\n private config?: Record<string, unknown>\n ) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n // Handle providers that don't support response_format\n let modifiedMessages = messages;\n let modifiedFormat = responseFormat;\n \n if (responseFormat?.type === 'json_object') {\n const providerInfo = this.detectProvider();\n \n if (providerInfo.needsJsonWorkaround) {\n this.logger?.debug?.(`[mem0] Provider '${providerInfo.name}' doesn't support json_object mode, using prompt-based JSON enforcement`);\n \n // Remove the unsupported response_format\n modifiedFormat = undefined;\n \n // Add JSON enforcement to the last user message\n modifiedMessages = messages.map((msg, idx) => {\n if (idx === messages.length - 1 && msg.role === 'user') {\n return { ...msg, content: msg.content + JsonCleaningLLM.JSON_PROMPT };\n }\n return msg;\n });\n }\n }\n\n const response = await this.wrappedLLM.generateResponse(modifiedMessages, modifiedFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n /**\n * Detect the LLM provider and whether it needs JSON mode workaround\n */\n private detectProvider(): { name: string; needsJsonWorkaround: boolean } {\n const baseURL = (this.config?.baseURL || this.config?.baseUrl || '') as string;\n const model = (this.config?.model || '') as string;\n \n // Providers known to NOT support response_format: json_object\n const noJsonModeProviders = [\n { pattern: 'minimax', name: 'MiniMax' },\n { pattern: 'api.minimax', name: 'MiniMax' },\n { pattern: 'abab', name: 'MiniMax' },\n { pattern: 'minimaxi', name: 'MiniMax' },\n ];\n \n // Providers that DO support json_object (no workaround needed)\n const supportsJsonMode = [\n 'openai.com',\n 'api.openai.com',\n 'api.deepseek.com',\n 'api.moonshot.cn',\n 'claude',\n 'anthropic',\n 'api.anthropic',\n ];\n\n const urlLower = baseURL.toLowerCase();\n const modelLower = model.toLowerCase();\n\n // Check if it's a known no-json-mode provider\n for (const provider of noJsonModeProviders) {\n if (urlLower.includes(provider.pattern) || modelLower.includes(provider.pattern)) {\n return { name: provider.name, needsJsonWorkaround: true };\n }\n }\n\n // Check if it's a known json-mode supporter\n for (const provider of supportsJsonMode) {\n if (urlLower.includes(provider)) {\n return { name: provider, needsJsonWorkaround: false };\n }\n }\n\n // Default for non-OpenAI URLs: assume no json_object support\n // This is safer for Chinese/local LLMs\n if (urlLower && !urlLower.includes('openai.com')) {\n return { name: baseURL.split('//')[1]?.split('/')[0] || 'unknown', needsJsonWorkaround: true };\n }\n\n return { name: 'unknown', needsJsonWorkaround: false };\n }\n}\n"}, "59f1b06d8a48876197e9ad550506d3a22cd0c1fd": {"t": 1785758955.337165, "n": 5301, "h": "8266e5d6a20d0f36", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Smartly fix common configuration mistakes for LLM providers, especially for Chinese models.\n */\nexport function fixLlmConfig(provider: string, config: Record<string, unknown>): { provider: string; config: Record<string, unknown> } {\n const cfg = { ...config };\n let prov = provider.toLowerCase();\n\n // 1. Normalize provider names (allow users to say \"deepseek\" instead of \"openai\")\n const KNOWN_PROVIDERS: Record<string, string> = {\n \"deepseek\": \"https://api.deepseek.com/v1\",\n \"moonshot\": \"https://api.moonshot.cn/v1\",\n \"yi\": \"https://api.lingyiwanwu.com/v1\",\n \"siliconflow\": \"https://api.siliconflow.cn/v1\",\n \"dashscope\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"zhipu\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"minimax\": \"https://api.minimax.chat/v1\",\n };\n\n if (KNOWN_PROVIDERS[prov]) {\n // If user used a specific provider name, switch to \"openai\" but inject the correct baseURL\n if (!cfg.baseURL) {\n cfg.baseURL = KNOWN_PROVIDERS[prov];\n }\n // DeepSeek specific: ensure v1 suffix if user manually typed it wrong\n if (prov === \"deepseek\" && cfg.baseURL && typeof cfg.baseURL === \"string\" && !cfg.baseURL.endsWith(\"/v1\")) {\n cfg.baseURL = cfg.baseURL.replace(/\\/$/, \"\") + \"/v1\";\n }\n prov = \"openai\";\n }\n\n // 2. Fix common URL mistakes for OpenAI-compatible providers\n if (prov === \"openai\" && cfg.baseURL && typeof cfg.baseURL === \"string\") {\n let url = cfg.baseURL.trim();\n \n // DeepSeek: https://api.deepseek.com -> https://api.deepseek.com/v1\n if (url.includes(\"api.deepseek.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Moonshot: https://api.moonshot.cn -> https://api.moonshot.cn/v1\n if (url.includes(\"api.moonshot.cn\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n // Yi: https://api.lingyiwanwu.com -> https://api.lingyiwanwu.com/v1\n if (url.includes(\"api.lingyiwanwu.com\") && !url.includes(\"/v1\")) {\n url = url.replace(/\\/$/, \"\") + \"/v1\";\n }\n \n cfg.baseURL = url;\n }\n\n // 3. Ollama: ensure url (not baseURL)\n if (prov === \"ollama\") {\n if (cfg.baseURL && !cfg.url) {\n cfg.url = cfg.baseURL; // User mistake: used baseURL for ollama\n delete cfg.baseURL;\n }\n }\n\n return { provider: prov, config: cfg };\n}\n\n/**\n * Clean JSON response from LLMs that might wrap it in markdown or add commentary.\n */\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n // Strategy 1: Full-text code block\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n // Strategy 2: Last code block\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n // Strategy 3: Raw JSON object/array\n const jsonMatch = cleaned.match(/(\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])/);\n if (jsonMatch) {\n try {\n JSON.parse(jsonMatch[1]);\n return jsonMatch[1];\n } catch { /* ignore */ }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to strip markdown code blocks from JSON responses.\n */\nexport class JsonCleaningLLM {\n constructor(private wrappedLLM: any, private logger?: OpenClawPluginApi[\"logger\"]) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n const response = await this.wrappedLLM.generateResponse(messages, responseFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n}\n"}, "17181f9e779bf8812c161547a96d10f09e062a24": {"t": 1785758957.5322957, "n": 2971, "h": "f6e6d07abbe96563", "c": "\nimport { type OpenClawPluginApi } from \"openclaw/plugin-sdk\";\n\nexport function resolveEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, envVar) => {\n const envValue = process.env[envVar];\n if (!envValue) {\n throw new Error(`Environment variable ${envVar} is not set`);\n }\n return envValue;\n });\n}\n\nexport function resolveEnvVarsDeep(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === \"string\") {\n result[key] = resolveEnvVars(value);\n } else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n result[key] = resolveEnvVarsDeep(value as Record<string, unknown>);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Clean JSON response from LLMs that might wrap it in markdown or add commentary.\n */\nexport function cleanJsonResponse(content: string): string {\n if (!content || typeof content !== \"string\") return content;\n\n let cleaned = content.trim();\n\n // Strategy 1: Full-text code block\n const fullMatch = cleaned.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/);\n if (fullMatch) return fullMatch[1].trim();\n\n // Strategy 2: Last code block\n const codeBlocks = [...cleaned.matchAll(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/g)];\n if (codeBlocks.length > 0) {\n const lastBlock = codeBlocks[codeBlocks.length - 1][1].trim();\n if (lastBlock.startsWith(\"{\") || lastBlock.startsWith(\"[\")) return lastBlock;\n return lastBlock;\n }\n\n // Strategy 3: Raw JSON object/array\n const jsonMatch = cleaned.match(/(\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])/);\n if (jsonMatch) {\n try {\n JSON.parse(jsonMatch[1]);\n return jsonMatch[1];\n } catch { /* ignore */ }\n }\n\n return cleaned;\n}\n\n/**\n * Wraps an LLM instance to strip markdown code blocks from JSON responses.\n */\nexport class JsonCleaningLLM {\n constructor(private wrappedLLM: any, private logger?: OpenClawPluginApi[\"logger\"]) { }\n\n async generateResponse(\n messages: Array<{ role: string; content: string }>,\n responseFormat?: any,\n tools?: any\n ): Promise<string | { content: string; role: string; toolCalls?: any[] }> {\n const response = await this.wrappedLLM.generateResponse(messages, responseFormat, tools);\n\n if (typeof response === \"string\") {\n return cleanJsonResponse(response);\n }\n\n if (response && typeof response === \"object\" && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n\n async generateChat(\n messages: Array<{ role: string; content: string }>\n ): Promise<{ content: string; role: string }> {\n const response = await this.wrappedLLM.generateChat(messages);\n\n if (response && response.content) {\n return {\n ...response,\n content: cleanJsonResponse(response.content),\n };\n }\n\n return response;\n }\n}\n"}}, "README.md": {"9d6c99b2dcd0e880213081cf959a65d63494dd3e": {"t": 1786068581.7057314, "n": 10018, "h": "f519575e2a69efa0", "c": "# 🧠 OpenClaw Mem0 智能记忆插件\n\n> **让 AI 拥有长期记忆,同时节省 70% 的 Token 消耗**\n\n基于 Mem0 构建的下一代智能记忆系统,专为 OpenClaw 设计。\n\n<p align=\"center\">\n <img src=\"https://img.shields.io/badge/version-0.6.4-blue.svg\" alt=\"Version\" />\n <img src=\"https://img.shields.io/badge/OpenClaw-2026.2+-green.svg\" alt=\"OpenClaw\" />\n <img src=\"https://img.shields.io/badge/license-Apache%202.0-orange.svg\" alt=\"License\" />\n</p>\n\n---\n\n## ⚡ 30 秒极速安装\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/1960697431/openclaw-mem0/main/install.sh | bash\n```\n\n安装脚本会自动检查 `curl`/`unzip`/`npm`,并在下载失败时自动重试。\n\n安装完成后重启 Gateway:\n```bash\nopenclaw gateway restart\n```\n\n如果你是老版本(如 `0.4.6`)且启动时报错 `Cannot find module ...contextManager.js`,请执行一次强制重装:\n\n```bash\nrm -rf ~/.openclaw/extensions/openclaw-mem0 ~/.openclaw/extensions/openclaw-meme\ncurl -fsSL https://raw.githubusercontent.com/1960697431/openclaw-mem0/main/install.sh | bash\nopenclaw gateway restart\n```\n\n可选:检查当前插件版本\n\n```bash\npython3 - <<'PY'\nimport json, os\np = os.path.expanduser(\"~/.openclaw/extensions/openclaw-mem0/package.json\")\nprint(json.load(open(p, \"r\", encoding=\"utf-8\")).get(\"version\"))\nPY\n```\n\n*首次启动会自动下载嵌入模型(~417MB),请等待 2-3 分钟。*\n\n---\n\n## 🏆 三方对比:为什么选择我们?\n\n| 特性 | OpenClaw Mem0 | 自带记忆 | 传统方案 |\n| :--- | :---: | :---: | :---: |\n| Token 智能管理 | ✅ 自动预算 | ❌ 无限制 | ❌ 全量读取 |\n| 语义搜索 | ✅ 向量检索 | ❌ 关键词 | ⚠️ 基础搜索 |\n| 冷热分离 | ✅ 自动归档 | ❌ 单文件 | ❌ 无分层 |\n| 记忆修剪 | ✅ 智能清理 | ❌ 永久堆积 | ❌ 手动管理 |\n| 并发安全 | ✅ 写入队列 | ❌ 无保护 | ⚠️ 可能锁死 |\n| 国产模型适配 | ✅ 自动修正 | ⚠️ 手动配置 | ⚠️ 手动配置 |\n| 零配置启动 | ✅ 继承配置 | ✅ 默认启用 | ❌ 复杂配置 |\n| 自动更新 | ✅ 后台静默 | ❌ 手动升级 | ❌ 手动升级 |\n| 状态监控 | ✅ 实时面板 | ❌ 无 | ❌ 无 |\n\n### 💰 Token 节省实测\n\n在 1000+ 条记忆的场景下:\n\n| 方案 | 注入 Token | 有效信息比 |\n| :--- | :---: | :---: |\n| OpenClaw Mem0 | ~800 | 92% ✅ |\n| 全量注入 | ~8000 | 15% ❌ |\n| 关键词匹配 | ~3000 | 45% ⚠️ |\n\n**结论:我们的方案在保证信息质量的同时,节省了约 70-90% 的 Token。**\n\n---\n\n## 🌟 核心技术\n\n### 1. 📊 智能上下文注入 (Smart Context Injection)\n\n**这是我们的核心优势,也是节省 Token 的关键:**\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Token 预算管理器 │\n├─────────────────────────────────────────────────────────┤\n│ 模型上下文: 128,000 tokens (GPT-4o) │\n│ 记忆预算: 1,920 tokens (1.5%) │\n│ │\n│ 100 条记忆 → 智能筛选 → 5 条最相关 → 800 tokens 注入 │\n│ │\n│ ✅ 按相关度排序 ✅ 中英文 Token 估算 ✅ 自动截断 │\n└─────────────────────────────────────────────────────────┘\n```\n\n**支持的模型上下文限制:**\n- GPT-4o / GPT-4-Turbo: 128K tokens\n- Claude 3.x: 200K tokens\n- DeepSeek: 64K tokens\n- Moonshot: 32K tokens\n- 自动识别,无需配置\n\n### 2. 🔥 冷热分离架构\n\n| | 🔥 **热库 (Hot)** | 🧊 **冷库 (Cold)** |\n|:---|:---|:---|\n| **存储** | SQLite + 向量索引 | JSONL 归档文件 |\n| **容量** | ≤ 2000 条 (可配置) | 无限 |\n| **速度** | 毫秒级 | 秒级 |\n| **内存** | ~1.5GB | ~0MB |\n| **场景** | 日常对话 | 历史查询 |\n\n### 3. 🔒 并发写入保护\n\n```typescript\nWriteQueue → 串行化写入 → 防止 SQLITE_BUSY → 稳定可靠\n```\n\n### 4. 📊 实时状态监控\n\n```bash\n# CLI 命令\nopenclaw mem0 stats # 简洁统计\nopenclaw mem0 dashboard # 美化面板\n\n# 或查看状态文件\ncat ~/.openclaw/data/mem0/mem0-status.json\n```\n\n---\n\n## 🛠️ 可用工具\n\n| 工具 | 功能 | 智能特性 |\n|:---|:---|:---|\n| `memory_search` | 搜索记忆 | `deep: true` 穿透冷库 |\n| `memory_store` | 存储记忆 | 自动去重、提取事实 |\n| `memory_list` | 列出记忆 | 显示 `id + 摘要`,支持 `limit` |\n| `memory_forget` | 删除记忆 | 支持 `query + deleteAll` 批量删除 |\n| `memory_stats` | 🆕 状态统计 | Token、存储、队列信息 |\n\n---\n\n## 🔧 配置选项\n\n### 极简模式(推荐)\n\n```json\n\"openclaw-mem0\": {\n \"enabled\": true\n}\n```\n\n自动继承 OpenClaw 主 LLM 配置,开箱即用。\n\n**支持的 API Key 来源(自动识别优先级):**\n1. 配置文件中的 `apiKey` 字段(支持 `${ENV_VAR}` 语法)\n2. 环境变量(如 `MINIMAX_API_KEY`、`DEEPSEEK_API_KEY`)\n3. `~/.openclaw/credentials/<provider>.json` 文件\n4. OpenClaw 主配置的 `auth` 认证系统\n\n示例:\n```json\n\"openclaw-mem0\": {\n \"enabled\": true,\n \"config\": {\n \"apiKey\": \"${MINIMAX_API_KEY}\"\n }\n}\n```\n\n### 进阶模式\n\n```json\n\"openclaw-mem0\": {\n \"enabled\": true,\n \"config\": {\n \"provider\": \"deepseek\",\n \"apiKey\": \"sk-xxx\",\n \"maxMemoryCount\": 2000,\n \"topK\": 5,\n \"searchThreshold\": 0.5\n }\n}\n```\n\n| 参数 | 说明 | 默认值 |\n|:---|:---|:---|\n| `provider` | LLM 提供商 (deepseek/moonshot/openai...) | 继承主配置 |\n| `maxMemoryCount` | 热库最大记忆数 | 2000 |\n| `topK` | 搜索返回数量 | 5 |\n| `searchThreshold` | 相关度阈值 | 0.5 |\n\n### MiniMax 推荐配置(避坑)\n\n如果你使用 MiniMax,建议显式指定 `baseURL` 为 v2 聊天端点,避免返回 HTML 页面或空 JSON 导致事实提取失败:\n\n```json\n\"openclaw-mem0\": {\n \"enabled\": true,\n \"config\": {\n \"provider\": \"minimax\",\n \"apiKey\": \"your-minimax-api-key\",\n \"baseURL\": \"https://api.minimaxi.com/v1/text/chatcompletion_v2\",\n \"model\": \"abab6.5-chat\"\n }\n}\n```\n\n补充说明:\n- 插件已支持自动清理 `<think>`/`reasoning` 等思考内容,再做 JSON 解析。\n- 当模型在 JSON 模式下返回空内容或无效 JSON 时,会自动降级为 `{}`,避免 `Unexpected end of JSON input` 中断写入。\n\n### 自动捕捉调优(高级)\n\n如果你发现自动捕捉太“敏感”或太“保守”,可以用以下环境变量微调:\n\n| 变量名 | 说明 | 默认值 |\n|:---|:---|:---|\n| `MEM0_CAPTURE_BATCH_WINDOW_MS` | 批量窗口(毫秒) | `1200` |\n| `MEM0_CAPTURE_BATCH_MAX_MSGS` | 每次写入最多消息条数 | `16` |\n| `MEM0_CAPTURE_INPUT_MAX_MSGS` | 缓冲区保留的输入消息数 | `12` |\n| `MEM0_CAPTURE_MAX_CHARS_PER_MSG` | 单条消息最大字符 | `500` |\n| `MEM0_CAPTURE_MAX_TOTAL_CHARS` | 单批总字符预算 | `2600` |\n| `MEM0_CAPTURE_MIN_CHARS` | 过滤超短低价值消息阈值 | `6` |\n| `MEM0_CAPTURE_DUP_TTL_MS` | 重复批次去重 TTL(毫秒) | `600000` |\n| `MEM0_CAPTURE_DUP_MAX` | 重复指纹缓存上限 | `1024` |\n\n自动捕捉现在会优先过滤低信号回复、截断超长消息、跳过重复批次,并确保批次中至少包含用户消息,避免噪音写入。\n\n---\n\n## 🔄 版本历史\n\n### v0.6.4 (API Key 自动解析与 MiniMax 兼容性)\n- 🆕 **API Key 自动解析**: 支持从环境变量、`~/.openclaw/credentials/` 文件、OpenClaw 主配置自动读取 API Key\n- 🆕 **MiniMax 思考模式兼容**: 自动清理 `<think>` 标签,解决 MiniMax M2.5 推理模式下的 JSON 解析错误\n- 🆕 **零配置增强**: 无需手动配置 API Key,插件自动继承 OpenClaw 主 LLM 的认证信息\n\n### v0.6.3 (兼容性修复)\n- 🛟 **老版本救援**: 插件入口切换为桥接 `index.ts`,启动时可自动补齐缺失 `src/*` 文件,避免因部分更新直接崩溃。\n- 🛠️ **拼写兼容**: 新增 `contextMenager.ts` 兼容别名,修复旧版错误导入导致的 `Cannot find module`。\n- 🧰 **安装器增强**: 安装时自动清理旧目录 `openclaw-meme`,减少升级残留冲突。\n\n### v0.6.2 (自动捕捉与可用性优化)\n- 🆕 **自动捕捉降噪**: 增加低信号消息过滤、超长内容预算裁剪、重复批次去重(TTL 指纹),减少无效写入。\n- 🆕 **删除体验增强**: `memory_list`/`memory_search` 直接展示 `id`,`memory_forget` 支持 `query + deleteAll` 批量删除。\n- 🐛 **稳定性改进**: 自动捕捉仅在含用户消息时触发,并过滤大段结构化噪音,降低 JSON 解析异常概率。\n\n### v0.6.1 (兼容性热修复)\n- 🐛 修复部分模型在 `json_object` 模式下返回空内容导致 `Unexpected end of JSON input` 的问题(增加 JSON 兜底)。\n- 🐛 修复部分错误 `baseURL` 返回 HTML(`<!DOCTYPE ...`)时的解析失败,错误信息更明确。\n- ✅ MiniMax 端点规范化:默认与推荐配置统一到 `.../v1/text/chatcompletion_v2`。\n\n### v0.6.0 (重大性能与兼容性更新)\n- 🆕 **多格式远端 Embedding**: 支持 Gemini、Ollama、OpenAI 等多种远端向量模型,不再局限于本地运行。\n- 🆕 **速度大幅提升**: 搜索请求并行化处理,增加搜索结果 LRU 缓存,对话捕获改为异步批处理。\n- 🆕 **国产模型深度适配**: 增强了对 DeepSeek、Kimi、智谱、MiniMax、通义千问等国产模型的思考模式(Thinking/Reasoning)识别与过滤。\n- 🆕 **反思引擎重构**: `ReflectionEngine` 现在复用统一 LLM 适配层,支持所有已配置的提供商。\n- 🐛 **稳定性修复**: 补齐了自动更新漏掉的核心文件,修复了 SQLite 并发初始化锁竞争问题。\n\n### v0.5.0 (通用模型支持)\n- 🆕 **Universal LLM Support**: 支持所有 OpenAI 兼容的 LLM 提供商\n- 🐛 修复 MiniMax 等国产模型不支持 `response_format: json_object` 的问题\n- 🆕 自动检测提供商并应用 JSON 模式变通方案\n- ✅ 已测试:MiniMax、DeepSeek、Moonshot、智谱、零一万物\n\n### v0.4.9 (零配置增强)\n- 🐛 修复 `{ \"enabled\": true }` 最小配置启动失败的问题\n- 🆕 支持 OpenClaw 新版 `models.providers` 配置格式\n- 🐛 修复 README 表格对齐问题\n\n### v0.4.8 (智能优化)\n- 🆕 **Smart Context Injection**: Token 预算管理,自动适配模型上下文\n- 🆕 **Write Queue**: 并发写入保护,彻底解决 SQLITE_BUSY\n- 🆕 **Status Dashboard**: 实时监控面板,CLI 命令增强\n\n### v0.4.7 (稳定性)\n- 🐛 修复 SQLITE_CANTOPEN 错误\n- 🐛 修复自动更新死循环\n\n### v0.4.6 (数据安全)\n- 🆕 旧版 memory.md 数据迁移工具\n\n### v0.4.5 (零配置)\n- 🆕 自动继承主 LLM 配置\n\n### v0.4.3 (深度检索)\n- 🆕 冷库搜索功能\n\n---\n\n## 🛠️ 故障排除指南\n\n### 安装前检查\n\n运行环境检查脚本,确保系统满足所有要求:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/1960697431/openclaw-mem0/main/check-env.sh | bash\n```\n\n### 常见问题\n\n#### 1. 安装失败:找不到 Node.js\n\n**错误信息**:\n```\n❌ Error: npm is not installed. Please install Node.js first\n```\n\n**解决方案**:\n```bash\n# macOS\nbrew install node\n\n# Ubuntu/Debian\ncurl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -\nsudo apt-get install -y nodejs\n\n# 或使用 nvm\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash\nnvm install 20\nnvm use 20\n```\n\n#### 2. 安装失败:网络超时\n\n**错误信息**:\n```\n⏱️ Download timeout for src/index.ts after 30000ms\n```\n\n**解决方案**:\n- 检查网络连接:`ping github.com`\n- 如果使用代理,设置环境变量:\n ```bash\n export HTTP_PROXY=http://proxy.example.com:8080\n export HTTPS_PROXY=http://proxy.example.com:8080\n ```\n- 手动安装:下载 ZIP 包后解压到 `~/.openclaw/extensions/openclaw-mem0/`\n\n#### 3. 启动失败:找不到模块\n\n**错误信息**:\n```\nError: Cannot find module '.../contextManager.js'\n```\n\n**解决方案**:\n```bash\n# 强制重装\nrm -rf ~/.openclaw/extensions/openclaw-mem0 ~/.openclaw/extensions/openclaw-meme\ncurl -fsSL https://raw.githubusercontent.com/1960697431/openclaw-mem0/main/install.sh | bash\nopenclaw gateway restart\n```\n\n#### 4. 启动失败:没有配置 LLM\n\n**错误信息**:\n```\n[mem0] CRITICAL: No LLM configured for memory extraction\n```\n\n**解决方案**:\n1. 确保 OpenClaw 主配置中有 LLM:\n ```bash\n openclaw config set models.providers.minimax.apiKey \"your-api-key\"\n openclaw config set models.providers.minimax.models '[{\"id\": \"MiniMax-M2.5\"}]'\n ```\n\n2. 或在插件配置中指定:\n ```json\n {\n \"openclaw-mem0\": {\n \"enabled\": true,\n \"provider\": \"minimax\",\n \"apiKey\": \"your-api-key\",\n \"model\": \"MiniMax-M2.5\"\n }\n }\n ```\n\n#### 5. 记忆提取失败:空 JSON 响应\n\n**错误信息**:\n```\n[mem0] LLM returned empty JSON-mode response, falling back to {}\n```\n\n**解决方案**:\n- 这是正常降级行为,插件会自动处理\n- 如果频繁出现,检查 API Key 是否有效\n- 对于 MiniMax,确保使用正确的端点:\n ```\n https://api.minimaxi.com/v1/text/chatcompletion_v2\n ```\n\n#### 6. 首次启动慢\n\n**现象**:\n安装后首次启动需要 2-3 分钟\n\n**原因**:\n- 首次启动需要下载嵌入模型(约 417MB)\n- 这是正常的,请耐心等待\n\n**加速方法**(中国大陆用户):\n```bash\n# 设置 HuggingFace 镜像\nexport HF_ENDPOINT=https://hf-mirror.com\nopenclaw gateway restart\n```\n\n#### 7. 数据库权限错误\n\n**错误信息**:\n```\nSQLITE_CANTOPEN: unable to open database file\n```\n\n**解决方案**:\n```bash\n# 检查并修复权限\nchmod u+w ~/.openclaw/data\nchmod u+w ~/.openclaw/data/mem0\n\n# 如果目录不存在,手动创建\nmkdir -p ~/.openclaw/data/mem0\n```\n\n### 获取帮助\n\n如果以上方法无法解决问题:\n\n1. **查看日志**:\n ```bash\n tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | grep mem0\n ```\n\n2. **检查版本**:\n ```bash\n cat ~/.openclaw/extensions/openclaw-mem0/package.json | grep version\n ```\n\n3. **提交 Issue**:\n - 访问:https://github.com/1960697431/openclaw-mem0/issues\n - 提供:错误日志、系统版本、插件版本\n\n---\n\n## 📄 License\n\nApache 2.0\n\n---\n\n## 🙏 致谢\n\n- [Mem0](https://github.com/mem0ai/mem0) - 核心记忆引擎\n- [OpenClaw](https://github.com/qingchencloud/openclaw) - 宿主平台\n"}}}}