From 8751436b0a81844e6110b2abe0f1747783299678 Mon Sep 17 00:00:00 2001 From: jiang Date: Thu, 19 Mar 2026 16:32:29 +0800 Subject: [PATCH 1/5] fix: fix windows dir error --- apps/memos-local-openclaw/index.ts | 62 +++++++++++++------ .../scripts/postinstall.cjs | 20 +++--- .../memos-local-openclaw/src/viewer/server.ts | 35 ++--------- 3 files changed, 62 insertions(+), 55 deletions(-) diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index b18d14a91..1d2b7c2bb 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 { SqliteStore } from "./src/storage/sqlite"; @@ -76,25 +77,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; @@ -107,13 +139,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() || ""; @@ -121,9 +147,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!"); @@ -195,7 +221,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); diff --git a/apps/memos-local-openclaw/scripts/postinstall.cjs b/apps/memos-local-openclaw/scripts/postinstall.cjs index 486a02669..419037c58 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; @@ -397,10 +403,10 @@ ${GREEN}${BOLD} ┌──────────────────── const startMs = Date.now(); -const result = spawnSync("npm", ["rebuild", "better-sqlite3"], { +const result = spawnSync(npmCmd, ["rebuild", "better-sqlite3"], { cwd: pluginDir, stdio: "pipe", - shell: true, + shell: false, timeout: 180_000, }); diff --git a/apps/memos-local-openclaw/src/viewer/server.ts b/apps/memos-local-openclaw/src/viewer/server.ts index 4c1159544..b03924aae 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"; @@ -1244,7 +1244,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}`); @@ -1252,25 +1253,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 @@ -1464,28 +1461,6 @@ export class ViewerServer { } } - private handleCleanupPolluted(res: http.ServerResponse): void { - try { - const polluted = this.store.findPollutedUserChunks(); - let deleted = 0; - for (const { id, reason } of polluted) { - if (this.store.deleteChunk(id)) { - deleted++; - this.log.info(`Cleaned polluted chunk ${id}: ${reason}`); - } - } - const fixed = this.store.fixMixedUserChunks(); - this.log.info(`Cleanup: removed ${deleted} polluted, fixed ${fixed} mixed chunks`); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ deleted, fixed, total: polluted.length })); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - this.log.error(`handleCleanupPolluted error: ${msg}`); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: msg })); - } - } - private handleMigrateScan(res: http.ServerResponse): void { try { const ocHome = this.getOpenClawHome(); From 7c5a84ed8c8fb6755cea8d3acadb3ea1ff228e57 Mon Sep 17 00:00:00 2001 From: jiang Date: Thu, 19 Mar 2026 18:06:39 +0800 Subject: [PATCH 2/5] chore: update doc --- apps/memos-local-openclaw/www/docs/index.html | 111 +++++++++++++++--- apps/memos-local-openclaw/www/index.html | 72 +++++++++--- 2 files changed, 151 insertions(+), 32 deletions(-) diff --git a/apps/memos-local-openclaw/www/docs/index.html b/apps/memos-local-openclaw/www/docs/index.html index 3c8d401e2..6aad16896 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} @@ -271,29 +290,48 @@

快速开始Quick StartEmbedding / Summarizer API 可选,不配自动用本地模型Embedding / Summarizer APIs optional, falls back to local -

Step 0:安装 C++ 编译工具(macOS / Linux 推荐)Step 0: Install C++ Build Tools (macOS / Linux recommended)

-

插件依赖 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
- -

Step 1:安装插件 & 启动Step 1: Install Plugin & Start

-
openclaw plugins install @memtensor/memos-local-openclaw-plugin
-openclaw gateway startbash
+

Step 1:安装插件 & 启动Step 1: Install Plugin & Start
首次安装First Install

+
+
+ +
+ + +
+
+
+
+ $ + curl -fsSL https://cdn.memtensor.com.cn/memos-local-openclaw/install.sh | bash + + +
+
+
-
安装失败?最常见的问题是 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.

升级Upgrade

-
openclaw plugins update memos-local-openclaw-plugin
-openclaw gateway stop && openclaw gateway startbash
+
+
+ +
+ + +
+
+
+
+ $ + curl -fsSL https://cdn.memtensor.com.cn/memos-local-openclaw/install.sh | bash + + +
+
+
升级自动完成依赖安装、旧版清理和原生模块编译,无需手动操作。如果 update 命令不可用,先删除旧目录再重新安装: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).
-

配置Configuration

+

Step2: 配置Step2: Configuration

两种方式:编辑 openclaw.json 或通过 Viewer 网页面板在线修改。支持分级模型。Two methods: edit openclaw.json or via Viewer web panel. Tiered models supported.

{
   "plugins": {
@@ -541,6 +579,43 @@ 

默认值Defaults< initDocsTheme();