diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index 42172f2b4..bcb2c0870 100644 --- a/apps/memos-local-openclaw/index.ts +++ b/apps/memos-local-openclaw/index.ts @@ -9,6 +9,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { Type } from "@sinclair/typebox"; import * as fs from "fs"; import * as path from "path"; +import { createRequire } from "node:module"; import { fileURLToPath } from "url"; import { buildContext } from "./src/config"; import type { HostModelsConfig } from "./src/openclaw-api"; @@ -83,25 +84,56 @@ const memosLocalPlugin = { configSchema: pluginConfigSchema, register(api: OpenClawPluginApi) { - // ─── Ensure better-sqlite3 native module is available ─── - const pluginDir = path.dirname(fileURLToPath(import.meta.url)); + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const localRequire = createRequire(import.meta.url); + const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"; + + function detectPluginDir(startDir: string): string { + let cur = startDir; + for (let i = 0; i < 6; i++) { + const pkg = path.join(cur, "package.json"); + if (fs.existsSync(pkg)) return cur; + const parent = path.dirname(cur); + if (parent === cur) break; + cur = parent; + } + return startDir; + } + + const pluginDir = detectPluginDir(moduleDir); function normalizeFsPath(p: string): string { - return path.resolve(p).replace(/\\/g, "/").toLowerCase(); + return path.resolve(p).replace(/^\\\\\?\\/, "").toLowerCase(); + } + + function isPathInside(baseDir: string, targetPath: string): boolean { + const baseNorm = normalizeFsPath(baseDir); + const targetNorm = normalizeFsPath(targetPath); + const rel = path.relative(baseNorm, targetNorm); + return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)); + } + + function runNpm(args: string[]) { + const { spawnSync } = localRequire("child_process") as typeof import("node:child_process"); + return spawnSync(npmCmd, args, { + cwd: pluginDir, + stdio: "pipe", + shell: false, + timeout: 120_000, + }); } let sqliteReady = false; function trySqliteLoad(): boolean { try { - const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] }); - const resolvedNorm = normalizeFsPath(resolved); - const pluginNorm = normalizeFsPath(pluginDir); - if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) { + const resolved = localRequire.resolve("better-sqlite3", { paths: [pluginDir] }); + const resolvedReal = fs.existsSync(resolved) ? fs.realpathSync.native(resolved) : resolved; + if (!isPathInside(pluginDir, resolvedReal)) { api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`); return false; } - require(resolved); + localRequire(resolvedReal); return true; } catch { return false; @@ -114,13 +146,7 @@ const memosLocalPlugin = { api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-rebuild ...`); try { - const { spawnSync } = require("child_process"); - const rebuildResult = spawnSync("npm", ["rebuild", "better-sqlite3"], { - cwd: pluginDir, - stdio: "pipe", - shell: true, - timeout: 120_000, - }); + const rebuildResult = runNpm(["rebuild", "better-sqlite3"]); const stdout = rebuildResult.stdout?.toString() || ""; const stderr = rebuildResult.stderr?.toString() || ""; @@ -128,9 +154,9 @@ const memosLocalPlugin = { if (stderr) api.logger.warn(`memos-local: rebuild stderr: ${stderr.slice(0, 500)}`); if (rebuildResult.status === 0) { - Object.keys(require.cache) + Object.keys(localRequire.cache) .filter(k => k.includes("better-sqlite3") || k.includes("better_sqlite3")) - .forEach(k => delete require.cache[k]); + .forEach(k => delete localRequire.cache[k]); sqliteReady = trySqliteLoad(); if (sqliteReady) { api.logger.info("memos-local: better-sqlite3 auto-rebuild succeeded!"); @@ -222,7 +248,7 @@ const memosLocalPlugin = { let pluginVersion = "0.0.0"; try { - const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf-8")); + const pkg = JSON.parse(fs.readFileSync(path.join(pluginDir, "package.json"), "utf-8")); pluginVersion = pkg.version ?? pluginVersion; } catch {} const telemetry = new Telemetry(ctx.config.telemetry ?? {}, stateDir, pluginVersion, ctx.log, pluginDir); diff --git a/apps/memos-local-openclaw/scripts/postinstall.cjs b/apps/memos-local-openclaw/scripts/postinstall.cjs index 523c93f2e..c077f8794 100644 --- a/apps/memos-local-openclaw/scripts/postinstall.cjs +++ b/apps/memos-local-openclaw/scripts/postinstall.cjs @@ -23,6 +23,11 @@ function phase(n, title) { } const pluginDir = path.resolve(__dirname, ".."); +const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"; + +function normalizePathForMatch(p) { + return path.resolve(p).replace(/^\\\\\?\\/, "").replace(/\\/g, "/").toLowerCase(); +} console.log(` ${CYAN}${BOLD}┌──────────────────────────────────────────────────┐ @@ -42,7 +47,8 @@ log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`); * ═══════════════════════════════════════════════════════════ */ function cleanStaleArtifacts() { - const isExtensionsDir = pluginDir.includes(path.join(".openclaw", "extensions")); + const pluginDirNorm = normalizePathForMatch(pluginDir); + const isExtensionsDir = pluginDirNorm.includes("/.openclaw/extensions/"); if (!isExtensionsDir) return; const pkgPath = path.join(pluginDir, "package.json"); @@ -133,10 +139,10 @@ function ensureDependencies() { log("Running: npm install --omit=dev ..."); const startMs = Date.now(); - const result = spawnSync("npm", ["install", "--omit=dev"], { + const result = spawnSync(npmCmd, ["install", "--omit=dev"], { cwd: pluginDir, stdio: "pipe", - shell: true, + shell: false, timeout: 120_000, }); const elapsed = ((Date.now() - startMs) / 1000).toFixed(1); @@ -223,8 +229,8 @@ function cleanupLegacy() { newEntry.source = oldSource .replace(/memos-lite-openclaw-plugin/g, "memos-local-openclaw-plugin") .replace(/memos-lite/g, "memos-local-openclaw-plugin") - .replace(/\/memos-local\//g, "/memos-local-openclaw-plugin/") - .replace(/\/memos-local$/g, "/memos-local-openclaw-plugin"); + .replace(/[\\/]memos-local[\\/]/g, `${path.sep}memos-local-openclaw-plugin${path.sep}`) + .replace(/[\\/]memos-local$/g, `${path.sep}memos-local-openclaw-plugin`); if (newEntry.source !== oldSource) { log(`Updated source path: ${DIM}${oldSource}${RESET} → ${GREEN}${newEntry.source}${RESET}`); cfgChanged = true; @@ -384,6 +390,16 @@ if (sqliteBindingsExist()) { warn("better-sqlite3 native bindings not found in plugin dir."); log(`Searched in: ${DIM}${sqliteModulePath}/build/${RESET}`); log("Running: npm rebuild better-sqlite3 (may take 30-60s)..."); +} + +const startMs = Date.now(); + +const result = spawnSync(npmCmd, ["rebuild", "better-sqlite3"], { + cwd: pluginDir, + stdio: "pipe", + shell: false, + timeout: 180_000, +}); const startMs = Date.now(); const result = spawnSync("npm", ["rebuild", "better-sqlite3"], { diff --git a/apps/memos-local-openclaw/src/embedding/index.ts b/apps/memos-local-openclaw/src/embedding/index.ts index 2adc7cac7..bb55ee501 100644 --- a/apps/memos-local-openclaw/src/embedding/index.ts +++ b/apps/memos-local-openclaw/src/embedding/index.ts @@ -61,6 +61,7 @@ export class Embedder { case "zhipu": case "siliconflow": case "bailian": + case "novita": result = await embedOpenAI(texts, cfg!, this.log); break; case "gemini": result = await embedGemini(texts, cfg!, this.log); break; diff --git a/apps/memos-local-openclaw/src/ingest/providers/index.ts b/apps/memos-local-openclaw/src/ingest/providers/index.ts index 85d0814c8..ce8b812e4 100644 --- a/apps/memos-local-openclaw/src/ingest/providers/index.ts +++ b/apps/memos-local-openclaw/src/ingest/providers/index.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; -import type { SummarizerConfig, SummaryProvider, Logger, OpenClawAPI } from "../../types"; -import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI, parseFilterResult, parseDedupResult } from "./openai"; +import type { SummarizerConfig, SummaryProvider, Logger } from "../../types"; +import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai"; import type { FilterResult, DedupResult } from "./openai"; export type { FilterResult, DedupResult } from "./openai"; import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic"; @@ -22,6 +22,7 @@ function detectProvider( return "gemini"; } if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock"; + if (key.includes("novita") || url.includes("api.novita.ai")) return "novita"; return "openai_compatible"; } @@ -466,10 +467,13 @@ function callSummarize(cfg: SummarizerConfig, text: string, log: Logger): Promis case "azure_openai": case "zhipu": case "siliconflow": + case "deepseek": + case "moonshot": case "bailian": case "cohere": case "mistral": case "voyage": + case "novita": return summarizeOpenAI(text, cfg, log); case "anthropic": return summarizeAnthropic(text, cfg, log); @@ -489,10 +493,13 @@ function callSummarizeTask(cfg: SummarizerConfig, text: string, log: Logger): Pr case "azure_openai": case "zhipu": case "siliconflow": + case "deepseek": + case "moonshot": case "bailian": case "cohere": case "mistral": case "voyage": + case "novita": return summarizeTaskOpenAI(text, cfg, log); case "anthropic": return summarizeTaskAnthropic(text, cfg, log); @@ -512,10 +519,13 @@ function callGenerateTaskTitle(cfg: SummarizerConfig, text: string, log: Logger) case "azure_openai": case "zhipu": case "siliconflow": + case "deepseek": + case "moonshot": case "bailian": case "cohere": case "mistral": case "voyage": + case "novita": return generateTaskTitleOpenAI(text, cfg, log); case "anthropic": return generateTaskTitleAnthropic(text, cfg, log); @@ -535,10 +545,13 @@ function callTopicJudge(cfg: SummarizerConfig, currentContext: string, newMessag case "azure_openai": case "zhipu": case "siliconflow": + case "deepseek": + case "moonshot": case "bailian": case "cohere": case "mistral": case "voyage": + case "novita": return judgeNewTopicOpenAI(currentContext, newMessage, cfg, log); case "anthropic": return judgeNewTopicAnthropic(currentContext, newMessage, cfg, log); @@ -558,10 +571,13 @@ function callFilterRelevant(cfg: SummarizerConfig, query: string, candidates: Ar case "azure_openai": case "zhipu": case "siliconflow": + case "deepseek": + case "moonshot": case "bailian": case "cohere": case "mistral": case "voyage": + case "novita": return filterRelevantOpenAI(query, candidates, cfg, log); case "anthropic": return filterRelevantAnthropic(query, candidates, cfg, log); @@ -581,10 +597,13 @@ function callJudgeDedup(cfg: SummarizerConfig, newSummary: string, candidates: A case "azure_openai": case "zhipu": case "siliconflow": + case "deepseek": + case "moonshot": case "bailian": case "cohere": case "mistral": case "voyage": + case "novita": return judgeDedupOpenAI(newSummary, candidates, cfg, log); case "anthropic": return judgeDedupAnthropic(newSummary, candidates, cfg, log); @@ -629,91 +648,3 @@ function wordCount(text: string): number { if (noCjk) count += noCjk.split(/\s+/).filter(Boolean).length; return count; } - -// ─── OpenClaw Prompt Templates ─── - -const OPENCLAW_TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information. - -CRITICAL LANGUAGE RULE: You MUST write in the SAME language as the user's messages. Chinese input → Chinese output. English input → English output. NEVER mix languages. - -Output EXACTLY this structure: - -📌 Title -A short, descriptive title (10-30 characters). Like a chat group name. - -🎯 Goal -One sentence: what the user wanted to accomplish. - -📋 Key Steps -- Describe each meaningful step in detail -- Include the ACTUAL content produced: code snippets, commands, config blocks, formulas, key paragraphs -- For code: include the function signature and core logic (up to ~30 lines per block), use fenced code blocks -- For configs: include the actual config values and structure -- For lists/instructions: include the actual items, not just "provided a list" -- Merge only truly trivial back-and-forth (like "ok" / "sure") -- Do NOT over-summarize: "provided a function" is BAD; show the actual function - -✅ Result -What was the final outcome? Include the final version of any code/config/content produced. - -💡 Key Details -- Decisions made, trade-offs discussed, caveats noted, alternative approaches mentioned -- Specific values: numbers, versions, thresholds, URLs, file paths, model names -- Omit this section only if there truly are no noteworthy details - -RULES: -- This summary is a KNOWLEDGE BASE ENTRY, not a brief note. Be thorough. -- PRESERVE verbatim: code, commands, URLs, file paths, error messages, config values, version numbers, names, amounts -- DISCARD only: greetings, filler, the assistant explaining what it will do before doing it -- Replace secrets (API keys, tokens, passwords) with [REDACTED] -- Target length: 30-50% of the original conversation length. Longer conversations need longer summaries. -- Output summary only, no preamble.`; - -const OPENCLAW_TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given a summary of the CURRENT conversation and a NEW user message, determine if the new message starts a DIFFERENT topic/task. - -Answer ONLY "NEW" or "SAME". - -Rules: -- "NEW" = the new message is about a completely different subject, project, or task -- "SAME" = the new message continues, follows up on, or is closely related to the current topic -- Follow-up questions, clarifications, refinements, bug fixes, or next steps on the same task = SAME -- Greetings or meta-questions like "你好" or "谢谢" without new substance = SAME -- A clearly unrelated request (e.g., current topic is deployment, new message asks about cooking) = NEW - -Output exactly one word: NEW or SAME`; - -const OPENCLAW_FILTER_RELEVANT_PROMPT = `You are a memory relevance judge. Given a user's QUERY and a list of CANDIDATE memory summaries, do two things: - -1. Select ALL candidates that could be useful for answering the query. When in doubt, INCLUDE the candidate. - - For questions about lists, history, or "what/where/who" across multiple items, include ALL matching items. - - For factual lookups, a single direct answer is enough. -2. Judge whether the selected memories are SUFFICIENT to fully answer the query WITHOUT fetching additional context. - -IMPORTANT for "sufficient" judgment: -- sufficient=true ONLY when the memories contain a concrete ANSWER, fact, decision, or actionable information that directly addresses the query. -- sufficient=false when the memories only repeat the question, show related topics but lack the specific detail, or contain partial information. - -Output a JSON object with exactly two fields: -{"relevant":[1,3,5],"sufficient":true} - -- "relevant": array of candidate numbers that are useful. Empty array [] if none are relevant. -- "sufficient": true ONLY if the memories contain a direct answer; false otherwise. - -Output ONLY the JSON object, nothing else.`; - -const OPENCLAW_DEDUP_JUDGE_PROMPT = `You are a memory deduplication system. Given a NEW memory summary and several EXISTING memory summaries, determine the relationship. - -For each EXISTING memory, the NEW memory is either: -- "DUPLICATE": NEW is fully covered by an EXISTING memory — no new information at all -- "UPDATE": NEW contains information that supplements or updates an EXISTING memory (new data, status change, additional detail) -- "NEW": NEW is a different topic/event despite surface similarity - -Pick the BEST match among all candidates. If none match well, choose "NEW". - -Output a single JSON object: -- If DUPLICATE: {"action":"DUPLICATE","targetIndex":2,"reason":"..."} -- If UPDATE: {"action":"UPDATE","targetIndex":3,"reason":"...","mergedSummary":"a combined summary preserving all info from both old and new, same language as input"} -- If NEW: {"action":"NEW","reason":"..."} - -CRITICAL: mergedSummary must use the SAME language as the input. Output ONLY the JSON object.`; - diff --git a/apps/memos-local-openclaw/src/types.ts b/apps/memos-local-openclaw/src/types.ts index 4cac79131..630488193 100644 --- a/apps/memos-local-openclaw/src/types.ts +++ b/apps/memos-local-openclaw/src/types.ts @@ -150,10 +150,13 @@ export type SummaryProvider = | "bedrock" | "zhipu" | "siliconflow" + | "deepseek" + | "moonshot" | "bailian" | "cohere" | "mistral" | "voyage" + | "novita" | "openclaw"; export type EmbeddingProvider = @@ -164,6 +167,7 @@ export type EmbeddingProvider = | "cohere" | "mistral" | "voyage" + | "novita" | "local" | "openclaw"; diff --git a/apps/memos-local-openclaw/src/viewer/server.ts b/apps/memos-local-openclaw/src/viewer/server.ts index ed0954cc3..ea35bcaf2 100644 --- a/apps/memos-local-openclaw/src/viewer/server.ts +++ b/apps/memos-local-openclaw/src/viewer/server.ts @@ -1,7 +1,7 @@ import http from "node:http"; import os from "node:os"; import crypto from "node:crypto"; -import { execSync, exec } from "node:child_process"; +import { execSync, exec, execFile } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; @@ -3248,7 +3248,8 @@ export class ViewerServer { // Install dependencies this.log.info(`update-install: installing dependencies...`); - exec(`cd ${extDir} && npm install --omit=dev --ignore-scripts`, { timeout: 120_000 }, (npmErr, npmOut, npmStderr) => { + const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"; + execFile(npmCmd, ["install", "--omit=dev", "--ignore-scripts"], { cwd: extDir, timeout: 120_000 }, (npmErr, npmOut, npmStderr) => { if (npmErr) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} this.log.warn(`update-install: npm install failed: ${npmErr.message}`); @@ -3256,25 +3257,21 @@ export class ViewerServer { return; } - // Rebuild native modules (do not swallow errors) - exec(`cd ${extDir} && npm rebuild better-sqlite3`, { timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => { + execFile(npmCmd, ["rebuild", "better-sqlite3"], { cwd: extDir, timeout: 60_000 }, (rebuildErr, rebuildOut, rebuildStderr) => { if (rebuildErr) { this.log.warn(`update-install: better-sqlite3 rebuild failed: ${rebuildErr.message}`); const stderr = String(rebuildStderr || "").trim(); if (stderr) this.log.warn(`update-install: rebuild stderr: ${stderr.slice(0, 500)}`); - // Continue so postinstall.cjs can run (it will try rebuild again and show user guidance) } - // Run postinstall.cjs: legacy cleanup, skill install, version marker, and optional sqlite re-check this.log.info(`update-install: running postinstall...`); - exec(`cd ${extDir} && node scripts/postinstall.cjs`, { timeout: 180_000 }, (postErr, postOut, postStderr) => { + execFile(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} if (postErr) { this.log.warn(`update-install: postinstall failed: ${postErr.message}`); const postStderrStr = String(postStderr || "").trim(); if (postStderrStr) this.log.warn(`update-install: postinstall stderr: ${postStderrStr.slice(0, 500)}`); - // Still report success; plugin is updated, user can run postinstall manually if needed } // Read new version diff --git a/apps/memos-local-openclaw/www/docs/index.html b/apps/memos-local-openclaw/www/docs/index.html index 64ab7c33e..164d68f5c 100644 --- a/apps/memos-local-openclaw/www/docs/index.html +++ b/apps/memos-local-openclaw/www/docs/index.html @@ -102,6 +102,25 @@ .callout strong{color:var(--text)} .callout.warn{border-color:var(--amber);background:var(--amber-bg)} .callout.success{border-color:var(--green);background:var(--green-bg)} +.install-switcher{background:var(--code-bg);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin:12px 0 18px} +.install-switcher-header{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--border)} +.install-switcher-header .dot{width:9px;height:9px;border-radius:50%} +.install-os-switch{margin-left:auto;display:flex;align-items:center;gap:6px;padding:3px;background:var(--muted);border:1px solid var(--border);border-radius:999px} +.install-os-switch .os-btn{background:transparent;border:none;color:var(--text-sec);font-size:11px;font-weight:700;padding:5px 10px;border-radius:999px;cursor:pointer;transition:all .15s} +.install-os-switch .os-btn.active{background:var(--grad-main);color:#06080f} +.install-switcher-body{padding:14px 16px} +.install-note{font-family:var(--mono);font-size:12px;line-height:1.7;color:var(--text-thr);margin-bottom:8px} +.install-row{display:flex;align-items:center;gap:8px} +.install-row .prompt{color:var(--green);font-family:var(--mono);font-size:12px} +.install-row .cmd{font-family:var(--mono);font-size:12px;color:var(--code-text);line-height:1.8;flex:1;white-space:nowrap;overflow:auto;scrollbar-width:none} +.install-row .cmd::-webkit-scrollbar{display:none} +.install-copy-btn{width:26px;height:26px;display:flex;align-items:center;justify-content:center;background:var(--muted);border:1px solid var(--border);color:var(--accent);border-radius:7px;cursor:pointer;transition:all .15s;flex-shrink:0;padding:0} +.install-copy-btn:hover{border-color:var(--accent)} +.install-copy-btn .copy-icon,.install-copy-btn .check-icon{width:13px;height:13px;display:block} +.install-copy-btn .check-icon{display:none;color:var(--green)} +.install-copy-btn.copied{border-color:rgba(0,230,118,.45);background:rgba(0,230,118,.12);color:var(--green)} +.install-copy-btn.copied .copy-icon{display:none} +.install-copy-btn.copied .check-icon{display:block} .diagram{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:24px 20px;margin:18px 0;overflow-x:auto} .diagram-flow{display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:center;min-width:560px} @@ -280,29 +299,48 @@
插件依赖 better-sqlite3 原生模块。macOS / Linux 用户建议先安装编译工具,可大幅提升安装成功率。Windows 用户使用 Node.js LTS 版本时通常有预编译文件,可直接跳到 Step 1。The plugin depends on better-sqlite3, a native C/C++ module. macOS / Linux users should install build tools first. Windows users with Node.js LTS usually have prebuilt binaries and can skip to Step 1.
# macOS
-xcode-select --install
-
-# Linux (Ubuntu / Debian)
-sudo apt install build-essential python3
-
-# Windows: 通常无需操作。如安装失败,安装 Visual Studio Build Tools:
-# https://visualstudio.microsoft.com/visual-cpp-build-tools/bash
-
-openclaw plugins install @memtensor/memos-local-openclaw-plugin
-openclaw gateway startbash
+better-sqlite3 原生模块编译失败。请确认已执行上方 Step 0,然后手动重建:cd ~/.openclaw/extensions/memos-local-openclaw-plugin && npm rebuild better-sqlite3。更多方案请查看 安装排查指南 或 better-sqlite3 官方文档。Install failed? The most common issue is better-sqlite3 compilation failure. Ensure Step 0 is done, then manually rebuild: cd ~/.openclaw/extensions/memos-local-openclaw-plugin && npm rebuild better-sqlite3. See the troubleshooting guide or official better-sqlite3 docs for more solutions.better-sqlite3 原生模块编译失败,可手动重建:cd ~/.openclaw/extensions/memos-local-openclaw-plugin && npm rebuild better-sqlite3。更多方案请查看 安装排查指南 或 better-sqlite3 官方文档。Install failed? The most common issue is better-sqlite3 compilation failure. You can manually rebuild it: cd ~/.openclaw/extensions/memos-local-openclaw-plugin && npm rebuild better-sqlite3. See the troubleshooting guide or official better-sqlite3 docs for more solutions.openclaw plugins update memos-local-openclaw-plugin
-openclaw gateway stop && openclaw gateway startbash
+rm -rf ~/.openclaw/extensions/memos-local-openclaw-plugin && openclaw plugins install @memtensor/memos-local-openclaw-plugin(记忆数据不受影响)。Upgrade automatically handles dependencies, legacy cleanup, and native module compilation. If update is unavailable, delete the old directory first: rm -rf ~/.openclaw/extensions/memos-local-openclaw-plugin && openclaw plugins install @memtensor/memos-local-openclaw-plugin (memory data is stored separately and won't be affected).两种方式:编辑 openclaw.json 或通过 Viewer 网页面板在线修改。支持分级模型。Two methods: edit openclaw.json or via Viewer web panel. Tiered models supported.
{
"plugins": {
@@ -692,6 +730,43 @@ 默认值Defaults<
initDocsTheme();