openclaw-openviking-setup-helper 0.2.9-dev.1 → 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 +853 -142
  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,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import { spawn } from "node:child_process";
26
- import { cp, mkdir, rm, writeFile } from "node:fs/promises";
26
+ import { cp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
27
27
  import { existsSync, readdirSync } from "node:fs";
28
28
  import { dirname, join, relative } from "node:path";
29
29
  import { createInterface } from "node:readline";
@@ -61,14 +61,19 @@ 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", "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
- };
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 = "";
@@ -76,23 +81,29 @@ let resolvedPluginId = "";
76
81
  let resolvedPluginKind = "";
77
82
  let resolvedPluginSlot = "";
78
83
  let resolvedFilesRequired = [];
79
- let resolvedFilesOptional = [];
80
- let resolvedNpmOmitDev = true;
81
- let resolvedMinOpenclawVersion = "";
82
- let resolvedMinOpenvikingVersion = "";
83
-
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;
84
+ let resolvedFilesOptional = [];
85
+ let resolvedNpmOmitDev = true;
86
+ let resolvedMinOpenclawVersion = "";
87
+ let resolvedMinOpenvikingVersion = "";
88
+ let resolvedPluginReleaseId = "";
89
+
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;
@@ -477,12 +506,25 @@ function versionGte(v1, v2) {
477
506
  return a3 >= b3;
478
507
  }
479
508
 
