| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- /**
- * 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<string, string> = {
- "&": "&",
- "<": "<",
- ">": ">",
- '"': """,
- "'": "'",
- };
- 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<string, unknown>;
- 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<string, number>();
- // ========================================================================
- // 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<void> {
- 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;
|