openclaw-openviking-setup-helper 0.2.8 → 0.2.9-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/install.js +528 -108
  2. package/package.json +2 -2
package/install.js CHANGED
@@ -11,10 +11,10 @@
11
11
  *
12
12
  * Direct run:
13
13
  * node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ]
14
- * [ --openviking-version=V ] [ --repo=PATH ]
14
+ * [ --plugin-version=TAG ] [ --openviking-version=V ] [ --repo=PATH ]
15
15
  *
16
16
  * Environment variables:
17
- * REPO, BRANCH, OPENVIKING_INSTALL_YES, SKIP_OPENCLAW, SKIP_OPENVIKING
17
+ * REPO, PLUGIN_VERSION (or BRANCH), OPENVIKING_INSTALL_YES, SKIP_OPENCLAW, SKIP_OPENVIKING
18
18
  * OPENVIKING_VERSION Pip install openviking==VERSION (omit for latest)
19
19
  * OPENVIKING_REPO Repo path: source install (pip -e) + local plugin (default: off)
20
20
  * NPM_REGISTRY, PIP_INDEX_URL
@@ -23,26 +23,26 @@
23
23
  */
24
24
 
25
25
  import { spawn } from "node:child_process";
26
- import { mkdir, readFile, writeFile } from "node:fs/promises";
26
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
27
27
  import { existsSync, readdirSync } from "node:fs";
28
- import { dirname, join } from "node:path";
28
+ import { dirname, join, relative } from "node:path";
29
29
  import { createInterface } from "node:readline";
30
30
  import { fileURLToPath } from "node:url";
31
31
 
32
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
33
 
34
- const REPO = process.env.REPO || "volcengine/OpenViking";
35
- const BRANCH = process.env.BRANCH || "main";
36
- const GH_RAW = `https://raw.githubusercontent.com/${REPO}/${BRANCH}`;
34
+ let REPO = process.env.REPO || "volcengine/OpenViking";
35
+ // PLUGIN_VERSION takes precedence over BRANCH (legacy)
36
+ let PLUGIN_VERSION = process.env.PLUGIN_VERSION || process.env.BRANCH || "main";
37
37
  const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmmirror.com";
38
- const PIP_INDEX_URL = process.env.PIP_INDEX_URL || "https://pypi.tuna.tsinghua.edu.cn/simple";
38
+ const PIP_INDEX_URL = process.env.PIP_INDEX_URL || "https://mirrors.volces.com/pypi/simple/";
39
39
 
40
40
  const IS_WIN = process.platform === "win32";
41
41
  const HOME = process.env.HOME || process.env.USERPROFILE || "";
42
42
 
43
43
  const DEFAULT_OPENCLAW_DIR = join(HOME, ".openclaw");
44
44
  let OPENCLAW_DIR = DEFAULT_OPENCLAW_DIR;
45
- let PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", "memory-openviking");
45
+ let PLUGIN_DEST = ""; // Will be set after resolving plugin config
46
46
 
47
47
  const OPENVIKING_DIR = join(HOME, ".openviking");
48
48
 
@@ -51,21 +51,35 @@ const DEFAULT_AGFS_PORT = 1833;
51
51
  const DEFAULT_VLM_MODEL = "doubao-seed-2-0-pro-260215";
52
52
  const DEFAULT_EMBED_MODEL = "doubao-embedding-vision-251215";
53
53
 
54
- const REQUIRED_PLUGIN_FILES = [
55
- "examples/openclaw-memory-plugin/index.ts",
56
- "examples/openclaw-memory-plugin/config.ts",
57
- "examples/openclaw-memory-plugin/openclaw.plugin.json",
58
- "examples/openclaw-memory-plugin/package.json",
59
- "examples/openclaw-memory-plugin/package-lock.json",
60
- "examples/openclaw-memory-plugin/.gitignore",
61
- ];
62
-
63
- const OPTIONAL_PLUGIN_FILES = [
64
- "examples/openclaw-memory-plugin/client.ts",
65
- "examples/openclaw-memory-plugin/process-manager.ts",
66
- "examples/openclaw-memory-plugin/memory-ranking.ts",
67
- "examples/openclaw-memory-plugin/text-utils.ts",
68
- ];
54
+ // Fallback configs for old versions without manifest
55
+ const FALLBACK_LEGACY = {
56
+ dir: "openclaw-memory-plugin",
57
+ id: "memory-openviking",
58
+ kind: "memory",
59
+ slot: "memory",
60
+ required: ["index.ts", "config.ts", "openclaw.plugin.json", "package.json"],
61
+ optional: ["package-lock.json", ".gitignore"],
62
+ };
63
+
64
+ const FALLBACK_CURRENT = {
65
+ dir: "openclaw-plugin",
66
+ id: "openviking",
67
+ kind: "context-engine",
68
+ slot: "contextEngine",
69
+ required: ["index.ts", "config.ts", "openclaw.plugin.json", "package.json"],
70
+ optional: ["context-engine.ts", "client.ts", "process-manager.ts", "memory-ranking.ts", "text-utils.ts", "session-transcript-repair.ts", "tool-call-id.ts", "tsconfig.json", "package-lock.json", ".gitignore"],
71
+ };
72
+
73
+ // Resolved plugin config (set by resolvePluginConfig)
74
+ let resolvedPluginDir = "";
75
+ let resolvedPluginId = "";
76
+ let resolvedPluginKind = "";
77
+ let resolvedPluginSlot = "";
78
+ let resolvedFilesRequired = [];
79
+ let resolvedFilesOptional = [];
80
+ let resolvedNpmOmitDev = true;
81
+ let resolvedMinOpenclawVersion = "";
82
+ let resolvedMinOpenvikingVersion = "";
69
83
 
70
84
  let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
71
85
  let langZh = false;
@@ -102,38 +116,86 @@ for (let i = 0; i < argv.length; i++) {
102
116
  i += 1;
103
117
  continue;
104
118
  }
119
+ if (arg.startsWith("--plugin-version=")) {
120
+ PLUGIN_VERSION = arg.slice("--plugin-version=".length).trim();
121
+ continue;
122
+ }
123
+ if (arg === "--plugin-version") {
124
+ const version = argv[i + 1]?.trim();
125
+ if (!version) {
126
+ console.error("--plugin-version requires a value");
127
+ process.exit(1);
128
+ }
129
+ PLUGIN_VERSION = version;
130
+ i += 1;
131
+ continue;
132
+ }
105
133
  if (arg.startsWith("--openviking-version=")) {
106
134
  openvikingVersion = arg.slice("--openviking-version=".length).trim();
107
135
  continue;
108
136
  }
137
+ if (arg === "--openviking-version") {
138
+ const version = argv[i + 1]?.trim();
139
+ if (!version) {
140
+ console.error("--openviking-version requires a value");
141
+ process.exit(1);
142
+ }
143
+ openvikingVersion = version;
144
+ i += 1;
145
+ continue;
146
+ }
109
147
  if (arg.startsWith("--repo=")) {
110
148
  openvikingRepo = arg.slice("--repo=".length).trim();
111
149
  continue;
112
150
  }
151
+ if (arg.startsWith("--github-repo=")) {
152
+ REPO = arg.slice("--github-repo=".length).trim();
153
+ continue;
154
+ }
155
+ if (arg === "--github-repo") {
156
+ const repo = argv[i + 1]?.trim();
157
+ if (!repo) {
158
+ console.error("--github-repo requires a value (e.g. owner/repo)");
159
+ process.exit(1);
160
+ }
161
+ REPO = repo;
162
+ i += 1;
163
+ continue;
164
+ }
113
165
  if (arg === "-h" || arg === "--help") {
114
166
  printHelp();
115
167
  process.exit(0);
116
168
  }
117
169
  }
118
170
 
119
- const OPENVIKING_PIP_SPEC = openvikingVersion ? `openviking==${openvikingVersion}` : "openviking";
120
-
121
171
  function setOpenClawDir(dir) {
122
172
  OPENCLAW_DIR = dir;
123
- PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", "memory-openviking");
124
173
  }
125
174
 
126
175
  function printHelp() {
127
- console.log("Usage: node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ] [ --openviking-version=V ] [ --repo=PATH ]");
176
+ console.log("Usage: node install.js [ OPTIONS ]");
177
+ console.log("");
178
+ console.log("Options:");
179
+ console.log(" --github-repo=OWNER/REPO GitHub repository (default: volcengine/OpenViking)");
180
+ console.log(" --plugin-version=TAG Plugin version (Git tag, e.g. v0.2.9, default: main)");
181
+ console.log(" --openviking-version=V OpenViking PyPI version (e.g. 0.2.9, default: latest)");
182
+ console.log(" --workdir PATH OpenClaw config directory (default: ~/.openclaw)");
183
+ console.log(" --repo=PATH Use local OpenViking repo at PATH (pip -e + local plugin)");
184
+ console.log(" -y, --yes Non-interactive (use defaults)");
185
+ console.log(" --zh Chinese prompts");
186
+ console.log(" -h, --help This help");
187
+ console.log("");
188
+ console.log("Examples:");
189
+ console.log(" # Install latest version");
190
+ console.log(" node install.js");
191
+ console.log("");
192
+ console.log(" # Install from a fork repository");
193
+ console.log(" node install.js --github-repo=yourname/OpenViking --plugin-version=dev-branch");
128
194
  console.log("");
129
- console.log(" -y, --yes Non-interactive (use defaults)");
130
- console.log(" --zh Chinese prompts");
131
- console.log(" --workdir OpenClaw config directory (default: ~/.openclaw)");
132
- console.log(" --openviking-version=VERSION Pip install openviking==VERSION (default: latest)");
133
- console.log(" --repo=PATH Use OpenViking repo at PATH: pip install -e PATH, plugin from repo (default: off)");
134
- console.log(" -h, --help This help");
195
+ console.log(" # Install specific plugin version");
196
+ console.log(" node install.js --plugin-version=v0.2.8");
135
197
  console.log("");
136
- console.log("Env: OPENVIKING_REPO, REPO, BRANCH, SKIP_OPENCLAW, SKIP_OPENVIKING, OPENVIKING_VERSION, NPM_REGISTRY, PIP_INDEX_URL");
198
+ console.log("Env: REPO, PLUGIN_VERSION, OPENVIKING_VERSION, SKIP_OPENCLAW, SKIP_OPENVIKING, NPM_REGISTRY, PIP_INDEX_URL");
137
199
  }
138
200
 
139
201
  function tr(en, zh) {
@@ -234,8 +296,19 @@ function question(prompt, defaultValue = "") {
234
296
  });
235
297
  }
236
298
 
299
+ async function resolveAbsoluteCommand(cmd) {
300
+ if (cmd.startsWith("/") || (IS_WIN && /^[A-Za-z]:[/\\]/.test(cmd))) return cmd;
301
+ if (IS_WIN) {
302
+ const r = await runCapture("where", [cmd], { shell: true });
303
+ return r.out.split(/\r?\n/)[0]?.trim() || cmd;
304
+ }
305
+ const r = await runCapture("which", [cmd], { shell: false });
306
+ return r.out.trim() || cmd;
307
+ }
308
+
237
309
  async function checkPython() {
238
- const py = process.env.OPENVIKING_PYTHON || (IS_WIN ? "python" : "python3");
310
+ const raw = process.env.OPENVIKING_PYTHON || (IS_WIN ? "python" : "python3");
311
+ const py = await resolveAbsoluteCommand(raw);
239
312
  const result = await runCapture(py, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"]);
240
313
  if (result.code !== 0 || !result.out) {
241
314
  return {
@@ -389,6 +462,222 @@ async function checkOpenClaw() {
389
462
  process.exit(1);
390
463
  }
391
464
 
465
+ // Compare versions: returns true if v1 >= v2
466
+ function versionGte(v1, v2) {
467
+ const parseVersion = (v) => {
468
+ const cleaned = v.replace(/^v/, "").replace(/-.*$/, "");
469
+ const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10) || 0);
470
+ while (parts.length < 3) parts.push(0);
471
+ return parts;
472
+ };
473
+ const [a1, a2, a3] = parseVersion(v1);
474
+ const [b1, b2, b3] = parseVersion(v2);
475
+ if (a1 !== b1) return a1 > b1;
476
+ if (a2 !== b2) return a2 > b2;
477
+ return a3 >= b3;
478
+ }
479
+
480
+ function isSemverLike(value) {
481
+ return /^v?\d+(\.\d+){1,2}$/.test(value);
482
+ }
483
+
484
+ // Detect OpenClaw version
485
+ async function detectOpenClawVersion() {
486
+ try {
487
+ const result = await runCapture("openclaw", ["--version"], { shell: IS_WIN });
488
+ if (result.code === 0 && result.out) {
489
+ const match = result.out.match(/\d+\.\d+(\.\d+)?/);
490
+ if (match) return match[0];
491
+ }
492
+ } catch {}
493
+ return "0.0.0";
494
+ }
495
+
496
+ // Try to fetch a URL, return response text or null
497
+ async function tryFetch(url, timeout = 15000) {
498
+ try {
499
+ const controller = new AbortController();
500
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
501
+ const response = await fetch(url, { signal: controller.signal });
502
+ clearTimeout(timeoutId);
503
+ if (response.ok) {
504
+ return await response.text();
505
+ }
506
+ } catch {}
507
+ return null;
508
+ }
509
+
510
+ // Check if a remote file exists
511
+ async function testRemoteFile(url) {
512
+ try {
513
+ const controller = new AbortController();
514
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
515
+ const response = await fetch(url, { method: "HEAD", signal: controller.signal });
516
+ clearTimeout(timeoutId);
517
+ return response.ok;
518
+ } catch {}
519
+ return false;
520
+ }
521
+
522
+ // Resolve plugin configuration from manifest or fallback
523
+ async function resolvePluginConfig() {
524
+ const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
525
+
526
+ info(tr(`Resolving plugin configuration for version: ${PLUGIN_VERSION}`, `正在解析插件配置,版本: ${PLUGIN_VERSION}`));
527
+
528
+ let pluginDir = "";
529
+ let manifestData = null;
530
+
531
+ // Try to detect plugin directory and download manifest
532
+ const manifestCurrent = await tryFetch(`${ghRaw}/examples/openclaw-plugin/install-manifest.json`);
533
+ if (manifestCurrent) {
534
+ pluginDir = "openclaw-plugin";
535
+ try {
536
+ manifestData = JSON.parse(manifestCurrent);
537
+ } catch {}
538
+ info(tr("Found manifest in openclaw-plugin", "在 openclaw-plugin 中找到 manifest"));
539
+ } else {
540
+ const manifestLegacy = await tryFetch(`${ghRaw}/examples/openclaw-memory-plugin/install-manifest.json`);
541
+ if (manifestLegacy) {
542
+ pluginDir = "openclaw-memory-plugin";
543
+ try {
544
+ manifestData = JSON.parse(manifestLegacy);
545
+ } catch {}
546
+ info(tr("Found manifest in openclaw-memory-plugin", "在 openclaw-memory-plugin 中找到 manifest"));
547
+ } else if (await testRemoteFile(`${ghRaw}/examples/openclaw-plugin/index.ts`)) {
548
+ pluginDir = "openclaw-plugin";
549
+ info(tr("No manifest found, using fallback for openclaw-plugin", "未找到 manifest,使用 openclaw-plugin 回退配置"));
550
+ } else if (await testRemoteFile(`${ghRaw}/examples/openclaw-memory-plugin/index.ts`)) {
551
+ pluginDir = "openclaw-memory-plugin";
552
+ info(tr("No manifest found, using fallback for openclaw-memory-plugin", "未找到 manifest,使用 openclaw-memory-plugin 回退配置"));
553
+ } else {
554
+ err(tr(`Cannot find plugin directory for version: ${PLUGIN_VERSION}`, `无法找到版本 ${PLUGIN_VERSION} 的插件目录`));
555
+ process.exit(1);
556
+ }
557
+ }
558
+
559
+ resolvedPluginDir = pluginDir;
560
+
561
+ if (manifestData) {
562
+ resolvedPluginId = manifestData.plugin?.id || "";
563
+ resolvedPluginKind = manifestData.plugin?.kind || "";
564
+ resolvedPluginSlot = manifestData.plugin?.slot || "";
565
+ resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
566
+ resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
567
+ resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
568
+ resolvedFilesRequired = manifestData.files?.required || [];
569
+ resolvedFilesOptional = manifestData.files?.optional || [];
570
+ } else {
571
+ // No manifest — determine plugin identity by package.json name
572
+ let fallbackKey = pluginDir === "openclaw-memory-plugin" ? "legacy" : "current";
573
+ let compatVer = "";
574
+
575
+ const pkgJson = await tryFetch(`${ghRaw}/examples/${pluginDir}/package.json`);
576
+ if (pkgJson) {
577
+ try {
578
+ const pkg = JSON.parse(pkgJson);
579
+ const pkgName = pkg.name || "";
580
+ if (pkgName && pkgName !== "@openclaw/openviking") {
581
+ fallbackKey = "legacy";
582
+ info(tr(`Detected legacy plugin by package name: ${pkgName}`, `通过 package.json 名称检测到旧版插件: ${pkgName}`));
583
+ } else if (pkgName) {
584
+ fallbackKey = "current";
585
+ }
586
+ compatVer = (pkg.engines?.openclaw || "").replace(/^>=?\s*/, "").trim();
587
+ if (compatVer) {
588
+ info(tr(`Read minOpenclawVersion from package.json engines.openclaw: >=${compatVer}`, `从 package.json engines.openclaw 读取到最低版本: >=${compatVer}`));
589
+ }
590
+ } catch {}
591
+ }
592
+
593
+ const fallback = fallbackKey === "legacy" ? FALLBACK_LEGACY : FALLBACK_CURRENT;
594
+ resolvedPluginDir = pluginDir;
595
+ resolvedPluginId = fallback.id;
596
+ resolvedPluginKind = fallback.kind;
597
+ resolvedPluginSlot = fallback.slot;
598
+ resolvedFilesRequired = fallback.required;
599
+ resolvedFilesOptional = fallback.optional;
600
+ resolvedNpmOmitDev = true;
601
+
602
+ // If no compatVer from package.json, try main branch manifest
603
+ if (!compatVer && PLUGIN_VERSION !== "main") {
604
+ const mainRaw = `https://raw.githubusercontent.com/${REPO}/main`;
605
+ const mainManifest = await tryFetch(`${mainRaw}/examples/openclaw-plugin/install-manifest.json`);
606
+ if (mainManifest) {
607
+ try {
608
+ const m = JSON.parse(mainManifest);
609
+ compatVer = m.compatibility?.minOpenclawVersion || "";
610
+ if (compatVer) {
611
+ info(tr(`Read minOpenclawVersion from main branch manifest: >=${compatVer}`, `从 main 分支 manifest 读取到最低版本: >=${compatVer}`));
612
+ }
613
+ } catch {}
614
+ }
615
+ }
616
+
617
+ resolvedMinOpenclawVersion = compatVer || "2026.3.7";
618
+ resolvedMinOpenvikingVersion = "";
619
+ }
620
+
621
+ // Set plugin destination
622
+ PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId);
623
+
624
+ info(tr(`Plugin: ${resolvedPluginId} (${resolvedPluginKind})`, `插件: ${resolvedPluginId} (${resolvedPluginKind})`));
625
+ }
626
+
627
+ // Check OpenClaw version compatibility
628
+ async function checkOpenClawCompatibility() {
629
+ if (process.env.SKIP_OPENCLAW === "1") {
630
+ return;
631
+ }
632
+
633
+ const ocVersion = await detectOpenClawVersion();
634
+ info(tr(`Detected OpenClaw version: ${ocVersion}`, `检测到 OpenClaw 版本: ${ocVersion}`));
635
+
636
+ // If no minimum version required, pass
637
+ if (!resolvedMinOpenclawVersion) {
638
+ return;
639
+ }
640
+
641
+ // If user explicitly requested an old version, pass
642
+ if (PLUGIN_VERSION !== "main" && isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.9")) {
643
+ return;
644
+ }
645
+
646
+ // Check compatibility
647
+ if (!versionGte(ocVersion, resolvedMinOpenclawVersion)) {
648
+ err(tr(
649
+ `OpenClaw ${ocVersion} does not support this plugin (requires >= ${resolvedMinOpenclawVersion})`,
650
+ `OpenClaw ${ocVersion} 不支持此插件(需要 >= ${resolvedMinOpenclawVersion})`
651
+ ));
652
+ console.log("");
653
+ bold(tr("Please choose one of the following options:", "请选择以下方案之一:"));
654
+ console.log("");
655
+ console.log(` ${tr("Option 1: Upgrade OpenClaw", "方案 1:升级 OpenClaw")}`);
656
+ console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
657
+ console.log("");
658
+ console.log(` ${tr("Option 2: Install legacy plugin (v0.2.8)", "方案 2:安装旧版插件 (v0.2.8)")}`);
659
+ console.log(` node install.js --plugin-version=v0.2.8${langZh ? " --zh" : ""}`);
660
+ console.log("");
661
+ process.exit(1);
662
+ }
663
+ }
664
+
665
+ function checkRequestedOpenVikingCompatibility() {
666
+ if (!resolvedMinOpenvikingVersion || !openvikingVersion) return;
667
+ if (versionGte(openvikingVersion, resolvedMinOpenvikingVersion)) return;
668
+
669
+ err(tr(
670
+ `OpenViking ${openvikingVersion} does not support this plugin (requires >= ${resolvedMinOpenvikingVersion})`,
671
+ `OpenViking ${openvikingVersion} 不支持此插件(需要 >= ${resolvedMinOpenvikingVersion})`,
672
+ ));
673
+ console.log("");
674
+ console.log(tr(
675
+ "Use a newer OpenViking version, or omit --openviking-version to install the latest release.",
676
+ "请使用更新版本的 OpenViking,或省略 --openviking-version 以安装最新版本。",
677
+ ));
678
+ process.exit(1);
679
+ }
680
+
392
681
  async function installOpenViking() {
393
682
  if (process.env.SKIP_OPENVIKING === "1") {
394
683
  info(tr("Skipping OpenViking install (SKIP_OPENVIKING=1)", "跳过 OpenViking 安装 (SKIP_OPENVIKING=1)"));
@@ -400,6 +689,12 @@ async function installOpenViking() {
400
689
  err(tr("Python check failed.", "Python 校验失败"));
401
690
  process.exit(1);
402
691
  }
692
+ if (!python.ok) {
693
+ warn(tr(
694
+ `${python.detail}. Will attempt to find a suitable Python for pip install.`,
695
+ `${python.detail}。将尝试查找合适的 Python 进行 pip 安装。`,
696
+ ));
697
+ }
403
698
 
404
699
  const py = python.cmd;
405
700
 
@@ -412,14 +707,20 @@ async function installOpenViking() {
412
707
  return;
413
708
  }
414
709
 
415
- info(tr("Installing OpenViking from PyPI...", "正在安装 OpenViking (PyPI)..."));
710
+ // Determine package spec
711
+ const pkgSpec = openvikingVersion ? `openviking==${openvikingVersion}` : "openviking";
712
+ if (openvikingVersion) {
713
+ info(tr(`Installing OpenViking ${openvikingVersion} from PyPI...`, `正在安装 OpenViking ${openvikingVersion} (PyPI)...`));
714
+ } else {
715
+ info(tr("Installing OpenViking (latest) from PyPI...", "正在安装 OpenViking (最新版) (PyPI)..."));
716
+ }
416
717
  info(tr(`Using pip index: ${PIP_INDEX_URL}`, `使用 pip 镜像源: ${PIP_INDEX_URL}`));
417
718
 
418
- info(`Package: ${OPENVIKING_PIP_SPEC}`);
719
+ info(`Package: ${pkgSpec}`);
419
720
  await runCapture(py, ["-m", "pip", "install", "--upgrade", "pip", "-q", "-i", PIP_INDEX_URL], { shell: false });
420
721
  const installResult = await runLiveCapture(
421
722
  py,
422
- ["-m", "pip", "install", "--progress-bar", "on", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL],
723
+ ["-m", "pip", "install", "--progress-bar", "on", pkgSpec, "-i", PIP_INDEX_URL],
423
724
  { shell: false },
424
725
  );
425
726
  if (installResult.code === 0) {
@@ -439,7 +740,7 @@ async function installOpenViking() {
439
740
  if (reuseCheck.code === 0) {
440
741
  await runLiveCapture(
441
742
  venvPy,
442
- ["-m", "pip", "install", "--progress-bar", "on", "-U", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL],
743
+ ["-m", "pip", "install", "--progress-bar", "on", "-U", pkgSpec, "-i", PIP_INDEX_URL],
443
744
  { shell: false },
444
745
  );
445
746
  openvikingPythonPath = venvPy;
@@ -475,7 +776,7 @@ async function installOpenViking() {
475
776
  await runCapture(venvPy, ["-m", "pip", "install", "--upgrade", "pip", "-q", "-i", PIP_INDEX_URL], { shell: false });
476
777
  const venvInstall = await runLiveCapture(
477
778
  venvPy,
478
- ["-m", "pip", "install", "--progress-bar", "on", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL],
779
+ ["-m", "pip", "install", "--progress-bar", "on", pkgSpec, "-i", PIP_INDEX_URL],
479
780
  { shell: false },
480
781
  );
481
782
  if (venvInstall.code === 0) {
@@ -492,7 +793,7 @@ async function installOpenViking() {
492
793
  if (process.env.OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES === "1") {
493
794
  const systemInstall = await runLiveCapture(
494
795
  py,
495
- ["-m", "pip", "install", "--progress-bar", "on", "--break-system-packages", OPENVIKING_PIP_SPEC, "-i", PIP_INDEX_URL],
796
+ ["-m", "pip", "install", "--progress-bar", "on", "--break-system-packages", pkgSpec, "-i", PIP_INDEX_URL],
496
797
  { shell: false },
497
798
  );
498
799
  if (systemInstall.code === 0) {
@@ -549,9 +850,17 @@ async function configureOvConf() {
549
850
  vectordb: { name: "context", backend: "local", project: "default" },
550
851
  agfs: { port: agfsPortNum, log_level: "warn", backend: "local", timeout: 10, retry_times: 3 },
551
852
  },
853
+ log: {
854
+ level: "WARNING",
855
+ format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
856
+ output: "file",
857
+ rotation: true,
858
+ rotation_days: 3,
859
+ rotation_interval: "midnight",
860
+ },
552
861
  embedding: {
553
862
  dense: {
554
- backend: "volcengine",
863
+ provider: "volcengine",
555
864
  api_key: embeddingApiKey || null,
556
865
  model: embeddingModel,
557
866
  api_base: "https://ark.cn-beijing.volces.com/api/v3",
@@ -560,7 +869,7 @@ async function configureOvConf() {
560
869
  },
561
870
  },
562
871
  vlm: {
563
- backend: "volcengine",
872
+ provider: "volcengine",
564
873
  api_key: vlmApiKey || null,
565
874
  model: vlmModel,
566
875
  api_base: "https://ark.cn-beijing.volces.com/api/v3",
@@ -574,9 +883,7 @@ async function configureOvConf() {
574
883
  info(tr(`Config generated: ${configPath}`, `已生成配置: ${configPath}`));
575
884
  }
576
885
 
577
- async function downloadPluginFile(relPath, required, index, total) {
578
- const fileName = relPath.split("/").pop();
579
- const url = `${GH_RAW}/${relPath}`;
886
+ async function downloadPluginFile(fileName, url, required, index, total) {
580
887
  const maxRetries = 3;
581
888
 
582
889
  process.stdout.write(` [${index}/${total}] ${fileName} `);
@@ -591,7 +898,7 @@ async function downloadPluginFile(relPath, required, index, total) {
591
898
  return;
592
899
  }
593
900
  if (!required && response.status === 404) {
594
- console.log(tr("(not present in target branch, skipped)", "(目标分支不存在,已跳过)"));
901
+ console.log(tr("(skipped)", "(跳过)"));
595
902
  return;
596
903
  }
597
904
  } catch {}
@@ -607,98 +914,188 @@ async function downloadPluginFile(relPath, required, index, total) {
607
914
  return;
608
915
  }
609
916
 
917
+ if (!required) {
918
+ console.log(tr("(skipped)", "(跳过)"));
919
+ return;
920
+ }
921
+
610
922
  console.log("");
611
923
  err(tr(`Download failed: ${url}`, `下载失败: ${url}`));
612
924
  process.exit(1);
613
925
  }
614
926
 
615
927
  async function downloadPlugin() {
928
+ const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
929
+ const pluginDir = resolvedPluginDir;
930
+ const total = resolvedFilesRequired.length + resolvedFilesOptional.length;
931
+
616
932
  await mkdir(PLUGIN_DEST, { recursive: true });
617
- const files = [
618
- ...REQUIRED_PLUGIN_FILES.map((relPath) => ({ relPath, required: true })),
619
- ...OPTIONAL_PLUGIN_FILES.map((relPath) => ({ relPath, required: false })),
620
- ];
621
933
 
622
- info(tr(`Downloading memory-openviking plugin from ${REPO}@${BRANCH}...`, `正在从 ${REPO}@${BRANCH} 下载 memory-openviking 插件...`));
623
- for (let i = 0; i < files.length; i++) {
624
- const file = files[i];
625
- await downloadPluginFile(file.relPath, file.required, i + 1, files.length);
934
+ info(tr(`Downloading plugin from ${REPO}@${PLUGIN_VERSION} (${total} files)...`, `正在从 ${REPO}@${PLUGIN_VERSION} 下载插件(共 ${total} 个文件)...`));
935
+
936
+ let i = 0;
937
+ // Download required files
938
+ for (const name of resolvedFilesRequired) {
939
+ if (!name) continue;
940
+ i++;
941
+ const url = `${ghRaw}/examples/${pluginDir}/${name}`;
942
+ await downloadPluginFile(name, url, true, i, total);
943
+ }
944
+
945
+ // Download optional files
946
+ for (const name of resolvedFilesOptional) {
947
+ if (!name) continue;
948
+ i++;
949
+ const url = `${ghRaw}/examples/${pluginDir}/${name}`;
950
+ await downloadPluginFile(name, url, false, i, total);
626
951
  }
627
952
 
953
+ // npm install
628
954
  info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
629
- await run("npm", ["install", "--no-audit", "--no-fund"], { cwd: PLUGIN_DEST, silent: false });
955
+ const npmArgs = resolvedNpmOmitDev
956
+ ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
957
+ : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
958
+ await run("npm", npmArgs, { cwd: PLUGIN_DEST, silent: false });
630
959
  info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
631
960
  }
632
961
 
633
- async function configureOpenClawPlugin(pluginPath = PLUGIN_DEST) {
962
+ async function deployLocalPlugin(localPluginDir) {
963
+ await rm(PLUGIN_DEST, { recursive: true, force: true });
964
+ await mkdir(PLUGIN_DEST, { recursive: true });
965
+ await cp(localPluginDir, PLUGIN_DEST, {
966
+ recursive: true,
967
+ force: true,
968
+ filter: (sourcePath) => {
969
+ const rel = relative(localPluginDir, sourcePath);
970
+ if (!rel) return true;
971
+ const firstSegment = rel.split(/[\\/]/)[0];
972
+ return firstSegment !== "node_modules" && firstSegment !== ".git";
973
+ },
974
+ });
975
+ }
976
+
977
+ async function configureOpenClawPlugin() {
634
978
  info(tr("Configuring OpenClaw plugin...", "正在配置 OpenClaw 插件..."));
635
979
 
636
- const configPath = join(OPENCLAW_DIR, "openclaw.json");
637
- let config = {};
980
+ const pluginId = resolvedPluginId;
981
+ const pluginSlot = resolvedPluginSlot;
638
982
 
639
- if (existsSync(configPath)) {
640
- try {
641
- const raw = await readFile(configPath, "utf8");
642
- if (raw.trim()) config = JSON.parse(raw);
643
- } catch {
644
- warn(tr("Existing openclaw.json invalid. Rebuilding required sections.", "已有 openclaw.json 非法,将重建相关配置节点。"));
645
- }
983
+ const ocEnv = { ...process.env };
984
+ if (OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
985
+ ocEnv.OPENCLAW_STATE_DIR = OPENCLAW_DIR;
646
986
  }
647
987
 
648
- if (!config.plugins) config.plugins = {};
649
- if (!config.gateway) config.gateway = {};
650
- if (!config.plugins.slots) config.plugins.slots = {};
651
- if (!config.plugins.load) config.plugins.load = {};
652
- if (!config.plugins.entries) config.plugins.entries = {};
988
+ const oc = async (args) => {
989
+ const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
990
+ if (result.code !== 0) {
991
+ const detail = result.err || result.out;
992
+ throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
993
+ }
994
+ return result;
995
+ };
653
996
 
654
- const existingPaths = Array.isArray(config.plugins.load.paths) ? config.plugins.load.paths : [];
655
- config.plugins.enabled = true;
656
- config.plugins.allow = ["memory-openviking"];
657
- config.plugins.slots.memory = "memory-openviking";
658
- config.plugins.load.paths = [...new Set([...existingPaths, pluginPath])];
997
+ // Enable plugin (files already deployed to extensions dir by deployPlugin)
998
+ await oc(["plugins", "enable", pluginId]);
999
+ await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
659
1000
 
660
- const pluginConfig = {
661
- mode: selectedMode,
662
- targetUri: "viking://user/memories",
663
- autoRecall: true,
664
- autoCapture: true,
665
- };
1001
+ // Set gateway mode
1002
+ await oc(["config", "set", "gateway.mode", "local"]);
666
1003
 
1004
+ // Set plugin config for the selected mode
667
1005
  if (selectedMode === "local") {
668
- pluginConfig.configPath = join(OPENVIKING_DIR, "ov.conf");
669
- pluginConfig.port = selectedServerPort;
1006
+ const ovConfPath = join(OPENVIKING_DIR, "ov.conf");
1007
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "local"]);
1008
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.configPath`, ovConfPath]);
1009
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.port`, String(selectedServerPort)]);
670
1010
  } else {
671
- pluginConfig.baseUrl = remoteBaseUrl;
672
- if (remoteApiKey) pluginConfig.apiKey = remoteApiKey;
673
- if (remoteAgentId) pluginConfig.agentId = remoteAgentId;
1011
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "remote"]);
1012
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, remoteBaseUrl]);
1013
+ if (remoteApiKey) {
1014
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, remoteApiKey]);
1015
+ }
1016
+ if (remoteAgentId) {
1017
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.agentId`, remoteAgentId]);
1018
+ }
674
1019
  }
675
1020
 
676
- config.plugins.entries["memory-openviking"] = { config: pluginConfig };
677
- config.gateway.mode = "local";
1021
+ // Legacy (memory) plugins need explicit targetUri/autoRecall/autoCapture (new version has defaults in config.ts)
1022
+ if (resolvedPluginKind === "memory") {
1023
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.targetUri`, "viking://user/memories"]);
1024
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.autoRecall`, "true", "--json"]);
1025
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.autoCapture`, "true", "--json"]);
1026
+ }
678
1027
 
679
- await mkdir(OPENCLAW_DIR, { recursive: true });
680
- await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
681
1028
  info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
682
1029
  }
683
1030
 
1031
+ async function discoverOpenvikingPython(failedPy) {
1032
+ const candidates = IS_WIN
1033
+ ? ["python3", "python", "py -3"]
1034
+ : ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
1035
+ for (const candidate of candidates) {
1036
+ if (candidate === failedPy) continue;
1037
+ const resolved = await resolveAbsoluteCommand(candidate);
1038
+ if (!resolved || resolved === candidate || resolved === failedPy) continue;
1039
+ const check = await runCapture(resolved, ["-c", "import openviking"], { shell: false });
1040
+ if (check.code === 0) return resolved;
1041
+ }
1042
+ return "";
1043
+ }
1044
+
684
1045
  async function resolvePythonPath() {
685
1046
  if (openvikingPythonPath) return openvikingPythonPath;
686
1047
  const python = await checkPython();
687
- const py = python.cmd;
688
- if (!py) return "";
689
-
690
- if (IS_WIN) {
691
- const result = await runCapture("where", [py], { shell: true });
692
- return result.out.split(/\r?\n/)[0]?.trim() || py;
693
- }
694
-
695
- const result = await runCapture("which", [py], { shell: false });
696
- return result.out.trim() || py;
1048
+ return python.cmd || "";
697
1049
  }
698
1050
 
699
1051
  async function writeOpenvikingEnv({ includePython }) {
700
1052
  const needStateDir = OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR;
701
- const pythonPath = includePython ? await resolvePythonPath() : "";
1053
+ let pythonPath = "";
1054
+ if (includePython) {
1055
+ pythonPath = await resolvePythonPath();
1056
+ if (!pythonPath) {
1057
+ pythonPath = (process.env.OPENVIKING_PYTHON || "").trim() || (IS_WIN ? "python" : "python3");
1058
+ warn(
1059
+ tr(
1060
+ "Could not resolve absolute Python path; wrote fallback OPENVIKING_PYTHON to openviking.env. Edit that file if OpenViking fails to start.",
1061
+ "未能解析 Python 绝对路径,已在 openviking.env 中写入后备值。若启动失败请手动修改为虚拟环境中的 python 可执行文件路径。",
1062
+ ),
1063
+ );
1064
+ }
1065
+
1066
+ // Verify the resolved Python can actually import openviking
1067
+ if (pythonPath) {
1068
+ const verify = await runCapture(pythonPath, ["-c", "import openviking"], { shell: false });
1069
+ if (verify.code !== 0) {
1070
+ warn(
1071
+ tr(
1072
+ `Resolved Python (${pythonPath}) cannot import openviking. The pip install target may differ from the runtime python3.`,
1073
+ `解析到的 Python(${pythonPath})无法 import openviking。pip 安装目标可能与运行时的 python3 不一致。`,
1074
+ ),
1075
+ );
1076
+ // Try to discover the correct Python via pip show
1077
+ const corrected = await discoverOpenvikingPython(pythonPath);
1078
+ if (corrected) {
1079
+ info(
1080
+ tr(
1081
+ `Auto-corrected OPENVIKING_PYTHON to ${corrected}`,
1082
+ `已自动修正 OPENVIKING_PYTHON 为 ${corrected}`,
1083
+ ),
1084
+ );
1085
+ pythonPath = corrected;
1086
+ } else {
1087
+ warn(
1088
+ tr(
1089
+ `Could not auto-detect the correct Python. Edit OPENVIKING_PYTHON in the env file manually.`,
1090
+ `无法自动检测正确的 Python。请手动修改 env 文件中的 OPENVIKING_PYTHON。`,
1091
+ ),
1092
+ );
1093
+ }
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ // Remote mode + default state dir + no python line → nothing to persist
702
1099
  if (!needStateDir && !pythonPath) return null;
703
1100
 
704
1101
  await mkdir(OPENCLAW_DIR, { recursive: true });
@@ -752,6 +1149,11 @@ async function main() {
752
1149
 
753
1150
  await selectWorkdir();
754
1151
  info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
1152
+ info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
1153
+ info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
1154
+ if (openvikingVersion) {
1155
+ info(tr(`OpenViking version: ${openvikingVersion}`, `OpenViking 版本: ${openvikingVersion}`));
1156
+ }
755
1157
 
756
1158
  await selectMode();
757
1159
  info(tr(`Mode: ${selectedMode}`, `模式: ${selectedMode}`));
@@ -759,28 +1161,38 @@ async function main() {
759
1161
  if (selectedMode === "local") {
760
1162
  await validateEnvironment();
761
1163
  await checkOpenClaw();
1164
+ // Resolve plugin config after OpenClaw is available (for version detection)
1165
+ await resolvePluginConfig();
1166
+ await checkOpenClawCompatibility();
1167
+ checkRequestedOpenVikingCompatibility();
762
1168
  await installOpenViking();
763
1169
  await configureOvConf();
764
1170
  } else {
765
1171
  await checkOpenClaw();
1172
+ await resolvePluginConfig();
1173
+ await checkOpenClawCompatibility();
766
1174
  await collectRemoteConfig();
767
1175
  }
768
1176
 
769
1177
  let pluginPath;
770
- const localPluginDir = openvikingRepo ? join(openvikingRepo, "examples", "openclaw-memory-plugin") : "";
1178
+ const localPluginDir = openvikingRepo ? join(openvikingRepo, "examples", resolvedPluginDir || "openclaw-plugin") : "";
771
1179
  if (openvikingRepo && existsSync(join(localPluginDir, "index.ts"))) {
772
1180
  pluginPath = localPluginDir;
1181
+ PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId || "openviking");
773
1182
  info(tr(`Using local plugin from repo: ${pluginPath}`, `使用仓库内插件: ${pluginPath}`));
774
- if (!existsSync(join(pluginPath, "node_modules"))) {
1183
+ await deployLocalPlugin(pluginPath);
775
1184
  info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
776
- await run("npm", ["install", "--no-audit", "--no-fund"], { cwd: pluginPath, silent: false });
777
- }
1185
+ const npmArgs = resolvedNpmOmitDev
1186
+ ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1187
+ : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1188
+ await run("npm", npmArgs, { cwd: PLUGIN_DEST, silent: false });
1189
+ pluginPath = PLUGIN_DEST;
778
1190
  } else {
779
1191
  await downloadPlugin();
780
1192
  pluginPath = PLUGIN_DEST;
781
1193
  }
782
1194
 
783
- await configureOpenClawPlugin(pluginPath);
1195
+ await configureOpenClawPlugin();
784
1196
  const envFiles = await writeOpenvikingEnv({
785
1197
  includePython: selectedMode === "local",
786
1198
  });
@@ -803,6 +1215,14 @@ async function main() {
803
1215
  console.log("");
804
1216
 
805
1217
  if (selectedMode === "local") {
1218
+ if (envFiles?.shellPath && !IS_WIN) {
1219
+ info(
1220
+ tr(
1221
+ 'If source fails, set: export OPENVIKING_PYTHON="$(command -v python3)"',
1222
+ '若 source 失败,可执行: export OPENVIKING_PYTHON="$(command -v python3)"',
1223
+ ),
1224
+ );
1225
+ }
806
1226
  info(tr(`You can edit the config freely: ${OPENVIKING_DIR}/ov.conf`, `你可以按需自由修改配置文件: ${OPENVIKING_DIR}/ov.conf`));
807
1227
  } else {
808
1228
  info(tr(`Remote server: ${remoteBaseUrl}`, `远程服务器: ${remoteBaseUrl}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-openviking-setup-helper",
3
- "version": "0.2.8",
3
+ "version": "0.2.9-dev.1",
4
4
  "description": "Setup helper for installing OpenViking memory plugin into OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "repository": {
22
22
  "type": "git",
23
23
  "url": "git+https://github.com/volcengine/OpenViking.git",
24
- "directory": "examples/openclaw-memory-plugin/setup-helper"
24
+ "directory": "examples/openclaw-plugin/setup-helper"
25
25
  },
26
26
  "files": [
27
27
  "install.js"