openclaw-openviking-setup-helper 0.2.9-dev.0 → 0.2.9-dev.2

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 +986 -183
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * openclaw-openviking-install
11
11
  *
12
12
  * Direct run:
13
- * node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ]
13
+ * node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ] [ --upgrade-plugin ]
14
14
  * [ --plugin-version=TAG ] [ --openviking-version=V ] [ --repo=PATH ]
15
15
  *
16
16
  * Environment variables:
@@ -23,9 +23,9 @@
23
23
  */
24
24
 
25
25
  import { spawn } from "node:child_process";
26
- import { cp, mkdir, rm, writeFile } from "node:fs/promises";
27
- import { existsSync, readdirSync } from "node:fs";
28
- import { dirname, join, relative } from "node:path";
26
+ import { cp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
27
+ import { existsSync, readdirSync } from "node:fs";
28
+ import { dirname, join, relative } from "node:path";
29
29
  import { createInterface } from "node:readline";
30
30
  import { fileURLToPath } from "node:url";
31
31
 
@@ -35,7 +35,7 @@ let REPO = process.env.REPO || "volcengine/OpenViking";
35
35
  // PLUGIN_VERSION takes precedence over BRANCH (legacy)
36
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://mirrors.volces.com/pypi/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 || "";
@@ -61,38 +61,49 @@ const FALLBACK_LEGACY = {
61
61
  optional: ["package-lock.json", ".gitignore"],
62
62
  };
63
63
 
64
- const FALLBACK_CURRENT = {
65
- dir: "openclaw-plugin",
66
- id: "openviking",
67
- kind: "context-engine",
68
- slot: "contextEngine",
69
- required: ["index.ts", "context-engine.ts", "config.ts", "client.ts", "process-manager.ts", "memory-ranking.ts", "text-utils.ts", "session-transcript-repair.ts", "tool-call-id.ts", "openclaw.plugin.json", "package.json", "tsconfig.json"],
70
- optional: ["package-lock.json", ".gitignore"],
71
- };
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
+ const PLUGIN_VARIANTS = [
74
+ { ...FALLBACK_LEGACY, generation: "legacy", slotFallback: "none" },
75
+ { ...FALLBACK_CURRENT, generation: "current", slotFallback: "legacy" },
76
+ ];
72
77
 
73
78
  // Resolved plugin config (set by resolvePluginConfig)
74
79
  let resolvedPluginDir = "";
75
80
  let resolvedPluginId = "";
76
81
  let resolvedPluginKind = "";
77
82
  let resolvedPluginSlot = "";
78
- let resolvedFilesRequired = [];
83
+ let resolvedFilesRequired = [];
79
84
  let resolvedFilesOptional = [];
80
85
  let resolvedNpmOmitDev = true;
81
86
  let resolvedMinOpenclawVersion = "";
82
87
  let resolvedMinOpenvikingVersion = "";
88
+ let resolvedPluginReleaseId = "";
83
89
 
84
- let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
85
- let langZh = false;
86
- let openvikingVersion = process.env.OPENVIKING_VERSION || "";
87
- let openvikingRepo = process.env.OPENVIKING_REPO || "";
88
- let workdirExplicit = false;
90
+ let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
91
+ let langZh = false;
92
+ let openvikingVersion = process.env.OPENVIKING_VERSION || "";
93
+ let openvikingRepo = process.env.OPENVIKING_REPO || "";
94
+ let workdirExplicit = false;
95
+ let upgradePluginOnly = false;
96
+ let rollbackLastUpgrade = false;
89
97
 
90
98
  let selectedMode = "local";
91
99
  let selectedServerPort = DEFAULT_SERVER_PORT;
92
- let remoteBaseUrl = "http://127.0.0.1:1933";
93
- let remoteApiKey = "";
94
- let remoteAgentId = "";
95
- let openvikingPythonPath = "";
100
+ let remoteBaseUrl = "http://127.0.0.1:1933";
101
+ let remoteApiKey = "";
102
+ let remoteAgentId = "";
103
+ let openvikingPythonPath = "";
104
+ let upgradeRuntimeConfig = null;
105
+ let installedUpgradeState = null;
106
+ let upgradeAudit = null;
96
107
 
97
108
  const argv = process.argv.slice(2);
98
109
  for (let i = 0; i < argv.length; i++) {
@@ -101,11 +112,19 @@ for (let i = 0; i < argv.length; i++) {
101
112
  installYes = true;
102
113
  continue;
103
114
  }
104
- if (arg === "--zh") {
105
- langZh = true;
106
- continue;
107
- }
108
- if (arg === "--workdir") {
115
+ if (arg === "--zh") {
116
+ langZh = true;
117
+ continue;
118
+ }
119
+ if (arg === "--upgrade-plugin" || arg === "--update" || arg === "--upgrade") {
120
+ upgradePluginOnly = true;
121
+ continue;
122
+ }
123
+ if (arg === "--rollback" || arg === "--rollback-last-upgrade") {
124
+ rollbackLastUpgrade = true;
125
+ continue;
126
+ }
127
+ if (arg === "--workdir") {
109
128
  const workdir = argv[i + 1]?.trim();
110
129
  if (!workdir) {
111
130
  console.error("--workdir requires a path");
@@ -178,12 +197,16 @@ function printHelp() {
178
197
  console.log("Options:");
179
198
  console.log(" --github-repo=OWNER/REPO GitHub repository (default: volcengine/OpenViking)");
180
199
  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");
200
+ console.log(" --openviking-version=V OpenViking PyPI version (e.g. 0.2.9, default: latest)");
201
+ console.log(" --workdir PATH OpenClaw config directory (default: ~/.openclaw)");
202
+ console.log(" --repo=PATH Use local OpenViking repo at PATH (pip -e + local plugin)");
203
+ console.log(" --update, --upgrade-plugin");
204
+ console.log(" Backup old plugin, clean only OpenViking plugin config, keep ov.conf");
205
+ console.log(" --rollback, --rollback-last-upgrade");
206
+ console.log(" Roll back the last plugin upgrade using the saved audit/backup files");
207
+ console.log(" -y, --yes Non-interactive (use defaults)");
208
+ console.log(" --zh Chinese prompts");
209
+ console.log(" -h, --help This help");
187
210
  console.log("");
188
211
  console.log("Examples:");
189
212
  console.log(" # Install latest version");
@@ -191,12 +214,18 @@ function printHelp() {
191
214
  console.log("");
192
215
  console.log(" # Install from a fork repository");
193
216
  console.log(" node install.js --github-repo=yourname/OpenViking --plugin-version=dev-branch");
194
- console.log("");
195
- console.log(" # Install specific plugin version");
196
- console.log(" node install.js --plugin-version=v0.2.8");
197
- console.log("");
198
- console.log("Env: REPO, PLUGIN_VERSION, OPENVIKING_VERSION, SKIP_OPENCLAW, SKIP_OPENVIKING, NPM_REGISTRY, PIP_INDEX_URL");
199
- }
217
+ console.log("");
218
+ console.log(" # Install specific plugin version");
219
+ console.log(" node install.js --plugin-version=v0.2.8");
220
+ console.log("");
221
+ console.log(" # Upgrade only the plugin files");
222
+ console.log(" node install.js --update --plugin-version=main");
223
+ console.log("");
224
+ console.log(" # Roll back the last plugin upgrade");
225
+ console.log(" node install.js --rollback");
226
+ console.log("");
227
+ console.log("Env: REPO, PLUGIN_VERSION, OPENVIKING_VERSION, SKIP_OPENCLAW, SKIP_OPENVIKING, NPM_REGISTRY, PIP_INDEX_URL");
228
+ }
200
229
 
201
230
  function tr(en, zh) {
202
231
  return langZh ? zh : en;
@@ -296,8 +325,19 @@ function question(prompt, defaultValue = "") {
296
325
  });
297
326
  }
298
327
 
328
+ async function resolveAbsoluteCommand(cmd) {
329
+ if (cmd.startsWith("/") || (IS_WIN && /^[A-Za-z]:[/\\]/.test(cmd))) return cmd;
330
+ if (IS_WIN) {
331
+ const r = await runCapture("where", [cmd], { shell: true });
332
+ return r.out.split(/\r?\n/)[0]?.trim() || cmd;
333
+ }
334
+ const r = await runCapture("which", [cmd], { shell: false });
335
+ return r.out.trim() || cmd;
336
+ }
337
+
299
338
  async function checkPython() {
300
- const py = process.env.OPENVIKING_PYTHON || (IS_WIN ? "python" : "python3");
339
+ const raw = process.env.OPENVIKING_PYTHON || (IS_WIN ? "python" : "python3");
340
+ const py = await resolveAbsoluteCommand(raw);
301
341
  const result = await runCapture(py, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"]);
302
342
  if (result.code !== 0 || !result.out) {
303
343
  return {
@@ -452,7 +492,7 @@ async function checkOpenClaw() {
452
492
  }
453
493
 
454
494
  // Compare versions: returns true if v1 >= v2
455
- function versionGte(v1, v2) {
495
+ function versionGte(v1, v2) {
456
496
  const parseVersion = (v) => {
457
497
  const cleaned = v.replace(/^v/, "").replace(/-.*$/, "");
458
498
  const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10) || 0);
@@ -463,15 +503,28 @@ function versionGte(v1, v2) {
463
503
  const [b1, b2, b3] = parseVersion(v2);
464
504
  if (a1 !== b1) return a1 > b1;
465
505
  if (a2 !== b2) return a2 > b2;
466
- return a3 >= b3;
467
- }
468
-
506
+ return a3 >= b3;
507
+ }
508
+
469
509
  function isSemverLike(value) {
470
510
  return /^v?\d+(\.\d+){1,2}$/.test(value);
471
511
  }
472
-
473
- // Detect OpenClaw version
474
- async function detectOpenClawVersion() {
512
+
513
+ function validateRequestedPluginVersion() {
514
+ if (!isSemverLike(PLUGIN_VERSION)) return;
515
+ if (versionGte(PLUGIN_VERSION, "v0.2.7") && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
516
+ err(tr("Plugin version v0.2.7 does not exist.", "插件版本 v0.2.7 不存在。"));
517
+ process.exit(1);
518
+ }
519
+ }
520
+
521
+ if (upgradePluginOnly && rollbackLastUpgrade) {
522
+ console.error("--update/--upgrade-plugin and --rollback cannot be used together");
523
+ process.exit(1);
524
+ }
525
+
526
+ // Detect OpenClaw version
527
+ async function detectOpenClawVersion() {
475
528
  try {
476
529
  const result = await runCapture("openclaw", ["--version"], { shell: IS_WIN });
477
530
  if (result.code === 0 && result.out) {
@@ -545,21 +598,45 @@ async function resolvePluginConfig() {
545
598
  }
546
599
  }
547
600
 
548
- resolvedPluginDir = pluginDir;
549
-
601
+ resolvedPluginDir = pluginDir;
602
+ resolvedPluginReleaseId = "";
603
+
550
604
  if (manifestData) {
551
605
  resolvedPluginId = manifestData.plugin?.id || "";
552
606
  resolvedPluginKind = manifestData.plugin?.kind || "";
553
607
  resolvedPluginSlot = manifestData.plugin?.slot || "";
554
608
  resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
555
609
  resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
610
+ resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
556
611
  resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
557
612
  resolvedFilesRequired = manifestData.files?.required || [];
558
613
  resolvedFilesOptional = manifestData.files?.optional || [];
559
614
  } else {
560
- // Use fallback config
561
- const fallback = pluginDir === "openclaw-memory-plugin" ? FALLBACK_LEGACY : FALLBACK_CURRENT;
562
- resolvedPluginDir = fallback.dir;
615
+ // No manifest — determine plugin identity by package.json name
616
+ let fallbackKey = pluginDir === "openclaw-memory-plugin" ? "legacy" : "current";
617
+ let compatVer = "";
618
+
619
+ const pkgJson = await tryFetch(`${ghRaw}/examples/${pluginDir}/package.json`);
620
+ if (pkgJson) {
621
+ try {
622
+ const pkg = JSON.parse(pkgJson);
623
+ const pkgName = pkg.name || "";
624
+ resolvedPluginReleaseId = pkg.version || "";
625
+ if (pkgName && pkgName !== "@openclaw/openviking") {
626
+ fallbackKey = "legacy";
627
+ info(tr(`Detected legacy plugin by package name: ${pkgName}`, `通过 package.json 名称检测到旧版插件: ${pkgName}`));
628
+ } else if (pkgName) {
629
+ fallbackKey = "current";
630
+ }
631
+ compatVer = (pkg.engines?.openclaw || "").replace(/^>=?\s*/, "").trim();
632
+ if (compatVer) {
633
+ info(tr(`Read minOpenclawVersion from package.json engines.openclaw: >=${compatVer}`, `从 package.json engines.openclaw 读取到最低版本: >=${compatVer}`));
634
+ }
635
+ } catch {}
636
+ }
637
+
638
+ const fallback = fallbackKey === "legacy" ? FALLBACK_LEGACY : FALLBACK_CURRENT;
639
+ resolvedPluginDir = pluginDir;
563
640
  resolvedPluginId = fallback.id;
564
641
  resolvedPluginKind = fallback.kind;
565
642
  resolvedPluginSlot = fallback.slot;
@@ -567,13 +644,24 @@ async function resolvePluginConfig() {
567
644
  resolvedFilesOptional = fallback.optional;
568
645
  resolvedNpmOmitDev = true;
569
646
 
570
- if (pluginDir === "openclaw-plugin") {
571
- resolvedMinOpenclawVersion = "3.7";
572
- } else {
573
- resolvedMinOpenclawVersion = "";
574
- }
575
- resolvedMinOpenvikingVersion = "";
576
- }
647
+ // If no compatVer from package.json, try main branch manifest
648
+ if (!compatVer && PLUGIN_VERSION !== "main") {
649
+ const mainRaw = `https://raw.githubusercontent.com/${REPO}/main`;
650
+ const mainManifest = await tryFetch(`${mainRaw}/examples/openclaw-plugin/install-manifest.json`);
651
+ if (mainManifest) {
652
+ try {
653
+ const m = JSON.parse(mainManifest);
654
+ compatVer = m.compatibility?.minOpenclawVersion || "";
655
+ if (compatVer) {
656
+ info(tr(`Read minOpenclawVersion from main branch manifest: >=${compatVer}`, `从 main 分支 manifest 读取到最低版本: >=${compatVer}`));
657
+ }
658
+ } catch {}
659
+ }
660
+ }
661
+
662
+ resolvedMinOpenclawVersion = compatVer || "2026.3.7";
663
+ resolvedMinOpenvikingVersion = "";
664
+ }
577
665
 
578
666
  // Set plugin destination
579
667
  PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId);
@@ -596,7 +684,7 @@ async function checkOpenClawCompatibility() {
596
684
  }
597
685
 
598
686
  // If user explicitly requested an old version, pass
599
- if (PLUGIN_VERSION !== "main" && isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.9")) {
687
+ if (PLUGIN_VERSION !== "main" && isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
600
688
  return;
601
689
  }
602
690
 
@@ -610,32 +698,32 @@ async function checkOpenClawCompatibility() {
610
698
  bold(tr("Please choose one of the following options:", "请选择以下方案之一:"));
611
699
  console.log("");
612
700
  console.log(` ${tr("Option 1: Upgrade OpenClaw", "方案 1:升级 OpenClaw")}`);
613
- console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
701
+ console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
614
702
  console.log("");
615
703
  console.log(` ${tr("Option 2: Install legacy plugin (v0.2.8)", "方案 2:安装旧版插件 (v0.2.8)")}`);
616
- console.log(` node install.js --plugin-version=v0.2.8${langZh ? " --zh" : ""}`);
704
+ console.log(` node install.js --plugin-version=v0.2.6${langZh ? " --zh" : ""}`);
617
705
  console.log("");
618
706
  process.exit(1);
619
707
  }
620
- }
621
-
622
- function checkRequestedOpenVikingCompatibility() {
623
- if (!resolvedMinOpenvikingVersion || !openvikingVersion) return;
624
- if (versionGte(openvikingVersion, resolvedMinOpenvikingVersion)) return;
625
-
626
- err(tr(
627
- `OpenViking ${openvikingVersion} does not support this plugin (requires >= ${resolvedMinOpenvikingVersion})`,
628
- `OpenViking ${openvikingVersion} 不支持此插件(需要 >= ${resolvedMinOpenvikingVersion})`,
629
- ));
630
- console.log("");
631
- console.log(tr(
632
- "Use a newer OpenViking version, or omit --openviking-version to install the latest release.",
633
- "请使用更新版本的 OpenViking,或省略 --openviking-version 以安装最新版本。",
634
- ));
635
- process.exit(1);
636
- }
637
-
638
- async function installOpenViking() {
708
+ }
709
+
710
+ function checkRequestedOpenVikingCompatibility() {
711
+ if (!resolvedMinOpenvikingVersion || !openvikingVersion) return;
712
+ if (versionGte(openvikingVersion, resolvedMinOpenvikingVersion)) return;
713
+
714
+ err(tr(
715
+ `OpenViking ${openvikingVersion} does not support this plugin (requires >= ${resolvedMinOpenvikingVersion})`,
716
+ `OpenViking ${openvikingVersion} 不支持此插件(需要 >= ${resolvedMinOpenvikingVersion})`,
717
+ ));
718
+ console.log("");
719
+ console.log(tr(
720
+ "Use a newer OpenViking version, or omit --openviking-version to install the latest release.",
721
+ "请使用更新版本的 OpenViking,或省略 --openviking-version 以安装最新版本。",
722
+ ));
723
+ process.exit(1);
724
+ }
725
+
726
+ async function installOpenViking() {
639
727
  if (process.env.SKIP_OPENVIKING === "1") {
640
728
  info(tr("Skipping OpenViking install (SKIP_OPENVIKING=1)", "跳过 OpenViking 安装 (SKIP_OPENVIKING=1)"));
641
729
  return;
@@ -646,6 +734,12 @@ async function installOpenViking() {
646
734
  err(tr("Python check failed.", "Python 校验失败"));
647
735
  process.exit(1);
648
736
  }
737
+ if (!python.ok) {
738
+ warn(tr(
739
+ `${python.detail}. Will attempt to find a suitable Python for pip install.`,
740
+ `${python.detail}。将尝试查找合适的 Python 进行 pip 安装。`,
741
+ ));
742
+ }
649
743
 
650
744
  const py = python.cmd;
651
745
 
@@ -809,20 +903,20 @@ async function configureOvConf() {
809
903
  rotation_days: 3,
810
904
  rotation_interval: "midnight",
811
905
  },
812
- embedding: {
813
- dense: {
814
- provider: "volcengine",
815
- api_key: embeddingApiKey || null,
816
- model: embeddingModel,
817
- api_base: "https://ark.cn-beijing.volces.com/api/v3",
906
+ embedding: {
907
+ dense: {
908
+ provider: "volcengine",
909
+ api_key: embeddingApiKey || null,
910
+ model: embeddingModel,
911
+ api_base: "https://ark.cn-beijing.volces.com/api/v3",
818
912
  dimension: 1024,
819
913
  input: "multimodal",
820
914
  },
821
- },
822
- vlm: {
823
- provider: "volcengine",
824
- api_key: vlmApiKey || null,
825
- model: vlmModel,
915
+ },
916
+ vlm: {
917
+ provider: "volcengine",
918
+ api_key: vlmApiKey || null,
919
+ model: vlmModel,
826
920
  api_base: "https://ark.cn-beijing.volces.com/api/v3",
827
921
  temperature: 0.1,
828
922
  max_retries: 3,
@@ -834,8 +928,507 @@ async function configureOvConf() {
834
928
  info(tr(`Config generated: ${configPath}`, `已生成配置: ${configPath}`));
835
929
  }
836
930
 
837
- async function downloadPluginFile(fileName, url, required, index, total) {
838
- const maxRetries = 3;
931
+ function getOpenClawConfigPath() {
932
+ return join(OPENCLAW_DIR, "openclaw.json");
933
+ }
934
+
935
+ function getOpenClawEnv() {
936
+ if (OPENCLAW_DIR === DEFAULT_OPENCLAW_DIR) {
937
+ return { ...process.env };
938
+ }
939
+ return { ...process.env, OPENCLAW_STATE_DIR: OPENCLAW_DIR };
940
+ }
941
+
942
+ async function readJsonFileIfExists(filePath) {
943
+ if (!existsSync(filePath)) return null;
944
+ const raw = await readFile(filePath, "utf8");
945
+ return JSON.parse(raw);
946
+ }
947
+
948
+ function getInstallStatePathForPlugin(pluginId) {
949
+ return join(OPENCLAW_DIR, "extensions", pluginId, ".ov-install-state.json");
950
+ }
951
+
952
+ function getUpgradeAuditDir() {
953
+ return join(OPENCLAW_DIR, ".openviking-upgrade-backup");
954
+ }
955
+
956
+ function getUpgradeAuditPath() {
957
+ return join(getUpgradeAuditDir(), "last-upgrade.json");
958
+ }
959
+
960
+ function getOpenClawConfigBackupPath() {
961
+ return join(getUpgradeAuditDir(), "openclaw.json.bak");
962
+ }
963
+
964
+ function normalizePluginMode(value) {
965
+ return value === "remote" ? "remote" : "local";
966
+ }
967
+
968
+ function getPluginVariantById(pluginId) {
969
+ return PLUGIN_VARIANTS.find((variant) => variant.id === pluginId) || null;
970
+ }
971
+
972
+ function detectPluginPresence(config, variant) {
973
+ const plugins = config?.plugins;
974
+ const reasons = [];
975
+ if (!plugins) {
976
+ return { variant, present: false, reasons };
977
+ }
978
+
979
+ if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
980
+ reasons.push("entry");
981
+ }
982
+ if (plugins.slots?.[variant.slot] === variant.id) {
983
+ reasons.push("slot");
984
+ }
985
+ if (Array.isArray(plugins.allow) && plugins.allow.includes(variant.id)) {
986
+ reasons.push("allow");
987
+ }
988
+ if (
989
+ Array.isArray(plugins.load?.paths)
990
+ && plugins.load.paths.some((item) => typeof item === "string" && (item.includes(variant.id) || item.includes(variant.dir)))
991
+ ) {
992
+ reasons.push("loadPath");
993
+ }
994
+ if (existsSync(join(OPENCLAW_DIR, "extensions", variant.id))) {
995
+ reasons.push("dir");
996
+ }
997
+
998
+ return { variant, present: reasons.length > 0, reasons };
999
+ }
1000
+
1001
+ async function detectInstalledPluginState() {
1002
+ const configPath = getOpenClawConfigPath();
1003
+ const config = await readJsonFileIfExists(configPath);
1004
+ const detections = [];
1005
+ for (const variant of PLUGIN_VARIANTS) {
1006
+ const detection = detectPluginPresence(config, variant);
1007
+ if (!detection.present) continue;
1008
+ detection.installState = await readJsonFileIfExists(getInstallStatePathForPlugin(variant.id));
1009
+ detections.push(detection);
1010
+ }
1011
+
1012
+ let generation = "none";
1013
+ if (detections.length === 1) {
1014
+ generation = detections[0].variant.generation;
1015
+ } else if (detections.length > 1) {
1016
+ generation = "mixed";
1017
+ }
1018
+
1019
+ return {
1020
+ config,
1021
+ configPath,
1022
+ detections,
1023
+ generation,
1024
+ };
1025
+ }
1026
+
1027
+ function formatInstalledDetectionLabel(detection) {
1028
+ const requestedRef = detection.installState?.requestedRef;
1029
+ const releaseId = detection.installState?.releaseId;
1030
+ if (requestedRef) return `${detection.variant.id}@${requestedRef}`;
1031
+ if (releaseId) return `${detection.variant.id}#${releaseId}`;
1032
+ return `${detection.variant.id} (${detection.variant.generation}, exact version unknown)`;
1033
+ }
1034
+
1035
+ function formatInstalledStateLabel(installedState) {
1036
+ if (!installedState?.detections?.length) {
1037
+ return "not-installed";
1038
+ }
1039
+ return installedState.detections.map(formatInstalledDetectionLabel).join(" + ");
1040
+ }
1041
+
1042
+ function formatTargetVersionLabel() {
1043
+ const base = `${resolvedPluginId || "openviking"}@${PLUGIN_VERSION}`;
1044
+ if (resolvedPluginReleaseId && resolvedPluginReleaseId !== PLUGIN_VERSION) {
1045
+ return `${base} (${resolvedPluginReleaseId})`;
1046
+ }
1047
+ return base;
1048
+ }
1049
+
1050
+ function extractRuntimeConfigFromPluginEntry(entryConfig) {
1051
+ if (!entryConfig || typeof entryConfig !== "object") return null;
1052
+
1053
+ const mode = normalizePluginMode(entryConfig.mode);
1054
+ const runtime = { mode };
1055
+
1056
+ if (mode === "remote") {
1057
+ if (typeof entryConfig.baseUrl === "string" && entryConfig.baseUrl.trim()) {
1058
+ runtime.baseUrl = entryConfig.baseUrl.trim();
1059
+ }
1060
+ if (typeof entryConfig.apiKey === "string" && entryConfig.apiKey.trim()) {
1061
+ runtime.apiKey = entryConfig.apiKey;
1062
+ }
1063
+ if (typeof entryConfig.agentId === "string" && entryConfig.agentId.trim()) {
1064
+ runtime.agentId = entryConfig.agentId.trim();
1065
+ }
1066
+ return runtime;
1067
+ }
1068
+
1069
+ if (typeof entryConfig.configPath === "string" && entryConfig.configPath.trim()) {
1070
+ runtime.configPath = entryConfig.configPath.trim();
1071
+ }
1072
+ if (entryConfig.port !== undefined && entryConfig.port !== null && `${entryConfig.port}`.trim()) {
1073
+ const parsedPort = Number.parseInt(String(entryConfig.port), 10);
1074
+ if (Number.isFinite(parsedPort) && parsedPort > 0) {
1075
+ runtime.port = parsedPort;
1076
+ }
1077
+ }
1078
+ return runtime;
1079
+ }
1080
+
1081
+ async function readPortFromOvConf(configPath) {
1082
+ const filePath = configPath || join(OPENVIKING_DIR, "ov.conf");
1083
+ if (!existsSync(filePath)) return null;
1084
+ try {
1085
+ const ovConf = await readJsonFileIfExists(filePath);
1086
+ const parsedPort = Number.parseInt(String(ovConf?.server?.port ?? ""), 10);
1087
+ return Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : null;
1088
+ } catch {
1089
+ return null;
1090
+ }
1091
+ }
1092
+
1093
+ async function backupOpenClawConfig(configPath) {
1094
+ await mkdir(getUpgradeAuditDir(), { recursive: true });
1095
+ const backupPath = getOpenClawConfigBackupPath();
1096
+ const configText = await readFile(configPath, "utf8");
1097
+ await writeFile(backupPath, configText, "utf8");
1098
+ return backupPath;
1099
+ }
1100
+
1101
+ async function writeUpgradeAuditFile(data) {
1102
+ await mkdir(getUpgradeAuditDir(), { recursive: true });
1103
+ await writeFile(getUpgradeAuditPath(), `${JSON.stringify(data, null, 2)}\n`, "utf8");
1104
+ }
1105
+
1106
+ async function writeInstallStateFile({ operation, fromVersion, configBackupPath, pluginBackups }) {
1107
+ const installStatePath = getInstallStatePathForPlugin(resolvedPluginId || "openviking");
1108
+ const state = {
1109
+ pluginId: resolvedPluginId || "openviking",
1110
+ generation: getPluginVariantById(resolvedPluginId || "openviking")?.generation || "unknown",
1111
+ requestedRef: PLUGIN_VERSION,
1112
+ releaseId: resolvedPluginReleaseId || "",
1113
+ operation,
1114
+ fromVersion: fromVersion || "",
1115
+ configBackupPath: configBackupPath || "",
1116
+ pluginBackups: pluginBackups || [],
1117
+ installedAt: new Date().toISOString(),
1118
+ repo: REPO,
1119
+ };
1120
+ await writeFile(installStatePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
1121
+ }
1122
+
1123
+ async function moveDirWithFallback(sourceDir, destDir) {
1124
+ try {
1125
+ await rename(sourceDir, destDir);
1126
+ } catch {
1127
+ await cp(sourceDir, destDir, { recursive: true, force: true });
1128
+ await rm(sourceDir, { recursive: true, force: true });
1129
+ }
1130
+ }
1131
+
1132
+ async function rollbackLastUpgradeOperation() {
1133
+ const auditPath = getUpgradeAuditPath();
1134
+ const audit = await readJsonFileIfExists(auditPath);
1135
+ if (!audit) {
1136
+ err(
1137
+ tr(
1138
+ `No rollback audit file found at ${auditPath}.`,
1139
+ `未找到回滚审计文件: ${auditPath}`,
1140
+ ),
1141
+ );
1142
+ process.exit(1);
1143
+ }
1144
+
1145
+ if (audit.rolledBackAt) {
1146
+ warn(
1147
+ tr(
1148
+ `The last recorded upgrade was already rolled back at ${audit.rolledBackAt}.`,
1149
+ `最近一次升级已在 ${audit.rolledBackAt} 回滚。`,
1150
+ ),
1151
+ );
1152
+ }
1153
+
1154
+ const configBackupPath = audit.configBackupPath || getOpenClawConfigBackupPath();
1155
+ if (!existsSync(configBackupPath)) {
1156
+ err(
1157
+ tr(
1158
+ `Rollback config backup is missing: ${configBackupPath}`,
1159
+ `回滚配置备份缺失: ${configBackupPath}`,
1160
+ ),
1161
+ );
1162
+ process.exit(1);
1163
+ }
1164
+
1165
+ const pluginBackups = Array.isArray(audit.pluginBackups) ? audit.pluginBackups : [];
1166
+ if (pluginBackups.length === 0) {
1167
+ err(tr("Rollback audit file contains no plugin backups.", "回滚审计文件中没有插件备份信息。"));
1168
+ process.exit(1);
1169
+ }
1170
+ for (const pluginBackup of pluginBackups) {
1171
+ if (!pluginBackup?.pluginId || !pluginBackup?.backupDir || !existsSync(pluginBackup.backupDir)) {
1172
+ err(
1173
+ tr(
1174
+ `Rollback plugin backup is missing: ${pluginBackup?.backupDir || "<unknown>"}`,
1175
+ `回滚插件备份缺失: ${pluginBackup?.backupDir || "<unknown>"}`,
1176
+ ),
1177
+ );
1178
+ process.exit(1);
1179
+ }
1180
+ }
1181
+
1182
+ info(tr(`Rolling back last upgrade: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`, `开始回滚最近一次升级: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`));
1183
+ await stopOpenClawGatewayForUpgrade();
1184
+
1185
+ const configText = await readFile(configBackupPath, "utf8");
1186
+ await writeFile(getOpenClawConfigPath(), configText, "utf8");
1187
+ info(tr(`Restored openclaw.json from backup: ${configBackupPath}`, `已从备份恢复 openclaw.json: ${configBackupPath}`));
1188
+
1189
+ const extensionsDir = join(OPENCLAW_DIR, "extensions");
1190
+ await mkdir(extensionsDir, { recursive: true });
1191
+ for (const variant of PLUGIN_VARIANTS) {
1192
+ const liveDir = join(extensionsDir, variant.id);
1193
+ if (existsSync(liveDir)) {
1194
+ await rm(liveDir, { recursive: true, force: true });
1195
+ }
1196
+ }
1197
+
1198
+ for (const pluginBackup of pluginBackups) {
1199
+ if (!pluginBackup?.pluginId || !pluginBackup?.backupDir) continue;
1200
+ if (!existsSync(pluginBackup.backupDir)) {
1201
+ err(
1202
+ tr(
1203
+ `Rollback plugin backup is missing: ${pluginBackup.backupDir}`,
1204
+ `回滚插件备份缺失: ${pluginBackup.backupDir}`,
1205
+ ),
1206
+ );
1207
+ process.exit(1);
1208
+ }
1209
+ const destDir = join(extensionsDir, pluginBackup.pluginId);
1210
+ await moveDirWithFallback(pluginBackup.backupDir, destDir);
1211
+ info(tr(`Restored plugin directory: ${destDir}`, `已恢复插件目录: ${destDir}`));
1212
+ }
1213
+
1214
+ audit.rolledBackAt = new Date().toISOString();
1215
+ audit.rollbackConfigPath = configBackupPath;
1216
+ await writeUpgradeAuditFile(audit);
1217
+
1218
+ console.log("");
1219
+ bold(tr("Rollback complete!", "回滚完成!"));
1220
+ console.log("");
1221
+ info(tr(`Rollback audit file: ${auditPath}`, `回滚审计文件: ${auditPath}`));
1222
+ info(tr("Run `openclaw gateway` and `openclaw status` to verify the restored plugin state.", "请运行 `openclaw gateway` 和 `openclaw status` 验证恢复后的插件状态。"));
1223
+ }
1224
+
1225
+ async function prepareUpgradeRuntimeConfig(installedState) {
1226
+ const plugins = installedState.config?.plugins ?? {};
1227
+ const candidateOrder = installedState.detections
1228
+ .map((item) => item.variant)
1229
+ .sort((left, right) => (right.generation === "current" ? 1 : 0) - (left.generation === "current" ? 1 : 0));
1230
+
1231
+ let runtime = null;
1232
+ for (const variant of candidateOrder) {
1233
+ const entryConfig = extractRuntimeConfigFromPluginEntry(plugins.entries?.[variant.id]?.config);
1234
+ if (entryConfig) {
1235
+ runtime = entryConfig;
1236
+ break;
1237
+ }
1238
+ }
1239
+
1240
+ if (!runtime) {
1241
+ runtime = { mode: "local" };
1242
+ }
1243
+
1244
+ if (runtime.mode === "remote") {
1245
+ runtime.baseUrl = runtime.baseUrl || remoteBaseUrl;
1246
+ return runtime;
1247
+ }
1248
+
1249
+ runtime.configPath = runtime.configPath || join(OPENVIKING_DIR, "ov.conf");
1250
+ runtime.port = runtime.port || await readPortFromOvConf(runtime.configPath) || DEFAULT_SERVER_PORT;
1251
+ return runtime;
1252
+ }
1253
+
1254
+ function removePluginConfig(config, variant) {
1255
+ const plugins = config?.plugins;
1256
+ if (!plugins) return false;
1257
+
1258
+ let changed = false;
1259
+
1260
+ if (Array.isArray(plugins.allow)) {
1261
+ const nextAllow = plugins.allow.filter((item) => item !== variant.id);
1262
+ changed = changed || nextAllow.length !== plugins.allow.length;
1263
+ plugins.allow = nextAllow;
1264
+ }
1265
+
1266
+ if (Array.isArray(plugins.load?.paths)) {
1267
+ const nextPaths = plugins.load.paths.filter(
1268
+ (item) => typeof item !== "string" || (!item.includes(variant.id) && !item.includes(variant.dir)),
1269
+ );
1270
+ changed = changed || nextPaths.length !== plugins.load.paths.length;
1271
+ plugins.load.paths = nextPaths;
1272
+ }
1273
+
1274
+ if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
1275
+ delete plugins.entries[variant.id];
1276
+ changed = true;
1277
+ }
1278
+
1279
+ if (plugins.slots?.[variant.slot] === variant.id) {
1280
+ plugins.slots[variant.slot] = variant.slotFallback;
1281
+ changed = true;
1282
+ }
1283
+
1284
+ return changed;
1285
+ }
1286
+
1287
+ async function prunePreviousUpgradeBackups(disabledDir, variant, keepDir) {
1288
+ if (!existsSync(disabledDir)) return;
1289
+
1290
+ const prefix = `${variant.id}-upgrade-backup-`;
1291
+ const keepName = keepDir ? keepDir.split(/[\\/]/).pop() : "";
1292
+ const entries = readdirSync(disabledDir, { withFileTypes: true });
1293
+ for (const entry of entries) {
1294
+ if (!entry.isDirectory()) continue;
1295
+ if (!entry.name.startsWith(prefix)) continue;
1296
+ if (keepName && entry.name === keepName) continue;
1297
+ await rm(join(disabledDir, entry.name), { recursive: true, force: true });
1298
+ }
1299
+ }
1300
+
1301
+ async function backupPluginDirectory(variant) {
1302
+ const pluginDir = join(OPENCLAW_DIR, "extensions", variant.id);
1303
+ if (!existsSync(pluginDir)) return null;
1304
+
1305
+ const disabledDir = join(OPENCLAW_DIR, "disabled-extensions");
1306
+ const backupDir = join(disabledDir, `${variant.id}-upgrade-backup-${Date.now()}`);
1307
+ await mkdir(disabledDir, { recursive: true });
1308
+ try {
1309
+ await rename(pluginDir, backupDir);
1310
+ } catch {
1311
+ await cp(pluginDir, backupDir, { recursive: true, force: true });
1312
+ await rm(pluginDir, { recursive: true, force: true });
1313
+ }
1314
+ info(tr(`Backed up plugin directory: ${backupDir}`, `已备份插件目录: ${backupDir}`));
1315
+ await prunePreviousUpgradeBackups(disabledDir, variant, backupDir);
1316
+ return backupDir;
1317
+ }
1318
+
1319
+ async function stopOpenClawGatewayForUpgrade() {
1320
+ const result = await runCapture("openclaw", ["gateway", "stop"], {
1321
+ env: getOpenClawEnv(),
1322
+ shell: IS_WIN,
1323
+ });
1324
+ if (result.code === 0) {
1325
+ info(tr("Stopped OpenClaw gateway before plugin upgrade", "升级插件前已停止 OpenClaw gateway"));
1326
+ } else {
1327
+ warn(tr("OpenClaw gateway may not be running; continuing", "OpenClaw gateway 可能未在运行,继续执行"));
1328
+ }
1329
+ }
1330
+
1331
+ function shouldClaimTargetSlot(installedState) {
1332
+ const currentOwner = installedState.config?.plugins?.slots?.[resolvedPluginSlot];
1333
+ if (!currentOwner || currentOwner === "none" || currentOwner === "legacy" || currentOwner === resolvedPluginId) {
1334
+ return true;
1335
+ }
1336
+ const currentOwnerVariant = getPluginVariantById(currentOwner);
1337
+ if (currentOwnerVariant && installedState.detections.some((item) => item.variant.id === currentOwnerVariant.id)) {
1338
+ return true;
1339
+ }
1340
+ return false;
1341
+ }
1342
+
1343
+ async function cleanupInstalledPluginConfig(installedState) {
1344
+ if (!installedState.config || !installedState.config.plugins) {
1345
+ warn(tr("openclaw.json has no plugins section; skipped targeted plugin cleanup", "openclaw.json 中没有 plugins 配置,已跳过定向插件清理"));
1346
+ return;
1347
+ }
1348
+
1349
+ const nextConfig = structuredClone(installedState.config);
1350
+ let changed = false;
1351
+ for (const detection of installedState.detections) {
1352
+ changed = removePluginConfig(nextConfig, detection.variant) || changed;
1353
+ }
1354
+
1355
+ if (!changed) {
1356
+ info(tr("No OpenViking plugin config changes were required", "无需修改 OpenViking 插件配置"));
1357
+ return;
1358
+ }
1359
+
1360
+ await writeFile(installedState.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
1361
+ info(tr("Cleaned existing OpenViking plugin config only", "已仅清理 OpenViking 自身插件配置"));
1362
+ }
1363
+
1364
+ async function prepareStrongPluginUpgrade() {
1365
+ const installedState = await detectInstalledPluginState();
1366
+ if (installedState.generation === "none") {
1367
+ err(
1368
+ tr(
1369
+ "Plugin upgrade mode requires an existing OpenViking plugin entry in openclaw.json.",
1370
+ "插件升级模式要求 openclaw.json 中已经存在 OpenViking 插件记录。",
1371
+ ),
1372
+ );
1373
+ process.exit(1);
1374
+ }
1375
+
1376
+ installedUpgradeState = installedState;
1377
+ upgradeRuntimeConfig = await prepareUpgradeRuntimeConfig(installedState);
1378
+ const fromVersion = formatInstalledStateLabel(installedState);
1379
+ const toVersion = formatTargetVersionLabel();
1380
+ selectedMode = upgradeRuntimeConfig.mode;
1381
+ info(
1382
+ tr(
1383
+ `Detected installed OpenViking plugin state: ${installedState.generation}`,
1384
+ `检测到已安装 OpenViking 插件状态: ${installedState.generation}`,
1385
+ ),
1386
+ );
1387
+ if (upgradeRuntimeConfig.mode === "remote") {
1388
+ remoteBaseUrl = upgradeRuntimeConfig.baseUrl || remoteBaseUrl;
1389
+ remoteApiKey = upgradeRuntimeConfig.apiKey || "";
1390
+ remoteAgentId = upgradeRuntimeConfig.agentId || "";
1391
+ } else {
1392
+ selectedServerPort = upgradeRuntimeConfig.port || DEFAULT_SERVER_PORT;
1393
+ }
1394
+ info(tr(`Upgrade runtime mode: ${selectedMode}`, `升级运行模式: ${selectedMode}`));
1395
+
1396
+ info(tr(`Upgrade path: ${fromVersion} -> ${toVersion}`, `升级路径: ${fromVersion} -> ${toVersion}`));
1397
+
1398
+ await stopOpenClawGatewayForUpgrade();
1399
+ const configBackupPath = await backupOpenClawConfig(installedState.configPath);
1400
+ info(tr(`Backed up openclaw.json: ${configBackupPath}`, `已备份 openclaw.json: ${configBackupPath}`));
1401
+ const pluginBackups = [];
1402
+ for (const detection of installedState.detections) {
1403
+ const backupDir = await backupPluginDirectory(detection.variant);
1404
+ if (backupDir) {
1405
+ pluginBackups.push({ pluginId: detection.variant.id, backupDir });
1406
+ }
1407
+ }
1408
+ upgradeAudit = {
1409
+ operation: "upgrade",
1410
+ createdAt: new Date().toISOString(),
1411
+ fromVersion,
1412
+ toVersion,
1413
+ configBackupPath,
1414
+ pluginBackups,
1415
+ runtimeMode: selectedMode,
1416
+ };
1417
+ await writeUpgradeAuditFile(upgradeAudit);
1418
+ await cleanupInstalledPluginConfig(installedState);
1419
+
1420
+ info(
1421
+ tr(
1422
+ "Upgrade will keep the existing OpenViking runtime file and re-apply only the minimum plugin runtime settings.",
1423
+ "升级将保留现有 OpenViking 运行时文件,并只回填最小插件运行配置。",
1424
+ ),
1425
+ );
1426
+ info(tr(`Upgrade audit file: ${getUpgradeAuditPath()}`, `升级审计文件: ${getUpgradeAuditPath()}`));
1427
+ }
1428
+
1429
+ async function downloadPluginFile(destDir, fileName, url, required, index, total) {
1430
+ const maxRetries = 3;
1431
+ const destPath = join(destDir, fileName);
839
1432
 
840
1433
  process.stdout.write(` [${index}/${total}] ${fileName} `);
841
1434
 
@@ -844,7 +1437,8 @@ async function downloadPluginFile(fileName, url, required, index, total) {
844
1437
  const response = await fetch(url);
845
1438
  if (response.ok) {
846
1439
  const buffer = Buffer.from(await response.arrayBuffer());
847
- await writeFile(join(PLUGIN_DEST, fileName), buffer);
1440
+ await mkdir(dirname(destPath), { recursive: true });
1441
+ await writeFile(destPath, buffer);
848
1442
  console.log("✓");
849
1443
  return;
850
1444
  }
@@ -861,7 +1455,8 @@ async function downloadPluginFile(fileName, url, required, index, total) {
861
1455
 
862
1456
  if (fileName === ".gitignore") {
863
1457
  console.log(tr("(retries failed, using minimal .gitignore)", "(重试失败,使用最小 .gitignore)"));
864
- await writeFile(join(PLUGIN_DEST, fileName), "node_modules/\n", "utf8");
1458
+ await mkdir(dirname(destPath), { recursive: true });
1459
+ await writeFile(destPath, "node_modules/\n", "utf8");
865
1460
  return;
866
1461
  }
867
1462
 
@@ -875,12 +1470,12 @@ async function downloadPluginFile(fileName, url, required, index, total) {
875
1470
  process.exit(1);
876
1471
  }
877
1472
 
878
- async function downloadPlugin() {
1473
+ async function downloadPlugin(destDir) {
879
1474
  const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
880
1475
  const pluginDir = resolvedPluginDir;
881
1476
  const total = resolvedFilesRequired.length + resolvedFilesOptional.length;
882
1477
 
883
- await mkdir(PLUGIN_DEST, { recursive: true });
1478
+ await mkdir(destDir, { recursive: true });
884
1479
 
885
1480
  info(tr(`Downloading plugin from ${REPO}@${PLUGIN_VERSION} (${total} files)...`, `正在从 ${REPO}@${PLUGIN_VERSION} 下载插件(共 ${total} 个文件)...`));
886
1481
 
@@ -890,7 +1485,7 @@ async function downloadPlugin() {
890
1485
  if (!name) continue;
891
1486
  i++;
892
1487
  const url = `${ghRaw}/examples/${pluginDir}/${name}`;
893
- await downloadPluginFile(name, url, true, i, total);
1488
+ await downloadPluginFile(destDir, name, url, true, i, total);
894
1489
  }
895
1490
 
896
1491
  // Download optional files
@@ -898,93 +1493,187 @@ async function downloadPlugin() {
898
1493
  if (!name) continue;
899
1494
  i++;
900
1495
  const url = `${ghRaw}/examples/${pluginDir}/${name}`;
901
- await downloadPluginFile(name, url, false, i, total);
1496
+ await downloadPluginFile(destDir, name, url, false, i, total);
902
1497
  }
903
1498
 
904
1499
  // npm install
905
1500
  info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
906
- const npmArgs = resolvedNpmOmitDev
907
- ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
908
- : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
909
- await run("npm", npmArgs, { cwd: PLUGIN_DEST, silent: false });
1501
+ const npmArgs = resolvedNpmOmitDev
1502
+ ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1503
+ : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1504
+ await run("npm", npmArgs, { cwd: destDir, silent: false });
910
1505
  info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
911
1506
  }
912
1507
 
913
- async function deployLocalPlugin(localPluginDir) {
914
- await rm(PLUGIN_DEST, { recursive: true, force: true });
915
- await mkdir(PLUGIN_DEST, { recursive: true });
916
- await cp(localPluginDir, PLUGIN_DEST, {
917
- recursive: true,
918
- force: true,
919
- filter: (sourcePath) => {
920
- const rel = relative(localPluginDir, sourcePath);
921
- if (!rel) return true;
922
- const firstSegment = rel.split(/[\\/]/)[0];
1508
+ async function deployLocalPlugin(localPluginDir, destDir) {
1509
+ await rm(destDir, { recursive: true, force: true });
1510
+ await mkdir(destDir, { recursive: true });
1511
+ await cp(localPluginDir, destDir, {
1512
+ recursive: true,
1513
+ force: true,
1514
+ filter: (sourcePath) => {
1515
+ const rel = relative(localPluginDir, sourcePath);
1516
+ if (!rel) return true;
1517
+ const firstSegment = rel.split(/[\\/]/)[0];
923
1518
  return firstSegment !== "node_modules" && firstSegment !== ".git";
924
1519
  },
925
1520
  });
926
1521
  }
927
1522
 
928
- async function configureOpenClawPlugin() {
1523
+ async function installPluginDependencies(destDir) {
1524
+ info(tr("Installing plugin npm dependencies...", "姝e湪瀹夎鎻掍欢 npm 渚濊禆..."));
1525
+ const npmArgs = resolvedNpmOmitDev
1526
+ ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1527
+ : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1528
+ await run("npm", npmArgs, { cwd: destDir, silent: false });
1529
+ return info(`Plugin prepared: ${destDir}`);
1530
+ }
1531
+
1532
+ async function createPluginStagingDir() {
1533
+ const pluginId = resolvedPluginId || "openviking";
1534
+ const extensionsDir = join(OPENCLAW_DIR, "extensions");
1535
+ const stagingDir = join(extensionsDir, `.${pluginId}.staging-${process.pid}-${Date.now()}`);
1536
+ await mkdir(extensionsDir, { recursive: true });
1537
+ await rm(stagingDir, { recursive: true, force: true });
1538
+ await mkdir(stagingDir, { recursive: true });
1539
+ return stagingDir;
1540
+ }
1541
+
1542
+ async function finalizePluginDeployment(stagingDir) {
1543
+ await rm(PLUGIN_DEST, { recursive: true, force: true });
1544
+ try {
1545
+ await rename(stagingDir, PLUGIN_DEST);
1546
+ } catch {
1547
+ await cp(stagingDir, PLUGIN_DEST, { recursive: true, force: true });
1548
+ await rm(stagingDir, { recursive: true, force: true });
1549
+ }
1550
+ return info(`Plugin deployed: ${PLUGIN_DEST}`);
1551
+ info(tr(`Plugin prepared: ${destDir}`, `鎻掍欢宸插噯澶? ${destDir}`));
1552
+ }
1553
+
1554
+ async function deployPluginFromRemote() {
1555
+ const stagingDir = await createPluginStagingDir();
1556
+ try {
1557
+ await downloadPlugin(stagingDir);
1558
+ await finalizePluginDeployment(stagingDir);
1559
+ } catch (error) {
1560
+ await rm(stagingDir, { recursive: true, force: true });
1561
+ throw error;
1562
+ }
1563
+ }
1564
+
1565
+ async function deployPluginFromLocal(localPluginDir) {
1566
+ const stagingDir = await createPluginStagingDir();
1567
+ try {
1568
+ await deployLocalPlugin(localPluginDir, stagingDir);
1569
+ await installPluginDependencies(stagingDir);
1570
+ await finalizePluginDeployment(stagingDir);
1571
+ } catch (error) {
1572
+ await rm(stagingDir, { recursive: true, force: true });
1573
+ throw error;
1574
+ }
1575
+ }
1576
+
1577
+ async function configureOpenClawPlugin({
1578
+ preserveExistingConfig = false,
1579
+ runtimeConfig = null,
1580
+ skipGatewayMode = false,
1581
+ claimSlot = true,
1582
+ } = {}) {
929
1583
  info(tr("Configuring OpenClaw plugin...", "正在配置 OpenClaw 插件..."));
930
1584
 
931
1585
  const pluginId = resolvedPluginId;
932
1586
  const pluginSlot = resolvedPluginSlot;
933
1587
 
934
- const ocEnv = { ...process.env };
935
- if (OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
936
- ocEnv.OPENCLAW_STATE_DIR = OPENCLAW_DIR;
937
- }
1588
+ const ocEnv = getOpenClawEnv();
1589
+
1590
+ const oc = async (args) => {
1591
+ const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
1592
+ if (result.code !== 0) {
1593
+ const detail = result.err || result.out;
1594
+ throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
1595
+ }
1596
+ return result;
1597
+ };
938
1598
 
939
- const oc = async (args) => {
940
- const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
941
- if (result.code !== 0) {
942
- const detail = result.err || result.out;
943
- throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
944
- }
945
- return result;
946
- };
947
-
948
1599
  // Enable plugin (files already deployed to extensions dir by deployPlugin)
949
1600
  await oc(["plugins", "enable", pluginId]);
950
- await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
951
-
952
- // Set gateway mode
953
- await oc(["config", "set", "gateway.mode", "local"]);
1601
+ if (claimSlot) {
1602
+ await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
1603
+ } else {
1604
+ warn(
1605
+ tr(
1606
+ `Skipped claiming plugins.slots.${pluginSlot}; it is currently owned by another plugin.`,
1607
+ `已跳过设置 plugins.slots.${pluginSlot},当前该 slot 由其他插件占用。`,
1608
+ ),
1609
+ );
1610
+ }
1611
+
1612
+ if (preserveExistingConfig) {
1613
+ info(
1614
+ tr(
1615
+ `Preserved existing plugin runtime config for ${pluginId}`,
1616
+ `宸蹭繚鐣?${pluginId} 鐨勭幇鏈夋彃浠惰繍琛岄厤缃?`,
1617
+ ),
1618
+ );
1619
+ return;
1620
+ }
1621
+
1622
+ const effectiveRuntimeConfig = runtimeConfig || (
1623
+ selectedMode === "remote"
1624
+ ? { mode: "remote", baseUrl: remoteBaseUrl, apiKey: remoteApiKey, agentId: remoteAgentId }
1625
+ : { mode: "local", configPath: join(OPENVIKING_DIR, "ov.conf"), port: selectedServerPort }
1626
+ );
1627
+
1628
+ if (!skipGatewayMode) {
1629
+ await oc(["config", "set", "gateway.mode", effectiveRuntimeConfig.mode === "remote" ? "remote" : "local"]);
1630
+ }
1631
+
1632
+ // Set plugin config for the selected mode
1633
+ if (effectiveRuntimeConfig.mode === "local") {
1634
+ const ovConfPath = effectiveRuntimeConfig.configPath || join(OPENVIKING_DIR, "ov.conf");
1635
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "local"]);
1636
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.configPath`, ovConfPath]);
1637
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.port`, String(effectiveRuntimeConfig.port || DEFAULT_SERVER_PORT)]);
1638
+ } else {
1639
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "remote"]);
1640
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, effectiveRuntimeConfig.baseUrl || remoteBaseUrl]);
1641
+ if (effectiveRuntimeConfig.apiKey) {
1642
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, effectiveRuntimeConfig.apiKey]);
1643
+ }
1644
+ if (effectiveRuntimeConfig.agentId) {
1645
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.agentId`, effectiveRuntimeConfig.agentId]);
1646
+ }
1647
+ }
954
1648
 
955
- // Set plugin config for the selected mode
956
- if (selectedMode === "local") {
957
- const ovConfPath = join(OPENVIKING_DIR, "ov.conf");
958
- await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "local"]);
959
- await oc(["config", "set", `plugins.entries.${pluginId}.config.configPath`, ovConfPath]);
960
- await oc(["config", "set", `plugins.entries.${pluginId}.config.port`, String(selectedServerPort)]);
961
- } else {
962
- await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "remote"]);
963
- await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, remoteBaseUrl]);
964
- if (remoteApiKey) {
965
- await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, remoteApiKey]);
966
- }
967
- if (remoteAgentId) {
968
- await oc(["config", "set", `plugins.entries.${pluginId}.config.agentId`, remoteAgentId]);
969
- }
1649
+ // Legacy (memory) plugins need explicit targetUri/autoRecall/autoCapture (new version has defaults in config.ts)
1650
+ if (resolvedPluginKind === "memory") {
1651
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.targetUri`, "viking://user/memories"]);
1652
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.autoRecall`, "true", "--json"]);
1653
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.autoCapture`, "true", "--json"]);
970
1654
  }
971
1655
 
972
1656
  info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
973
1657
  }
974
1658
 
1659
+ async function discoverOpenvikingPython(failedPy) {
1660
+ const candidates = IS_WIN
1661
+ ? ["python3", "python", "py -3"]
1662
+ : ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
1663
+ for (const candidate of candidates) {
1664
+ if (candidate === failedPy) continue;
1665
+ const resolved = await resolveAbsoluteCommand(candidate);
1666
+ if (!resolved || resolved === candidate || resolved === failedPy) continue;
1667
+ const check = await runCapture(resolved, ["-c", "import openviking"], { shell: false });
1668
+ if (check.code === 0) return resolved;
1669
+ }
1670
+ return "";
1671
+ }
1672
+
975
1673
  async function resolvePythonPath() {
976
1674
  if (openvikingPythonPath) return openvikingPythonPath;
977
1675
  const python = await checkPython();
978
- const py = python.cmd;
979
- if (!py) return "";
980
-
981
- if (IS_WIN) {
982
- const result = await runCapture("where", [py], { shell: true });
983
- return result.out.split(/\r?\n/)[0]?.trim() || py;
984
- }
985
-
986
- const result = await runCapture("which", [py], { shell: false });
987
- return result.out.trim() || py;
1676
+ return python.cmd || "";
988
1677
  }
989
1678
 
990
1679
  async function writeOpenvikingEnv({ includePython }) {
@@ -1001,6 +1690,37 @@ async function writeOpenvikingEnv({ includePython }) {
1001
1690
  ),
1002
1691
  );
1003
1692
  }
1693
+
1694
+ // Verify the resolved Python can actually import openviking
1695
+ if (pythonPath) {
1696
+ const verify = await runCapture(pythonPath, ["-c", "import openviking"], { shell: false });
1697
+ if (verify.code !== 0) {
1698
+ warn(
1699
+ tr(
1700
+ `Resolved Python (${pythonPath}) cannot import openviking. The pip install target may differ from the runtime python3.`,
1701
+ `解析到的 Python(${pythonPath})无法 import openviking。pip 安装目标可能与运行时的 python3 不一致。`,
1702
+ ),
1703
+ );
1704
+ // Try to discover the correct Python via pip show
1705
+ const corrected = await discoverOpenvikingPython(pythonPath);
1706
+ if (corrected) {
1707
+ info(
1708
+ tr(
1709
+ `Auto-corrected OPENVIKING_PYTHON to ${corrected}`,
1710
+ `已自动修正 OPENVIKING_PYTHON 为 ${corrected}`,
1711
+ ),
1712
+ );
1713
+ pythonPath = corrected;
1714
+ } else {
1715
+ warn(
1716
+ tr(
1717
+ `Could not auto-detect the correct Python. Edit OPENVIKING_PYTHON in the env file manually.`,
1718
+ `无法自动检测正确的 Python。请手动修改 env 文件中的 OPENVIKING_PYTHON。`,
1719
+ ),
1720
+ );
1721
+ }
1722
+ }
1723
+ }
1004
1724
  }
1005
1725
 
1006
1726
  // Remote mode + default state dir + no python line → nothing to persist
@@ -1044,18 +1764,59 @@ async function writeOpenvikingEnv({ includePython }) {
1044
1764
  return { shellPath: envPath };
1045
1765
  }
1046
1766
 
1047
- function wrapCommand(command, envFiles) {
1048
- if (!envFiles) return command;
1049
- if (IS_WIN) return `call "${envFiles.shellPath}" && ${command}`;
1050
- return `source '${envFiles.shellPath.replace(/'/g, "'\"'\"'")}' && ${command}`;
1051
- }
1052
-
1053
- async function main() {
1767
+ function wrapCommand(command, envFiles) {
1768
+ if (!envFiles) return command;
1769
+ if (IS_WIN) return `call "${envFiles.shellPath}" && ${command}`;
1770
+ return `source '${envFiles.shellPath.replace(/'/g, "'\"'\"'")}' && ${command}`;
1771
+ }
1772
+
1773
+ function getExistingEnvFiles() {
1774
+ if (IS_WIN) {
1775
+ const batPath = join(OPENCLAW_DIR, "openviking.env.bat");
1776
+ const ps1Path = join(OPENCLAW_DIR, "openviking.env.ps1");
1777
+ if (existsSync(batPath)) {
1778
+ return { shellPath: batPath, powershellPath: existsSync(ps1Path) ? ps1Path : undefined };
1779
+ }
1780
+ if (existsSync(ps1Path)) {
1781
+ return { shellPath: ps1Path, powershellPath: ps1Path };
1782
+ }
1783
+ return null;
1784
+ }
1785
+
1786
+ const envPath = join(OPENCLAW_DIR, "openviking.env");
1787
+ return existsSync(envPath) ? { shellPath: envPath } : null;
1788
+ }
1789
+
1790
+ function ensureExistingPluginForUpgrade() {
1791
+ if (!existsSync(PLUGIN_DEST)) {
1792
+ err(
1793
+ tr(
1794
+ `Plugin upgrade mode expects an existing plugin at ${PLUGIN_DEST}. Run the full installer first if this is a fresh install.`,
1795
+ `鎻掍欢鍗囩骇妯″紡瑕佹眰鐜版湁鎻掍欢宸插畨瑁呭湪 ${PLUGIN_DEST}銆傚鏄娆″畨瑁咃紝璇疯繍琛屽叏閲忓畨瑁呮祦绋嬨€?`,
1796
+ ),
1797
+ );
1798
+ process.exit(1);
1799
+ }
1800
+ }
1801
+
1802
+ async function main() {
1054
1803
  console.log("");
1055
1804
  bold(tr("🦣 OpenClaw + OpenViking Installer", "🦣 OpenClaw + OpenViking 一键安装"));
1056
1805
  console.log("");
1057
1806
 
1058
- await selectWorkdir();
1807
+ await selectWorkdir();
1808
+ if (rollbackLastUpgrade) {
1809
+ info(tr("Mode: rollback last plugin upgrade", "模式: 回滚最近一次插件升级"));
1810
+ if (PLUGIN_VERSION !== "main") {
1811
+ warn("--plugin-version is ignored in --rollback mode.");
1812
+ }
1813
+ if (openvikingVersion) {
1814
+ warn("--openviking-version is ignored in --rollback mode.");
1815
+ }
1816
+ await rollbackLastUpgradeOperation();
1817
+ return;
1818
+ }
1819
+ validateRequestedPluginVersion();
1059
1820
  info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
1060
1821
  info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
1061
1822
  info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
@@ -1063,18 +1824,31 @@ async function main() {
1063
1824
  info(tr(`OpenViking version: ${openvikingVersion}`, `OpenViking 版本: ${openvikingVersion}`));
1064
1825
  }
1065
1826
 
1066
- await selectMode();
1827
+ if (upgradePluginOnly) {
1828
+ selectedMode = "local";
1829
+ info("Mode: plugin upgrade only (backup old plugin, clean only OpenViking plugin config, keep ov.conf)");
1830
+ if (openvikingVersion) {
1831
+ warn("--openviking-version is ignored in --upgrade-plugin mode.");
1832
+ }
1833
+ } else {
1834
+ await selectMode();
1835
+ }
1067
1836
  info(tr(`Mode: ${selectedMode}`, `模式: ${selectedMode}`));
1068
1837
 
1069
- if (selectedMode === "local") {
1070
- await validateEnvironment();
1071
- await checkOpenClaw();
1838
+ if (upgradePluginOnly) {
1839
+ await checkOpenClaw();
1840
+ await resolvePluginConfig();
1841
+ await checkOpenClawCompatibility();
1842
+ await prepareStrongPluginUpgrade();
1843
+ } else if (selectedMode === "local") {
1844
+ await validateEnvironment();
1845
+ await checkOpenClaw();
1072
1846
  // Resolve plugin config after OpenClaw is available (for version detection)
1073
1847
  await resolvePluginConfig();
1074
1848
  await checkOpenClawCompatibility();
1075
1849
  checkRequestedOpenVikingCompatibility();
1076
1850
  await installOpenViking();
1077
- await configureOvConf();
1851
+ await configureOvConf();
1078
1852
  } else {
1079
1853
  await checkOpenClaw();
1080
1854
  await resolvePluginConfig();
@@ -1088,22 +1862,41 @@ async function main() {
1088
1862
  pluginPath = localPluginDir;
1089
1863
  PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId || "openviking");
1090
1864
  info(tr(`Using local plugin from repo: ${pluginPath}`, `使用仓库内插件: ${pluginPath}`));
1091
- await deployLocalPlugin(pluginPath);
1865
+ await deployPluginFromLocal(pluginPath);
1092
1866
  info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
1093
- const npmArgs = resolvedNpmOmitDev
1094
- ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1095
- : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1096
- await run("npm", npmArgs, { cwd: PLUGIN_DEST, silent: false });
1097
- pluginPath = PLUGIN_DEST;
1867
+ pluginPath = PLUGIN_DEST;
1098
1868
  } else {
1099
- await downloadPlugin();
1869
+ await deployPluginFromRemote();
1100
1870
  pluginPath = PLUGIN_DEST;
1101
1871
  }
1102
1872
 
1103
- await configureOpenClawPlugin();
1104
- const envFiles = await writeOpenvikingEnv({
1105
- includePython: selectedMode === "local",
1106
- });
1873
+ await configureOpenClawPlugin(
1874
+ upgradePluginOnly
1875
+ ? {
1876
+ runtimeConfig: upgradeRuntimeConfig,
1877
+ skipGatewayMode: true,
1878
+ claimSlot: installedUpgradeState ? shouldClaimTargetSlot(installedUpgradeState) : true,
1879
+ }
1880
+ : { preserveExistingConfig: false },
1881
+ );
1882
+ await writeInstallStateFile({
1883
+ operation: upgradePluginOnly ? "upgrade" : "install",
1884
+ fromVersion: upgradeAudit?.fromVersion || "",
1885
+ configBackupPath: upgradeAudit?.configBackupPath || "",
1886
+ pluginBackups: upgradeAudit?.pluginBackups || [],
1887
+ });
1888
+ if (upgradeAudit) {
1889
+ upgradeAudit.completedAt = new Date().toISOString();
1890
+ await writeUpgradeAuditFile(upgradeAudit);
1891
+ }
1892
+ let envFiles = getExistingEnvFiles();
1893
+ if (!upgradePluginOnly) {
1894
+ envFiles = await writeOpenvikingEnv({
1895
+ includePython: selectedMode === "local",
1896
+ });
1897
+ } else if (!envFiles && OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
1898
+ envFiles = await writeOpenvikingEnv({ includePython: false });
1899
+ }
1107
1900
 
1108
1901
  console.log("");
1109
1902
  bold("═══════════════════════════════════════════════════════════");
@@ -1111,7 +1904,17 @@ async function main() {
1111
1904
  bold("═══════════════════════════════════════════════════════════");
1112
1905
  console.log("");
1113
1906
 
1114
- if (selectedMode === "local") {
1907
+ if (upgradeAudit) {
1908
+ info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
1909
+ info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
1910
+ for (const pluginBackup of upgradeAudit.pluginBackups || []) {
1911
+ info(tr(`Rollback plugin backup: ${pluginBackup.backupDir}`, `回滚插件备份: ${pluginBackup.backupDir}`));
1912
+ }
1913
+ info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
1914
+ console.log("");
1915
+ }
1916
+
1917
+ if (selectedMode === "local") {
1115
1918
  info(tr("Run these commands to start OpenClaw + OpenViking:", "请按以下命令启动 OpenClaw + OpenViking:"));
1116
1919
  } else {
1117
1920
  info(tr("Run these commands to start OpenClaw:", "请按以下命令启动 OpenClaw:"));