480
- function isSemverLike(value) {
481
- return /^v?\d+(\.\d+){1,2}$/.test(value);
482
- }
483
-
484
- // Detect OpenClaw version
485
- async function detectOpenClawVersion() {
509
+ function isSemverLike(value) {
510
+ return /^v?\d+(\.\d+){1,2}$/.test(value);
511
+ }
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() {
486
528
  try {
487
529
  const result = await runCapture("openclaw", ["--version"], { shell: IS_WIN });
488
530
  if (result.code === 0 && result.out) {
@@ -556,28 +598,31 @@ async function resolvePluginConfig() {
556
598
  }
557
599
  }
558
600
 
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 {
601
+ resolvedPluginDir = pluginDir;
602
+ resolvedPluginReleaseId = "";
603
+
604
+ if (manifestData) {
605
+ resolvedPluginId = manifestData.plugin?.id || "";
606
+ resolvedPluginKind = manifestData.plugin?.kind || "";
607
+ resolvedPluginSlot = manifestData.plugin?.slot || "";
608
+ resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
609
+ resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
610
+ resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
611
+ resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
612
+ resolvedFilesRequired = manifestData.files?.required || [];
613
+ resolvedFilesOptional = manifestData.files?.optional || [];
614
+ } else {
571
615
  // No manifest — determine plugin identity by package.json name
572
616
  let fallbackKey = pluginDir === "openclaw-memory-plugin" ? "legacy" : "current";
573
617
  let compatVer = "";
574
618
 
575
619
  const pkgJson = await tryFetch(`${ghRaw}/examples/${pluginDir}/package.json`);
576
620
  if (pkgJson) {
577
- try {
578
- const pkg = JSON.parse(pkgJson);
579
- const pkgName = pkg.name || "";
580
- if (pkgName && pkgName !== "@openclaw/openviking") {
621
+ try {
622
+ const pkg = JSON.parse(pkgJson);
623
+ const pkgName = pkg.name || "";
624
+ resolvedPluginReleaseId = pkg.version || "";
625
+ if (pkgName && pkgName !== "@openclaw/openviking") {
581
626
  fallbackKey = "legacy";
582
627
  info(tr(`Detected legacy plugin by package name: ${pkgName}`, `通过 package.json 名称检测到旧版插件: ${pkgName}`));
583
628
  } else if (pkgName) {
@@ -639,9 +684,9 @@ async function checkOpenClawCompatibility() {
639
684
  }
640
685
 
641
686
  // 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
- }
687
+ if (PLUGIN_VERSION !== "main" && isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
688
+ return;
689
+ }
645
690
 
646
691
  // Check compatibility
647
692
  if (!versionGte(ocVersion, resolvedMinOpenclawVersion)) {
@@ -656,7 +701,7 @@ async function checkOpenClawCompatibility() {
656
701
  console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
657
702
  console.log("");
658
703
  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" : ""}`);
704
+ console.log(` node install.js --plugin-version=v0.2.6${langZh ? " --zh" : ""}`);
660
705
  console.log("");
661
706
  process.exit(1);
662
707
  }
@@ -883,8 +928,507 @@ async function configureOvConf() {
883
928
  info(tr(`Config generated: ${configPath}`, `已生成配置: ${configPath}`));
884
929
  }
885
930
 
886
- async function downloadPluginFile(fileName, url, required, index, total) {
887
- 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);
888
1432
 
889
1433
  process.stdout.write(` [${index}/${total}] ${fileName} `);
890
1434
 
@@ -893,7 +1437,8 @@ async function downloadPluginFile(fileName, url, required, index, total) {
893
1437
  const response = await fetch(url);
894
1438
  if (response.ok) {
895
1439
  const buffer = Buffer.from(await response.arrayBuffer());
896
- await writeFile(join(PLUGIN_DEST, fileName), buffer);
1440
+ await mkdir(dirname(destPath), { recursive: true });
1441
+ await writeFile(destPath, buffer);
897
1442
  console.log("✓");
898
1443
  return;
899
1444
  }
@@ -910,7 +1455,8 @@ async function downloadPluginFile(fileName, url, required, index, total) {
910
1455
 
911
1456
  if (fileName === ".gitignore") {
912
1457
  console.log(tr("(retries failed, using minimal .gitignore)", "(重试失败,使用最小 .gitignore)"));
913
- 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");
914
1460
  return;
915
1461
  }
916
1462
 
@@ -924,12 +1470,12 @@ async function downloadPluginFile(fileName, url, required, index, total) {
924
1470
  process.exit(1);
925
1471
  }
926
1472
 
927
- async function downloadPlugin() {
1473
+ async function downloadPlugin(destDir) {
928
1474
  const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
929
1475
  const pluginDir = resolvedPluginDir;
930
1476
  const total = resolvedFilesRequired.length + resolvedFilesOptional.length;
931
1477
 
932
- await mkdir(PLUGIN_DEST, { recursive: true });
1478
+ await mkdir(destDir, { recursive: true });
933
1479
 
934
1480
  info(tr(`Downloading plugin from ${REPO}@${PLUGIN_VERSION} (${total} files)...`, `正在从 ${REPO}@${PLUGIN_VERSION} 下载插件(共 ${total} 个文件)...`));
935
1481
 
@@ -939,7 +1485,7 @@ async function downloadPlugin() {
939
1485
  if (!name) continue;
940
1486
  i++;
941
1487
  const url = `${ghRaw}/examples/${pluginDir}/${name}`;
942
- await downloadPluginFile(name, url, true, i, total);
1488
+ await downloadPluginFile(destDir, name, url, true, i, total);
943
1489
  }
944
1490
 
945
1491
  // Download optional files
@@ -947,7 +1493,7 @@ async function downloadPlugin() {
947
1493
  if (!name) continue;
948
1494
  i++;
949
1495
  const url = `${ghRaw}/examples/${pluginDir}/${name}`;
950
- await downloadPluginFile(name, url, false, i, total);
1496
+ await downloadPluginFile(destDir, name, url, false, i, total);
951
1497
  }
952
1498
 
953
1499
  // npm install
@@ -955,35 +1501,91 @@ async function downloadPlugin() {
955
1501
  const npmArgs = resolvedNpmOmitDev
956
1502
  ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
957
1503
  : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
958
- await run("npm", npmArgs, { cwd: PLUGIN_DEST, silent: false });
1504
+ await run("npm", npmArgs, { cwd: destDir, silent: false });
959
1505
  info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
960
1506
  }
961
1507
 
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, {
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, {
966
1512
  recursive: true,
967
1513
  force: true,
968
1514
  filter: (sourcePath) => {
969
1515
  const rel = relative(localPluginDir, sourcePath);
970
1516
  if (!rel) return true;
971
1517
  const firstSegment = rel.split(/[\\/]/)[0];
972
- return firstSegment !== "node_modules" && firstSegment !== ".git";
973
- },
974
- });
975
- }
976
-
977
- async function configureOpenClawPlugin() {
1518
+ return firstSegment !== "node_modules" && firstSegment !== ".git";
1519
+ },
1520
+ });
1521
+ }
1522
+
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
+ } = {}) {
978
1583
  info(tr("Configuring OpenClaw plugin...", "正在配置 OpenClaw 插件..."));
979
1584
 
980
1585
  const pluginId = resolvedPluginId;
981
1586
  const pluginSlot = resolvedPluginSlot;
982
1587
 
983
- const ocEnv = { ...process.env };
984
- if (OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
985
- ocEnv.OPENCLAW_STATE_DIR = OPENCLAW_DIR;
986
- }
1588
+ const ocEnv = getOpenClawEnv();
987
1589
 
988
1590
  const oc = async (args) => {
989
1591
  const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
@@ -994,29 +1596,55 @@ async function configureOpenClawPlugin() {
994
1596
  return result;
995
1597
  };
996
1598
 
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]);
1000
-
1001
- // Set gateway mode
1002
- await oc(["config", "set", "gateway.mode", "local"]);
1003
-
1004
- // Set plugin config for the selected mode
1005
- if (selectedMode === "local") {
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)]);
1010
- } else {
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
- }
1019
- }
1599
+ // Enable plugin (files already deployed to extensions dir by deployPlugin)
1600
+ await oc(["plugins", "enable", pluginId]);
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
+ }
1020
1648
 
1021
1649
  // Legacy (memory) plugins need explicit targetUri/autoRecall/autoCapture (new version has defaults in config.ts)
1022
1650
  if (resolvedPluginKind === "memory") {
@@ -1136,18 +1764,59 @@ async function writeOpenvikingEnv({ includePython }) {
1136
1764
  return { shellPath: envPath };
1137
1765
  }
1138
1766
 
1139
- function wrapCommand(command, envFiles) {
1140
- if (!envFiles) return command;
1141
- if (IS_WIN) return `call "${envFiles.shellPath}" && ${command}`;
1142
- return `source '${envFiles.shellPath.replace(/'/g, "'\"'\"'")}' && ${command}`;
1143
- }
1144
-
1145
- 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() {
1146
1803
  console.log("");
1147
1804
  bold(tr("🦣 OpenClaw + OpenViking Installer", "🦣 OpenClaw + OpenViking 一键安装"));
1148
1805
  console.log("");
1149
1806
 
1150
- 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();
1151
1820
  info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
1152
1821
  info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
1153
1822
  info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
@@ -1155,18 +1824,31 @@ async function main() {
1155
1824
  info(tr(`OpenViking version: ${openvikingVersion}`, `OpenViking 版本: ${openvikingVersion}`));
1156
1825
  }
1157
1826
 
1158
- 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
+ }
1159
1836
  info(tr(`Mode: ${selectedMode}`, `模式: ${selectedMode}`));
1160
1837
 
1161
- if (selectedMode === "local") {
1162
- await validateEnvironment();
1163
- await checkOpenClaw();
1164
- // Resolve plugin config after OpenClaw is available (for version detection)
1165
- await resolvePluginConfig();
1166
- await checkOpenClawCompatibility();
1167
- checkRequestedOpenVikingCompatibility();
1168
- await installOpenViking();
1169
- await configureOvConf();
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();
1846
+ // Resolve plugin config after OpenClaw is available (for version detection)
1847
+ await resolvePluginConfig();
1848
+ await checkOpenClawCompatibility();
1849
+ checkRequestedOpenVikingCompatibility();
1850
+ await installOpenViking();
1851
+ await configureOvConf();
1170
1852
  } else {
1171
1853
  await checkOpenClaw();
1172
1854
  await resolvePluginConfig();
@@ -1180,22 +1862,41 @@ async function main() {
1180
1862
  pluginPath = localPluginDir;
1181
1863
  PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId || "openviking");
1182
1864
  info(tr(`Using local plugin from repo: ${pluginPath}`, `使用仓库内插件: ${pluginPath}`));
1183
- await deployLocalPlugin(pluginPath);
1865
+ await deployPluginFromLocal(pluginPath);
1184
1866
  info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
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
1867
  pluginPath = PLUGIN_DEST;
1190
1868
  } else {
1191
- await downloadPlugin();
1869
+ await deployPluginFromRemote();
1192
1870
  pluginPath = PLUGIN_DEST;
1193
1871
  }
1194
1872
 
1195
- await configureOpenClawPlugin();
1196
- const envFiles = await writeOpenvikingEnv({
1197
- includePython: selectedMode === "local",
1198
- });
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
+ }
1199
1900
 
1200
1901
  console.log("");
1201
1902
  bold("═══════════════════════════════════════════════════════════");
@@ -1203,7 +1904,17 @@ async function main() {
1203
1904
  bold("═══════════════════════════════════════════════════════════");
1204
1905
  console.log("");
1205
1906
 
1206
- 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") {
1207
1918
  info(tr("Run these commands to start OpenClaw + OpenViking:", "请按以下命令启动 OpenClaw + OpenViking:"));
1208
1919
  } else {
1209
1920
  info(tr("Run these commands to start OpenClaw:", "请按以下命令启动 OpenClaw:"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-openviking-setup-helper",
3
- "version": "0.2.9-dev.1",
3
+ "version": "0.2.9-dev.2",
4
4
  "description": "Setup helper for installing OpenViking memory plugin into OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {