/** * OpenClaw KnowHub Plugin * * Knowledge management integration for OpenClaw agents. * Provides tools for searching and saving knowledge, with automatic reminders. */ import { Type } from "@sinclair/typebox"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; // ============================================================================ // Types // ============================================================================ type KnowHubConfig = { apiUrl: string; submittedBy: string; reminderMode: "off" | "minimal" | "normal" | "aggressive"; enableServerExtraction: boolean; privacyMode: "strict" | "relaxed"; }; type KnowledgeSearchResult = { id: string; task: string; content: string; types: string[]; eval: { score: number; helpful: number; harmful: number; confidence: number; }; quality_score: number; source?: { name?: string; urls?: string[]; }; }; // ============================================================================ // Security // ============================================================================ const PROMPT_INJECTION_PATTERNS = [ /ignore (all|any|previous|above|prior) instructions/i, /do not follow (the )?(system|developer)/i, /system prompt/i, /<\s*(system|assistant|developer|tool)\b/i, ]; function looksLikePromptInjection(text: string): boolean { const normalized = text.replace(/\s+/g, " ").trim(); return PROMPT_INJECTION_PATTERNS.some((pattern) => pattern.test(normalized)); } export { looksLikePromptInjection }; const PROMPT_ESCAPE_MAP: Record = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; function escapeForPrompt(text: string): string { return text.replace(/[&<>"']/g, (char) => PROMPT_ESCAPE_MAP[char] ?? char); } export { escapeForPrompt }; function sanitizeMessage(msg: unknown, mode: "strict" | "relaxed"): object { if (!msg || typeof msg !== "object") { return {}; } const msgObj = msg as Record; const content = msgObj.content; if (mode === "relaxed") { return { role: msgObj.role, content, }; } // strict mode: redact sensitive info if (typeof content === "string") { return { role: msgObj.role, content: redactSensitiveInfo(content), }; } return { role: msgObj.role, content, }; } function redactSensitiveInfo(text: string): string { return text .replace(/\/Users\/[^\/\s]+/g, "/Users/[REDACTED]") .replace(/\/home\/[^\/\s]+/g, "/home/[REDACTED]") .replace(/C:\\Users\\[^\\\s]+/g, "C:\\Users\\[REDACTED]") .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "[EMAIL]") .replace(/\b\d{3}-\d{3}-\d{4}\b/g, "[PHONE]") .replace(/\+\d{10,}/g, "[PHONE]") .replace(/sk-[a-zA-Z0-9]{32,}/g, "[API_KEY]") .replace(/Bearer\s+[a-zA-Z0-9_-]+/g, "Bearer [TOKEN]") .replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, "[IP]"); } export { redactSensitiveInfo }; // ============================================================================ // Helper Functions // ============================================================================ function getReminderInterval(mode: string): number { switch (mode) { case "minimal": return 5; case "normal": return 3; case "aggressive": return 2; default: return 3; } } export { getReminderInterval }; function formatKnowledgeResults(results: KnowledgeSearchResult[]): string { if (results.length === 0) { return "未找到相关知识"; } return results .map((k, idx) => { // Security: check for prompt injection if ( looksLikePromptInjection(k.task) || looksLikePromptInjection(k.content) ) { return null; } const typesStr = k.types.join(", "); const sourceName = k.source?.name ? ` (来源: ${escapeForPrompt(k.source.name)})` : ""; const contentPreview = escapeForPrompt(k.content.substring(0, 150)); return `${idx + 1}. [${escapeForPrompt(k.task)}]${sourceName}\n 类型: ${typesStr}\n 内容: ${contentPreview}${k.content.length > 150 ? "..." : ""}\n 评分: ${k.eval.score}/5 (质量分: ${k.quality_score.toFixed(1)})`; }) .filter(Boolean) .join("\n\n"); } // ============================================================================ // Plugin Definition // ============================================================================ const knowhubPlugin = { id: "knowhub", name: "KnowHub", description: "Knowledge management integration for OpenClaw agents", kind: "knowledge" as const, register(api: OpenClawPluginApi) { const cfg = api.pluginConfig as KnowHubConfig; // Validate config if (!cfg.apiUrl) { throw new Error("knowhub: apiUrl is required"); } api.logger.info(`knowhub: plugin registered (server: ${cfg.apiUrl})`); // State for reminder counter const llmCallCount = new Map(); // ======================================================================== // Tools // ======================================================================== api.registerTool( { name: "kb_search", label: "KnowHub Search", description: "搜索 KnowHub 知识库。在遇到复杂任务、不确定用什么工具、多次失败时使用。", parameters: Type.Object({ query: Type.String({ description: "搜索查询" }), top_k: Type.Optional(Type.Number({ description: "返回数量 (默认: 5)" })), min_score: Type.Optional(Type.Number({ description: "最低评分 (默认: 3)" })), types: Type.Optional( Type.Array(Type.String(), { description: "知识类型过滤,如 ['tool', 'strategy']" }) ), }), async execute(_toolCallId, params) { const { query, top_k = 5, min_score = 3, types } = params as { query: string; top_k?: number; min_score?: number; types?: string[]; }; try { let url = `${cfg.apiUrl}/api/knowledge/search?q=${encodeURIComponent(query)}&top_k=${top_k}&min_score=${min_score}`; if (types && types.length > 0) { url += `&types=${types.join(",")}`; } const response = await fetch(url); if (!response.ok) { return { content: [ { type: "text", text: `搜索失败: ${response.statusText}`, }, ], details: { error: response.statusText }, }; } const data = (await response.json()) as { results: KnowledgeSearchResult[]; count: number; }; const formatted = formatKnowledgeResults(data.results); return { content: [ { type: "text", text: `找到 ${data.count} 条知识:\n\n${formatted}`, }, ], details: { count: data.count, results: data.results }, }; } catch (err) { api.logger.warn(`knowhub: search failed: ${String(err)}`); return { content: [ { type: "text", text: `搜索失败: ${String(err)}`, }, ], details: { error: String(err) }, }; } }, }, { name: "kb_search" } ); api.registerTool( { name: "kb_save", label: "KnowHub Save", description: "保存知识到 KnowHub。在使用资源后、获得用户反馈后、搜索过程有发现时使用。", parameters: Type.Object({ task: Type.String({ description: "任务场景描述" }), content: Type.String({ description: "核心知识内容" }), types: Type.Array(Type.String(), { description: "知识类型,如 ['tool', 'strategy']。可选: user_profile, strategy, tool, usecase, definition, plan", }), score: Type.Optional(Type.Number({ description: "评分 1-5 (默认: 3)" })), source_name: Type.Optional(Type.String({ description: "资源名称" })), source_urls: Type.Optional( Type.Array(Type.String(), { description: "参考链接" }) ), }), async execute(_toolCallId, params, ctx) { const { task, content, types, score = 3, source_name, source_urls, } = params as { task: string; content: string; types: string[]; score?: number; source_name?: string; source_urls?: string[]; }; // Validate if (!task || !content || !types || types.length === 0) { return { content: [ { type: "text", text: "缺少必需参数: task, content, types", }, ], details: { error: "missing_params" }, }; } if (score < 1 || score > 5) { return { content: [ { type: "text", text: "评分必须在 1-5 之间", }, ], details: { error: "invalid_score" }, }; } try { const body = { task, content, types, score, scopes: ["org:openclaw"], owner: `agent:${ctx?.agentId || "unknown"}`, source: { name: source_name || "", category: "exp", urls: source_urls || [], agent_id: ctx?.agentId || "unknown", submitted_by: cfg.submittedBy, message_id: "", }, }; const response = await fetch(`${cfg.apiUrl}/api/knowledge`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) { return { content: [ { type: "text", text: `保存失败: ${response.statusText}`, }, ], details: { error: response.statusText }, }; } const data = (await response.json()) as { id: string }; return { content: [ { type: "text", text: `✅ 知识已保存 (ID: ${data.id})`, }, ], details: { id: data.id }, }; } catch (err) { api.logger.warn(`knowhub: save failed: ${String(err)}`); return { content: [ { type: "text", text: `保存失败: ${String(err)}`, }, ], details: { error: String(err) }, }; } }, }, { name: "kb_save" } ); api.registerTool( { name: "kb_update", label: "KnowHub Update", description: "更新知识的有效性反馈。使用知识后提供反馈。", parameters: Type.Object({ knowledge_id: Type.String({ description: "知识 ID" }), is_helpful: Type.Boolean({ description: "是否有用" }), feedback: Type.Optional(Type.String({ description: "反馈说明" })), }), async execute(_toolCallId, params) { const { knowledge_id, is_helpful, feedback } = params as { knowledge_id: string; is_helpful: boolean; feedback?: string; }; try { const body = is_helpful ? { add_helpful_case: { task: feedback || "使用成功", outcome: "有效", timestamp: new Date().toISOString(), }, } : { add_harmful_case: { task: feedback || "使用失败", outcome: "无效", reason: feedback || "未说明", timestamp: new Date().toISOString(), }, }; const response = await fetch( `${cfg.apiUrl}/api/knowledge/${knowledge_id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), } ); if (!response.ok) { return { content: [ { type: "text", text: `更新失败: ${response.statusText}`, }, ], details: { error: response.statusText }, }; } return { content: [ { type: "text", text: "✅ 反馈已提交", }, ], details: { success: true }, }; } catch (err) { api.logger.warn(`knowhub: update failed: ${String(err)}`); return { content: [ { type: "text", text: `更新失败: ${String(err)}`, }, ], details: { error: String(err) }, }; } }, }, { name: "kb_update" } ); // ======================================================================== // Lifecycle Hooks // ======================================================================== // Initial reminder if (cfg.reminderMode !== "off") { api.on("before_agent_start", async () => { return { prependContext: ` 💡 KnowHub 知识库已启用 可用工具: - kb_search: 搜索知识(遇到复杂任务、不确定用什么工具、多次失败时) - kb_save: 保存知识(使用资源后、获得用户反馈后、搜索过程有发现时) - kb_update: 反馈知识有效性(使用知识后) 建议:开始任务前先搜索相关知识,完成后及时保存新发现。 `.trim(), }; }); } // Periodic reminder if (cfg.reminderMode !== "off") { api.on("before_prompt_build", async (_event, ctx) => { const sessionKey = ctx.sessionKey ?? "default"; // Increment counter const count = (llmCallCount.get(sessionKey) ?? 0) + 1; llmCallCount.set(sessionKey, count); // Check interval const interval = getReminderInterval(cfg.reminderMode); if (count % interval !== 0) { return; } return { prependContext: "💡 提醒:如果遇到复杂问题、使用了工具/资源、或获得了用户反馈,记得用 kb_save 保存知识到 KnowHub。", }; }); } // Cleanup and optional server extraction api.on("agent_end", async (event, ctx) => { const sessionKey = ctx.sessionKey ?? "default"; // Cleanup counter llmCallCount.delete(sessionKey); // Optional: server extraction if (cfg.enableServerExtraction && event.messages && event.messages.length > 0) { // Async submission, don't block submitForExtraction(event.messages, ctx, cfg, api).catch((err) => { api.logger.warn(`knowhub: extraction failed: ${String(err)}`); }); } }); // ======================================================================== // Service // ======================================================================== api.registerService({ id: "knowhub", start: () => { api.logger.info( `knowhub: initialized (server: ${cfg.apiUrl}, reminder: ${cfg.reminderMode})` ); }, stop: () => { api.logger.info("knowhub: stopped"); }, }); }, }; // ============================================================================ // Server Extraction // ============================================================================ async function submitForExtraction( messages: unknown[], ctx: { agentId?: string; sessionKey?: string }, cfg: KnowHubConfig, api: OpenClawPluginApi ): Promise { const sanitized = messages.map((msg) => sanitizeMessage(msg, cfg.privacyMode)); const response = await fetch(`${cfg.apiUrl}/api/extract`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: sanitized, agent_id: ctx.agentId, submitted_by: cfg.submittedBy, session_key: ctx.sessionKey, }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = (await response.json()) as { extracted_count: number }; api.logger.info?.(`knowhub: extracted ${result.extracted_count} knowledge items`); } export default knowhubPlugin;