openclaw-openviking-setup-helper 0.3.0-beta.9 → 0.3.0

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 +682 -93
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -3,18 +3,18 @@
3
3
  * OpenClaw OpenViking plugin installer (remote OpenViking server — does not install Python/OpenViking server).
4
4
  *
5
5
  * One-liner (after npm publish; use package name + bin name):
6
- * npx -p openclaw-openviking-setup-helper ov-install [ -y ] [ --zh ] [ --workdir PATH ]
6
+ * npx -p openclaw-openviking-setup-helper ov-install [ --base-url URL ] [ --api-key KEY ] [ --zh ] [ --workdir PATH ]
7
7
  * Or install globally then run:
8
8
  * npm i -g openclaw-openviking-setup-helper
9
9
  * ov-install
10
10
  * openclaw-openviking-install
11
11
  *
12
12
  * Direct run:
13
- * node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ] [ --upgrade-plugin ]
13
+ * node install.js [ --base-url URL ] [ --api-key KEY ] [ --zh ] [ --workdir PATH ] [ --upgrade-plugin ]
14
14
  * [ --plugin-version=TAG ]
15
15
  *
16
16
  * Environment variables:
17
- * REPO, PLUGIN_VERSION (or BRANCH), OPENVIKING_INSTALL_YES, SKIP_OPENCLAW
17
+ * REPO, PLUGIN_VERSION (or BRANCH), OPENVIKING_BASE_URL, OPENVIKING_API_KEY, SKIP_OPENCLAW
18
18
  * NPM_REGISTRY
19
19
  */
20
20
 
@@ -33,6 +33,8 @@ const pluginVersionEnv = (process.env.PLUGIN_VERSION || process.env.BRANCH || ""
33
33
  let PLUGIN_VERSION = pluginVersionEnv;
34
34
  let pluginVersionExplicit = Boolean(pluginVersionEnv);
35
35
  const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmmirror.com";
36
+ const DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION = "2026.5.3";
37
+ const OPENCLAW_SHORT_VERSION_YEAR = 2026;
36
38
 
37
39
  const IS_WIN = process.platform === "win32";
38
40
  const HOME = process.env.HOME || process.env.USERPROFILE || "";
@@ -47,8 +49,9 @@ const FALLBACK_LEGACY = {
47
49
  id: "memory-openviking",
48
50
  kind: "memory",
49
51
  slot: "memory",
50
- required: ["index.ts", "config.ts", "openclaw.plugin.json", "package.json"],
51
- optional: ["package-lock.json", ".gitignore"],
52
+ minOpenclawVersion: "2026.3.7",
53
+ required: ["index.ts", "config.ts", "client.ts", "openclaw.plugin.json", "package.json"],
54
+ optional: ["package-lock.json", ".gitignore", "memory-ranking.ts", "text-utils.ts", "process-manager.ts", "tsconfig.json"],
52
55
  };
53
56
 
54
57
  // Must match examples/openclaw-plugin/install-manifest.json (npm only installs package deps, not these .ts files).
@@ -57,15 +60,19 @@ const FALLBACK_CURRENT = {
57
60
  id: "openviking",
58
61
  kind: "context-engine",
59
62
  slot: "contextEngine",
63
+ minOpenclawVersion: "2026.4.8",
60
64
  required: ["index.ts", "config.ts", "package.json", "openclaw.plugin.json"],
61
65
  optional: [
62
66
  "context-engine.ts",
67
+ "auto-recall.ts",
63
68
  "client.ts",
64
69
  "process-manager.ts",
65
70
  "memory-ranking.ts",
66
71
  "text-utils.ts",
67
72
  "tool-call-id.ts",
68
73
  "session-transcript-repair.ts",
74
+ "runtime-utils.ts",
75
+ "commands/setup.ts",
69
76
  "tsconfig.json",
70
77
  "package-lock.json",
71
78
  ".gitignore",
@@ -85,21 +92,31 @@ let resolvedPluginSlot = "";
85
92
  let resolvedFilesRequired = [];
86
93
  let resolvedFilesOptional = [];
87
94
  let resolvedNpmOmitDev = true;
95
+ let resolvedNpmBuild = false;
96
+ let resolvedNpmBuildMinOpenclawVersion = DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
97
+ let resolvedNpmBuildScript = "build";
98
+ let resolvedNpmPruneAfterBuild = true;
88
99
  let resolvedMinOpenclawVersion = "";
89
100
  let resolvedMinOpenvikingVersion = "";
90
101
  let resolvedPluginReleaseId = "";
102
+ let detectedOpenClawVersion = "";
91
103
 
92
- let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
104
+ let nonInteractive = false;
93
105
  let langZh = false;
94
106
  let workdirExplicit = false;
95
107
  let upgradePluginOnly = false;
96
108
  let rollbackLastUpgrade = false;
97
109
  let showCurrentVersion = false;
110
+ let uninstallPlugin = false;
98
111
 
99
112
  const selectedMode = "remote";
113
+ const baseUrlFromEnv = !!process.env.OPENVIKING_BASE_URL;
100
114
  let remoteBaseUrl = (process.env.OPENVIKING_BASE_URL || "http://127.0.0.1:1933").trim();
101
115
  let remoteApiKey = (process.env.OPENVIKING_API_KEY || "").trim();
102
116
  let remoteAgentPrefix = (process.env.OPENVIKING_AGENT_PREFIX || "").trim();
117
+ let remoteAccountId = (process.env.OPENVIKING_ACCOUNT_ID || "").trim();
118
+ let remoteUserId = (process.env.OPENVIKING_USER_ID || "").trim();
119
+ let baseUrlExplicit = baseUrlFromEnv;
103
120
  let upgradeRuntimeConfig = null;
104
121
  let installedUpgradeState = null;
105
122
  let upgradeAudit = null;
@@ -108,8 +125,11 @@ const argv = process.argv.slice(2);
108
125
  for (let i = 0; i < argv.length; i++) {
109
126
  const arg = argv[i];
110
127
  if (arg === "-y" || arg === "--yes") {
111
- installYes = true;
112
- continue;
128
+ err(tr(
129
+ "-y/--yes has been removed. Use --base-url <URL> [--api-key <KEY>] for non-interactive mode.",
130
+ "-y/--yes 已移除。使用 --base-url <URL> [--api-key <KEY>] 进入非交互模式。",
131
+ ));
132
+ process.exit(1);
113
133
  }
114
134
  if (arg === "--zh") {
115
135
  langZh = true;
@@ -127,6 +147,10 @@ for (let i = 0; i < argv.length; i++) {
127
147
  rollbackLastUpgrade = true;
128
148
  continue;
129
149
  }
150
+ if (arg === "--uninstall" || arg === "--remove") {
151
+ uninstallPlugin = true;
152
+ continue;
153
+ }
130
154
  if (arg === "--workdir") {
131
155
  const workdir = argv[i + 1]?.trim();
132
156
  if (!workdir) {
@@ -173,12 +197,71 @@ for (let i = 0; i < argv.length; i++) {
173
197
  i += 1;
174
198
  continue;
175
199
  }
200
+ if (arg === "--base-url") {
201
+ const val = argv[i + 1]?.trim();
202
+ if (!val) { console.error("--base-url requires a URL"); process.exit(1); }
203
+ remoteBaseUrl = val;
204
+ baseUrlExplicit = true;
205
+ i += 1;
206
+ continue;
207
+ }
208
+ if (arg.startsWith("--base-url=")) {
209
+ remoteBaseUrl = arg.slice("--base-url=".length).trim();
210
+ baseUrlExplicit = true;
211
+ continue;
212
+ }
213
+ if (arg === "--api-key") {
214
+ const val = argv[i + 1]?.trim();
215
+ if (!val) { console.error("--api-key requires a value"); process.exit(1); }
216
+ remoteApiKey = val;
217
+ i += 1;
218
+ continue;
219
+ }
220
+ if (arg.startsWith("--api-key=")) {
221
+ remoteApiKey = arg.slice("--api-key=".length).trim();
222
+ continue;
223
+ }
224
+ if (arg === "--agent-prefix") {
225
+ const val = argv[i + 1]?.trim();
226
+ if (!val) { console.error("--agent-prefix requires a value"); process.exit(1); }
227
+ remoteAgentPrefix = val;
228
+ i += 1;
229
+ continue;
230
+ }
231
+ if (arg.startsWith("--agent-prefix=")) {
232
+ remoteAgentPrefix = arg.slice("--agent-prefix=".length).trim();
233
+ continue;
234
+ }
235
+ if (arg === "--account-id") {
236
+ const val = argv[i + 1]?.trim();
237
+ if (!val) { console.error("--account-id requires a value"); process.exit(1); }
238
+ remoteAccountId = val;
239
+ i += 1;
240
+ continue;
241
+ }
242
+ if (arg.startsWith("--account-id=")) {
243
+ remoteAccountId = arg.slice("--account-id=".length).trim();
244
+ continue;
245
+ }
246
+ if (arg === "--user-id") {
247
+ const val = argv[i + 1]?.trim();
248
+ if (!val) { console.error("--user-id requires a value"); process.exit(1); }
249
+ remoteUserId = val;
250
+ i += 1;
251
+ continue;
252
+ }
253
+ if (arg.startsWith("--user-id=")) {
254
+ remoteUserId = arg.slice("--user-id=".length).trim();
255
+ continue;
256
+ }
176
257
  if (arg === "-h" || arg === "--help") {
177
258
  printHelp();
178
259
  process.exit(0);
179
260
  }
180
261
  }
181
262
 
263
+ nonInteractive = baseUrlExplicit;
264
+
182
265
  function setOpenClawDir(dir) {
183
266
  OPENCLAW_DIR = dir;
184
267
  }
@@ -195,7 +278,12 @@ function printHelp() {
195
278
  console.log(" Upgrade only the plugin to the requested --plugin-version; keeps existing plugin runtime config");
196
279
  console.log(" --rollback, --rollback-last-upgrade");
197
280
  console.log(" Roll back the last plugin upgrade using the saved audit/backup files");
198
- console.log(" -y, --yes Non-interactive (use defaults)");
281
+ console.log(" --uninstall, --remove Uninstall OpenViking plugin from OpenClaw (backup config, remove plugin entries)");
282
+ console.log(" --base-url=URL OpenViking server URL (default: $OPENVIKING_BASE_URL or http://127.0.0.1:1933)");
283
+ console.log(" --api-key=KEY OpenViking API key (default: $OPENVIKING_API_KEY)");
284
+ console.log(" --agent-prefix=PREFIX Agent routing prefix (default: $OPENVIKING_AGENT_PREFIX)");
285
+ console.log(" --account-id=ID Account ID for root API key (default: $OPENVIKING_ACCOUNT_ID)");
286
+ console.log(" --user-id=ID User ID for root API key (default: $OPENVIKING_USER_ID)");
199
287
  console.log(" --zh Chinese prompts");
200
288
  console.log(" -h, --help This help");
201
289
  console.log("");
@@ -326,6 +414,48 @@ function question(prompt, defaultValue = "") {
326
414
  });
327
415
  }
328
416
 
417
+ function isValidAgentPrefixInput(value) {
418
+ const trimmed = String(value || "").trim();
419
+ return !trimmed || /^[a-zA-Z0-9_-]+$/.test(trimmed);
420
+ }
421
+
422
+ function parseJsonObjectFromOutput(output) {
423
+ const text = String(output || "").trim();
424
+ if (!text) return null;
425
+ try {
426
+ return JSON.parse(text);
427
+ } catch {
428
+ // OpenClaw may print plugin registration logs before --json output.
429
+ }
430
+ for (let index = text.lastIndexOf("{"); index >= 0; index = text.lastIndexOf("{", index - 1)) {
431
+ try {
432
+ const parsed = JSON.parse(text.slice(index).trim());
433
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
434
+ return parsed;
435
+ }
436
+ } catch {
437
+ // Keep scanning earlier braces until the outer JSON object is found.
438
+ }
439
+ }
440
+ return null;
441
+ }
442
+
443
+ async function questionAgentPrefix(defaultValue = "") {
444
+ while (true) {
445
+ const answer = (await question(
446
+ tr("Agent Prefix (optional)", "Agent Prefix(可选)"),
447
+ defaultValue,
448
+ )).trim();
449
+ if (isValidAgentPrefixInput(answer)) {
450
+ return answer;
451
+ }
452
+ warn(tr(
453
+ "Agent Prefix may only contain letters, digits, underscores, and hyphens, or be empty.",
454
+ "Agent Prefix 只能包含字母、数字、下划线和连字符,或留空。",
455
+ ));
456
+ }
457
+ }
458
+
329
459
  function detectOpenClawInstances() {
330
460
  const instances = [];
331
461
  try {
@@ -349,7 +479,7 @@ async function selectWorkdir() {
349
479
  setOpenClawDir(instances[0]);
350
480
  return;
351
481
  }
352
- if (installYes) return;
482
+ if (nonInteractive) return;
353
483
 
354
484
  console.log("");
355
485
  bold(tr("Found multiple OpenClaw instances:", "发现多个 OpenClaw 实例:"));
@@ -369,10 +499,10 @@ async function selectWorkdir() {
369
499
  }
370
500
 
371
501
  async function collectRemoteConfig() {
372
- if (installYes) return;
502
+ if (nonInteractive) return;
373
503
  remoteBaseUrl = await question(tr("OpenViking server URL", "OpenViking 服务器地址"), remoteBaseUrl);
374
504
  remoteApiKey = await question(tr("API Key (optional)", "API Key(可选)"), remoteApiKey);
375
- remoteAgentPrefix = await question(tr("Agent Prefix (optional)", "Agent Prefix(可选)"), remoteAgentPrefix);
505
+ remoteAgentPrefix = await questionAgentPrefix(remoteAgentPrefix);
376
506
  }
377
507
 
378
508
  async function checkOpenClaw() {
@@ -414,6 +544,51 @@ function versionGte(v1, v2) {
414
544
  return a3 >= b3;
415
545
  }
416
546
 
547
+ function parseOpenClawPolicyVersion(value) {
548
+ const parts = String(value || "")
549
+ .match(/\d+/g)
550
+ ?.map((part) => Number.parseInt(part, 10) || 0) || [];
551
+ if (parts.length === 0) return [0, 0, 0];
552
+ if (parts[0] >= 2000) {
553
+ return [parts[0], parts[1] || 0, parts[2] || 0];
554
+ }
555
+ return [OPENCLAW_SHORT_VERSION_YEAR, parts[0] || 0, parts[1] || 0];
556
+ }
557
+
558
+ function openClawPolicyVersionGte(v1, v2) {
559
+ const a = parseOpenClawPolicyVersion(v1);
560
+ const b = parseOpenClawPolicyVersion(v2);
561
+ for (let i = 0; i < 3; i++) {
562
+ if (a[i] !== b[i]) return a[i] > b[i];
563
+ }
564
+ return true;
565
+ }
566
+
567
+ function applyOpenClawBuildPolicy(openClawVersion) {
568
+ if (!resolvedNpmBuild || !resolvedNpmBuildMinOpenclawVersion) {
569
+ return;
570
+ }
571
+ if (!openClawVersion || openClawVersion === "0.0.0") {
572
+ warn(tr(
573
+ "Could not determine OpenClaw version; keeping plugin source build enabled.",
574
+ "无法确定 OpenClaw 版本,保持插件源码构建开启。",
575
+ ));
576
+ return;
577
+ }
578
+ if (openClawPolicyVersionGte(openClawVersion, resolvedNpmBuildMinOpenclawVersion)) {
579
+ info(tr(
580
+ `OpenClaw ${openClawVersion} requires plugin source build (>= ${resolvedNpmBuildMinOpenclawVersion})`,
581
+ `OpenClaw ${openClawVersion} 需要插件源码构建(>= ${resolvedNpmBuildMinOpenclawVersion})`,
582
+ ));
583
+ return;
584
+ }
585
+ resolvedNpmBuild = false;
586
+ info(tr(
587
+ `OpenClaw ${openClawVersion} is below ${resolvedNpmBuildMinOpenclawVersion}; skipping plugin source build`,
588
+ `OpenClaw ${openClawVersion} 低于 ${resolvedNpmBuildMinOpenclawVersion},跳过插件源码构建`,
589
+ ));
590
+ }
591
+
417
592
  function isSemverLike(value) {
418
593
  return /^v?\d+(\.\d+){1,2}$/.test(value);
419
594
  }
@@ -431,16 +606,29 @@ if (upgradePluginOnly && rollbackLastUpgrade) {
431
606
  process.exit(1);
432
607
  }
433
608
 
609
+ if (uninstallPlugin && (upgradePluginOnly || rollbackLastUpgrade)) {
610
+ console.error("--uninstall cannot be used with --upgrade-plugin or --rollback");
611
+ process.exit(1);
612
+ }
613
+
434
614
  // Detect OpenClaw version
435
615
  async function detectOpenClawVersion() {
616
+ if (detectedOpenClawVersion) {
617
+ return detectedOpenClawVersion;
618
+ }
436
619
  try {
437
620
  const result = await runCapture("openclaw", ["--version"], { shell: IS_WIN });
438
- if (result.code === 0 && result.out) {
439
- const match = result.out.match(/\d+\.\d+(\.\d+)?/);
440
- if (match) return match[0];
621
+ const output = `${result.out || ""}\n${result.err || ""}`;
622
+ if (result.code === 0 && output) {
623
+ const match = output.match(/\d+\.\d+(\.\d+)?/);
624
+ if (match) {
625
+ detectedOpenClawVersion = match[0];
626
+ return detectedOpenClawVersion;
627
+ }
441
628
  }
442
629
  } catch {}
443
- return "0.0.0";
630
+ detectedOpenClawVersion = "0.0.0";
631
+ return detectedOpenClawVersion;
444
632
  }
445
633
 
446
634
  // Try to fetch a URL, return response text or null
@@ -589,6 +777,12 @@ async function resolvePluginConfig() {
589
777
 
590
778
  info(tr(`Resolving plugin configuration for version: ${PLUGIN_VERSION}`, `正在解析插件配置,版本: ${PLUGIN_VERSION}`));
591
779
 
780
+ resolvedNpmOmitDev = true;
781
+ resolvedNpmBuild = false;
782
+ resolvedNpmBuildMinOpenclawVersion = DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
783
+ resolvedNpmBuildScript = "build";
784
+ resolvedNpmPruneAfterBuild = true;
785
+
592
786
  let pluginDir = "";
593
787
  let manifestData = null;
594
788
 
@@ -630,7 +824,19 @@ async function resolvePluginConfig() {
630
824
  resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
631
825
  resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
632
826
  resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
633
- resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
827
+ const npmConfig = manifestData.npm && typeof manifestData.npm === "object"
828
+ ? manifestData.npm
829
+ : {};
830
+ resolvedNpmOmitDev = npmConfig.omitDev !== false;
831
+ resolvedNpmBuild = npmConfig.build === true || npmConfig.buildFromSource === true;
832
+ resolvedNpmBuildMinOpenclawVersion =
833
+ typeof npmConfig.buildMinOpenclawVersion === "string" && npmConfig.buildMinOpenclawVersion.trim()
834
+ ? npmConfig.buildMinOpenclawVersion.trim()
835
+ : DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
836
+ resolvedNpmBuildScript = typeof npmConfig.buildScript === "string" && npmConfig.buildScript.trim()
837
+ ? npmConfig.buildScript.trim()
838
+ : "build";
839
+ resolvedNpmPruneAfterBuild = npmConfig.pruneAfterBuild !== false;
634
840
  resolvedFilesRequired = manifestData.files?.required || [];
635
841
  resolvedFilesOptional = manifestData.files?.optional || [];
636
842
  } else {
@@ -665,6 +871,10 @@ async function resolvePluginConfig() {
665
871
  resolvedFilesRequired = fallback.required;
666
872
  resolvedFilesOptional = fallback.optional;
667
873
  resolvedNpmOmitDev = true;
874
+ resolvedNpmBuild = false;
875
+ resolvedNpmBuildMinOpenclawVersion = DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
876
+ resolvedNpmBuildScript = "build";
877
+ resolvedNpmPruneAfterBuild = true;
668
878
 
669
879
  // If no compatVer from package.json, try main branch manifest
670
880
  if (!compatVer && PLUGIN_VERSION !== "main") {
@@ -681,7 +891,7 @@ async function resolvePluginConfig() {
681
891
  }
682
892
  }
683
893
 
684
- resolvedMinOpenclawVersion = compatVer || "2026.3.7";
894
+ resolvedMinOpenclawVersion = compatVer || fallback.minOpenclawVersion || "2026.3.7";
685
895
  resolvedMinOpenvikingVersion = "";
686
896
  }
687
897
 
@@ -699,6 +909,7 @@ async function checkOpenClawCompatibility() {
699
909
 
700
910
  const ocVersion = await detectOpenClawVersion();
701
911
  info(tr(`Detected OpenClaw version: ${ocVersion}`, `检测到 OpenClaw 版本: ${ocVersion}`));
912
+ applyOpenClawBuildPolicy(ocVersion);
702
913
 
703
914
  // If no minimum version required, pass
704
915
  if (!resolvedMinOpenclawVersion) {
@@ -711,7 +922,7 @@ async function checkOpenClawCompatibility() {
711
922
  }
712
923
 
713
924
  // Check compatibility
714
- if (!versionGte(ocVersion, resolvedMinOpenclawVersion)) {
925
+ if (!openClawPolicyVersionGte(ocVersion, resolvedMinOpenclawVersion)) {
715
926
  err(tr(
716
927
  `OpenClaw ${ocVersion} does not support this plugin (requires >= ${resolvedMinOpenclawVersion})`,
717
928
  `OpenClaw ${ocVersion} 不支持此插件(需要 >= ${resolvedMinOpenclawVersion})`
@@ -1270,6 +1481,103 @@ async function downloadPluginFile(destDir, fileName, url, required, index, total
1270
1481
  process.exit(1);
1271
1482
  }
1272
1483
 
1484
+ function runtimeOutputCandidatesForEntry(entry) {
1485
+ const normalized = String(entry || "").replace(/\\/g, "/").replace(/^\.\//, "");
1486
+ if (!normalized.endsWith(".ts")) {
1487
+ return [];
1488
+ }
1489
+ const withoutExt = normalized.slice(0, -3);
1490
+ return [
1491
+ `dist/${withoutExt}.js`,
1492
+ `dist/${withoutExt}.mjs`,
1493
+ `dist/${withoutExt}.cjs`,
1494
+ `${withoutExt}.js`,
1495
+ `${withoutExt}.mjs`,
1496
+ `${withoutExt}.cjs`,
1497
+ ];
1498
+ }
1499
+
1500
+ async function assertBuiltRuntimeOutputs(destDir) {
1501
+ let pkg = null;
1502
+ try {
1503
+ pkg = JSON.parse(await readFile(join(destDir, "package.json"), "utf8"));
1504
+ } catch {
1505
+ return;
1506
+ }
1507
+
1508
+ const entries = [];
1509
+ const extensions = pkg?.openclaw?.extensions;
1510
+ if (Array.isArray(extensions)) {
1511
+ for (const entry of extensions) {
1512
+ if (typeof entry === "string") entries.push(entry);
1513
+ }
1514
+ }
1515
+ if (typeof pkg?.openclaw?.setupEntry === "string") {
1516
+ entries.push(pkg.openclaw.setupEntry);
1517
+ }
1518
+
1519
+ const missing = [];
1520
+ for (const entry of entries) {
1521
+ const candidates = runtimeOutputCandidatesForEntry(entry);
1522
+ if (candidates.length === 0) continue;
1523
+ const found = candidates.some((candidate) => existsSync(join(destDir, ...candidate.split("/"))));
1524
+ if (!found) {
1525
+ missing.push(`${entry} (expected one of: ${candidates.join(", ")})`);
1526
+ }
1527
+ }
1528
+
1529
+ if (missing.length === 0) {
1530
+ return;
1531
+ }
1532
+
1533
+ err(tr(
1534
+ `Plugin build did not create required runtime output:\n - ${missing.join("\n - ")}`,
1535
+ `插件构建未生成必需的运行时产物:\n - ${missing.join("\n - ")}`,
1536
+ ));
1537
+ process.exit(1);
1538
+ }
1539
+
1540
+ async function installPluginNpmDependencies(destDir) {
1541
+ if (!resolvedNpmBuild) {
1542
+ info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
1543
+ const npmArgs = resolvedNpmOmitDev
1544
+ ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1545
+ : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1546
+ await run("npm", npmArgs, { cwd: destDir, silent: false });
1547
+ return;
1548
+ }
1549
+
1550
+ info(tr(
1551
+ "Installing plugin npm dependencies for source build...",
1552
+ "正在安装插件源码构建所需的 npm 依赖...",
1553
+ ));
1554
+ await run("npm", [
1555
+ "install",
1556
+ "--include=dev",
1557
+ "--no-audit",
1558
+ "--no-fund",
1559
+ "--registry",
1560
+ NPM_REGISTRY,
1561
+ ], { cwd: destDir, silent: false });
1562
+
1563
+ info(tr(
1564
+ `Building plugin runtime output with npm run ${resolvedNpmBuildScript}...`,
1565
+ `正在执行 npm run ${resolvedNpmBuildScript} 构建插件运行时产物...`,
1566
+ ));
1567
+ await run("npm", ["run", resolvedNpmBuildScript], { cwd: destDir, silent: false });
1568
+ await assertBuiltRuntimeOutputs(destDir);
1569
+
1570
+ if (resolvedNpmOmitDev && resolvedNpmPruneAfterBuild) {
1571
+ info(tr("Pruning plugin dev dependencies...", "正在裁剪插件开发依赖..."));
1572
+ await run("npm", [
1573
+ "prune",
1574
+ "--omit=dev",
1575
+ "--no-audit",
1576
+ "--no-fund",
1577
+ ], { cwd: destDir, silent: false });
1578
+ }
1579
+ }
1580
+
1273
1581
  async function downloadPlugin(destDir) {
1274
1582
  const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
1275
1583
  const pluginDir = resolvedPluginDir;
@@ -1296,12 +1604,7 @@ async function downloadPlugin(destDir) {
1296
1604
  await downloadPluginFile(destDir, name, url, false, i, total);
1297
1605
  }
1298
1606
 
1299
- // npm install
1300
- info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
1301
- const npmArgs = resolvedNpmOmitDev
1302
- ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1303
- : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1304
- await run("npm", npmArgs, { cwd: destDir, silent: false });
1607
+ await installPluginNpmDependencies(destDir);
1305
1608
  info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
1306
1609
  }
1307
1610
 
@@ -1407,6 +1710,64 @@ async function scrubStaleOpenClawPluginRegistration() {
1407
1710
  await rename(tmp, configPath);
1408
1711
  }
1409
1712
 
1713
+ async function cleanupConflictingPluginVariants() {
1714
+ const configPath = getOpenClawConfigPath();
1715
+ if (!existsSync(configPath)) return;
1716
+ let cfg;
1717
+ try {
1718
+ cfg = JSON.parse(await readFile(configPath, "utf8"));
1719
+ } catch { return; }
1720
+ if (!cfg.plugins) return;
1721
+ const p = cfg.plugins;
1722
+ let changed = false;
1723
+ for (const variant of PLUGIN_VARIANTS) {
1724
+ if (variant.id === resolvedPluginId) continue;
1725
+ if (p.entries && Object.prototype.hasOwnProperty.call(p.entries, variant.id)) {
1726
+ info(tr(`Removing conflicting plugin variant: ${variant.id}`, `正在移除冲突的插件变体: ${variant.id}`));
1727
+ delete p.entries[variant.id];
1728
+ changed = true;
1729
+ }
1730
+ if (Array.isArray(p.allow)) {
1731
+ const next = p.allow.filter((id) => id !== variant.id);
1732
+ if (next.length !== p.allow.length) {
1733
+ p.allow = next;
1734
+ changed = true;
1735
+ }
1736
+ }
1737
+ if (p.installs && Object.prototype.hasOwnProperty.call(p.installs, variant.id)) {
1738
+ delete p.installs[variant.id];
1739
+ changed = true;
1740
+ }
1741
+ if (p.slots && p.slots[variant.slot] === variant.id) {
1742
+ p.slots[variant.slot] = variant.slotFallback || "none";
1743
+ changed = true;
1744
+ }
1745
+ if (p.load && Array.isArray(p.load.paths)) {
1746
+ const norm = (s) => String(s).replace(/\\/g, "/");
1747
+ const extNeedle = `/extensions/${variant.id}`;
1748
+ const next = p.load.paths.filter((path) => {
1749
+ if (typeof path !== "string") return true;
1750
+ return !norm(path).includes(extNeedle);
1751
+ });
1752
+ if (next.length !== p.load.paths.length) {
1753
+ p.load.paths = next;
1754
+ changed = true;
1755
+ }
1756
+ }
1757
+ const variantDir = join(OPENCLAW_DIR, "extensions", variant.id);
1758
+ if (existsSync(variantDir)) {
1759
+ info(tr(`Removing conflicting plugin directory: ${variantDir}`, `正在移除冲突的插件目录: ${variantDir}`));
1760
+ await rm(variantDir, { recursive: true, force: true });
1761
+ }
1762
+ }
1763
+ if (!changed) return;
1764
+ const out = JSON.stringify(cfg, null, 2) + "\n";
1765
+ const tmp = `${configPath}.ov-install-tmp.${process.pid}`;
1766
+ await writeFile(tmp, out, "utf8");
1767
+ await rename(tmp, configPath);
1768
+ info(tr("Conflicting plugin variants cleaned up", "冲突的插件变体已清理"));
1769
+ }
1770
+
1410
1771
  async function configureOpenClawPlugin({
1411
1772
  preserveExistingConfig = false,
1412
1773
  runtimeConfig = null,
@@ -1447,12 +1808,15 @@ async function configureOpenClawPlugin({
1447
1808
  const p = cfg.plugins;
1448
1809
  if (!p.entries) p.entries = {};
1449
1810
  if (!p.entries[pluginId]) p.entries[pluginId] = {};
1811
+ p.entries[pluginId].enabled = true;
1450
1812
  if (!p.entries[pluginId].config) p.entries[pluginId].config = {};
1451
1813
  if (!Array.isArray(p.allow)) p.allow = [];
1452
1814
  if (!p.allow.includes(pluginId)) p.allow.push(pluginId);
1453
1815
  return cfg;
1454
1816
  };
1455
1817
 
1818
+ await cleanupConflictingPluginVariants();
1819
+
1456
1820
  if (!preserveExistingConfig) {
1457
1821
  await scrubStaleOpenClawPluginRegistration();
1458
1822
  }
@@ -1488,7 +1852,7 @@ async function configureOpenClawPlugin({
1488
1852
  `已保留 ${pluginId} 的现有插件运行时配置`,
1489
1853
  ),
1490
1854
  );
1491
- return;
1855
+ return { runtimeConfigOk: true };
1492
1856
  }
1493
1857
 
1494
1858
  const writeConfigDirect = async (pluginConfig, slotValue) => {
@@ -1507,19 +1871,43 @@ async function configureOpenClawPlugin({
1507
1871
  const effectiveRuntimeConfig = runtimeConfig || {
1508
1872
  baseUrl: remoteBaseUrl,
1509
1873
  apiKey: remoteApiKey,
1874
+ agent_prefix: remoteAgentPrefix,
1510
1875
  };
1511
- const pluginConfig = {
1876
+
1877
+ let allowedPropsLegacy = null;
1878
+ try {
1879
+ const manifestPath = join(PLUGIN_DEST, "openclaw.plugin.json");
1880
+ if (existsSync(manifestPath)) {
1881
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1882
+ const schema = manifest?.configSchema;
1883
+ if (schema?.properties && typeof schema.properties === "object") {
1884
+ allowedPropsLegacy = new Set(Object.keys(schema.properties));
1885
+ }
1886
+ }
1887
+ } catch { /* ignore parse errors */ }
1888
+
1889
+ const agentVal = effectiveRuntimeConfig.agent_prefix || "";
1890
+ const candidates = {
1891
+ mode: "remote",
1512
1892
  baseUrl: effectiveRuntimeConfig.baseUrl || remoteBaseUrl,
1513
1893
  targetUri: "viking://user/memories",
1514
1894
  autoRecall: true,
1515
1895
  autoCapture: true,
1896
+ apiKey: effectiveRuntimeConfig.apiKey || undefined,
1897
+ agentId: agentVal || undefined,
1516
1898
  };
1517
- if (effectiveRuntimeConfig.apiKey) {
1518
- pluginConfig.apiKey = effectiveRuntimeConfig.apiKey;
1899
+
1900
+ const pluginConfig = {};
1901
+ for (const [key, val] of Object.entries(candidates)) {
1902
+ if (val === undefined) continue;
1903
+ if (allowedPropsLegacy && !allowedPropsLegacy.has(key)) continue;
1904
+ pluginConfig[key] = val;
1519
1905
  }
1906
+ if (!pluginConfig.baseUrl) pluginConfig.baseUrl = effectiveRuntimeConfig.baseUrl || remoteBaseUrl;
1907
+
1520
1908
  await writeConfigDirect(pluginConfig, claimSlot ? pluginId : null);
1521
- info(tr("OpenClaw plugin configured (legacy mode)", "OpenClaw 插件配置完成(旧版模式)"));
1522
- return;
1909
+ info(tr("OpenClaw plugin configured (legacy mode, remote)", "OpenClaw 插件配置完成(旧版模式,远程连接)"));
1910
+ return { runtimeConfigOk: true };
1523
1911
  }
1524
1912
 
1525
1913
  // Current (context-engine) plugins: delegate to `openclaw openviking setup --json`
@@ -1529,38 +1917,60 @@ async function configureOpenClawPlugin({
1529
1917
  baseUrl: remoteBaseUrl,
1530
1918
  apiKey: remoteApiKey,
1531
1919
  agent_prefix: remoteAgentPrefix,
1920
+ accountId: remoteAccountId,
1921
+ userId: remoteUserId,
1532
1922
  };
1533
1923
 
1534
- const setupArgs = needWorkdirFlag
1535
- ? ["--workdir", OPENCLAW_DIR, "openviking", "setup"]
1536
- : ["openviking", "setup"];
1537
- setupArgs.push("--base-url", effectiveRuntimeConfig.baseUrl || remoteBaseUrl);
1538
- setupArgs.push("--json");
1539
- if (effectiveRuntimeConfig.apiKey) {
1540
- setupArgs.push("--api-key", effectiveRuntimeConfig.apiKey);
1541
- }
1542
- if (effectiveRuntimeConfig.agent_prefix) {
1543
- setupArgs.push("--agent-id", effectiveRuntimeConfig.agent_prefix);
1544
- }
1545
- if (claimSlot) {
1546
- setupArgs.push("--force-slot");
1547
- }
1548
- if (installYes) {
1549
- setupArgs.push("--allow-offline");
1550
- }
1924
+ // Detect if the installed plugin supports `setup --json` by checking the deployed setup.ts
1925
+ let setupJsonSupported = false;
1926
+ try {
1927
+ const setupTsPath = join(PLUGIN_DEST, "commands", "setup.ts");
1928
+ if (existsSync(setupTsPath)) {
1929
+ const setupSrc = await readFile(setupTsPath, "utf8");
1930
+ setupJsonSupported = setupSrc.includes('"--json"') || setupSrc.includes("'--json'");
1931
+ }
1932
+ } catch { /* ignore read errors */ }
1551
1933
 
1552
- info(tr(
1553
- "Delegating configuration to: openclaw openviking setup --json",
1554
- "委托配置给: openclaw openviking setup --json",
1555
- ));
1934
+ let setupResult = null;
1935
+ if (setupJsonSupported) {
1936
+ const setupArgs = ["openviking", "setup"];
1937
+ setupArgs.push("--base-url", effectiveRuntimeConfig.baseUrl || remoteBaseUrl);
1938
+ setupArgs.push("--json");
1939
+ if (effectiveRuntimeConfig.apiKey) {
1940
+ setupArgs.push("--api-key", effectiveRuntimeConfig.apiKey);
1941
+ }
1942
+ if (effectiveRuntimeConfig.agent_prefix) {
1943
+ setupArgs.push("--agent-prefix", effectiveRuntimeConfig.agent_prefix);
1944
+ }
1945
+ if (effectiveRuntimeConfig.accountId) {
1946
+ setupArgs.push("--account-id", effectiveRuntimeConfig.accountId);
1947
+ }
1948
+ if (effectiveRuntimeConfig.userId) {
1949
+ setupArgs.push("--user-id", effectiveRuntimeConfig.userId);
1950
+ }
1951
+ if (claimSlot) {
1952
+ setupArgs.push("--force-slot");
1953
+ }
1954
+ if (nonInteractive) {
1955
+ setupArgs.push("--allow-offline");
1956
+ }
1556
1957
 
1557
- const setupResult = await runCapture("openclaw", setupArgs, { env: ocEnv, shell: IS_WIN });
1958
+ info(tr(
1959
+ "Delegating configuration to: openclaw openviking setup --json",
1960
+ "委托配置给: openclaw openviking setup --json",
1961
+ ));
1962
+
1963
+ setupResult = await runCapture("openclaw", setupArgs, { env: ocEnv, shell: IS_WIN });
1964
+ } else {
1965
+ info(tr(
1966
+ "Installed plugin does not support setup --json, using direct config write",
1967
+ "已安装的插件不支持 setup --json,使用直接配置写入",
1968
+ ));
1969
+ }
1558
1970
 
1559
1971
  let parsed = null;
1560
- try {
1561
- parsed = JSON.parse(setupResult.out.trim());
1562
- } catch {
1563
- // If JSON parse fails, fall back to checking exit code
1972
+ if (setupResult) {
1973
+ parsed = parseJsonObjectFromOutput(`${setupResult.out || ""}\n${setupResult.err || ""}`);
1564
1974
  }
1565
1975
 
1566
1976
  if (parsed) {
@@ -1593,34 +2003,67 @@ async function configureOpenClawPlugin({
1593
2003
  `Setup failed: ${parsed.error || "unknown error"}`,
1594
2004
  `配置失败: ${parsed.error || "未知错误"}`,
1595
2005
  ));
2006
+ return {
2007
+ runtimeConfigOk: false,
2008
+ error: parsed.error || parsed.action || "unknown error",
2009
+ };
1596
2010
  }
1597
2011
  }
1598
- } else if (setupResult.code !== 0) {
1599
- // JSON parse failed and non-zero exit — setup CLI not available (old plugin version)
2012
+ } else if (setupResult && setupResult.code !== 0) {
1600
2013
  warn(tr(
1601
2014
  `openclaw openviking setup exited with code ${setupResult.code}. Falling back to direct JSON config write.`,
1602
2015
  `openclaw openviking setup 退出码 ${setupResult.code},回退到直接写入 JSON 配置。`,
1603
2016
  ));
1604
- const pluginConfig = {
2017
+ }
2018
+
2019
+ if (!parsed) {
2020
+ // Direct write: only used when the installed plugin doesn't support `setup --json` (old version).
2021
+ // Read the deployed configSchema to determine which fields are allowed, avoiding
2022
+ // "additionalProperties" validation failures when writing new fields to old schemas.
2023
+ let allowedProps = null;
2024
+ try {
2025
+ const manifestPath = join(PLUGIN_DEST, "openclaw.plugin.json");
2026
+ if (existsSync(manifestPath)) {
2027
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
2028
+ const schema = manifest?.configSchema;
2029
+ if (schema?.properties && typeof schema.properties === "object") {
2030
+ allowedProps = new Set(Object.keys(schema.properties));
2031
+ }
2032
+ }
2033
+ } catch { /* ignore parse errors, write all fields */ }
2034
+
2035
+ const agentVal = effectiveRuntimeConfig.agent_prefix || "";
2036
+ const useAgentPrefix = !allowedProps || allowedProps.has("agent_prefix");
2037
+ const candidates = {
2038
+ mode: "remote",
1605
2039
  baseUrl: effectiveRuntimeConfig.baseUrl || remoteBaseUrl,
2040
+ apiKey: effectiveRuntimeConfig.apiKey || "",
2041
+ accountId: effectiveRuntimeConfig.accountId || undefined,
2042
+ userId: effectiveRuntimeConfig.userId || undefined,
1606
2043
  };
1607
- if (effectiveRuntimeConfig.apiKey) {
1608
- pluginConfig.apiKey = effectiveRuntimeConfig.apiKey;
2044
+ if (useAgentPrefix) {
2045
+ candidates.agent_prefix = agentVal;
2046
+ } else {
2047
+ candidates.agentId = agentVal;
1609
2048
  }
1610
- if (effectiveRuntimeConfig.agent_prefix) {
1611
- pluginConfig.agent_prefix = effectiveRuntimeConfig.agent_prefix;
2049
+
2050
+ const pluginConfig = {};
2051
+ for (const [key, val] of Object.entries(candidates)) {
2052
+ if (val === undefined) continue;
2053
+ if (allowedProps && !allowedProps.has(key)) continue;
2054
+ pluginConfig[key] = val;
1612
2055
  }
2056
+ if (!pluginConfig.baseUrl) pluginConfig.baseUrl = effectiveRuntimeConfig.baseUrl || remoteBaseUrl;
2057
+ if (!("apiKey" in pluginConfig)) pluginConfig.apiKey = effectiveRuntimeConfig.apiKey || "";
2058
+
1613
2059
  await writeConfigDirect(pluginConfig, claimSlot ? pluginId : null);
1614
- info(tr("OpenClaw plugin configured (direct write)", "OpenClaw 插件配置完成(直接写入)"));
1615
- if (effectiveRuntimeConfig.apiKey) {
1616
- info(tr(
1617
- "Note: Root API key detection and tenant context are only available via `openclaw openviking setup`. If using a root API key, upgrade the plugin and re-run setup.",
1618
- "提示:Root API Key 检测和租户上下文仅在 `openclaw openviking setup` 中可用。如果使用 root API key,请升级插件后重新执行 setup。",
1619
- ));
1620
- }
1621
- } else {
1622
- info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
2060
+ info(tr(
2061
+ `OpenClaw plugin configured (direct write): baseUrl=${pluginConfig.baseUrl}, apiKey=${pluginConfig.apiKey ? "***" : "(empty)"}`,
2062
+ `OpenClaw 插件配置完成(直接写入): baseUrl=${pluginConfig.baseUrl}, apiKey=${pluginConfig.apiKey ? "***" : "(空)"}`,
2063
+ ));
1623
2064
  }
2065
+
2066
+ return { runtimeConfigOk: true };
1624
2067
  }
1625
2068
 
1626
2069
  async function writeOpenvikingEnv() {
@@ -1678,6 +2121,125 @@ function getExistingEnvFiles() {
1678
2121
  return existsSync(envPath) ? { shellPath: envPath } : null;
1679
2122
  }
1680
2123
 
2124
+ async function performUninstall() {
2125
+ info(tr("Mode: uninstall plugin", "模式: 卸载插件"));
2126
+ info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
2127
+
2128
+ const configPath = getOpenClawConfigPath();
2129
+ if (!existsSync(configPath)) {
2130
+ info(tr(
2131
+ "No openclaw.json found. Nothing to uninstall.",
2132
+ "未找到 openclaw.json,无需卸载。",
2133
+ ));
2134
+ return;
2135
+ }
2136
+
2137
+ const installedState = await detectInstalledPluginState();
2138
+ if (installedState.generation === "none") {
2139
+ info(tr(
2140
+ "No OpenViking plugin entries found in openclaw.json. Nothing to uninstall.",
2141
+ "openclaw.json 中未找到 OpenViking 插件配置,无需卸载。",
2142
+ ));
2143
+ return;
2144
+ }
2145
+
2146
+ info(tr(
2147
+ `Detected installed plugin: ${formatInstalledStateLabel(installedState)}`,
2148
+ `检测到已安装插件: ${formatInstalledStateLabel(installedState)}`,
2149
+ ));
2150
+
2151
+ if (!nonInteractive) {
2152
+ const answer = await question(
2153
+ tr("Confirm uninstall? (y/N)", "确认卸载?(y/N)"),
2154
+ "N",
2155
+ );
2156
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
2157
+ info(tr("Cancelled.", "已取消。"));
2158
+ return;
2159
+ }
2160
+ }
2161
+
2162
+ // Step 1: Stop gateway
2163
+ info(tr("Step 1: Stopping OpenClaw gateway...", "步骤 1: 停止 OpenClaw gateway..."));
2164
+ await stopOpenClawGatewayForUpgrade();
2165
+
2166
+ // Step 2: Backup config
2167
+ info(tr("Step 2: Backing up configuration...", "步骤 2: 备份配置..."));
2168
+ const configBackupPath = await backupOpenClawConfig(configPath);
2169
+ info(tr(`Config backed up to: ${configBackupPath}`, `配置已备份至: ${configBackupPath}`));
2170
+
2171
+ // Step 3: Clean plugin config from openclaw.json
2172
+ info(tr("Step 3: Cleaning plugin configuration...", "步骤 3: 清理插件配置..."));
2173
+ await cleanupInstalledPluginConfig(installedState);
2174
+
2175
+ // Step 4: Backup and remove plugin directories
2176
+ info(tr("Step 4: Backing up plugin directories...", "步骤 4: 备份插件目录..."));
2177
+ const pluginBackups = [];
2178
+ for (const detection of installedState.detections) {
2179
+ const backupDir = await backupPluginDirectory(detection.variant);
2180
+ if (backupDir) {
2181
+ pluginBackups.push({ pluginId: detection.variant.id, backupDir });
2182
+ }
2183
+ }
2184
+
2185
+ // Step 5: Remove env files
2186
+ info(tr("Step 5: Removing environment files...", "步骤 5: 移除环境文件..."));
2187
+ const envFilesToRemove = IS_WIN
2188
+ ? [
2189
+ join(OPENCLAW_DIR, "openviking.env.bat"),
2190
+ join(OPENCLAW_DIR, "openviking.env.ps1"),
2191
+ ]
2192
+ : [join(OPENCLAW_DIR, "openviking.env")];
2193
+ let removedEnvCount = 0;
2194
+ for (const f of envFilesToRemove) {
2195
+ if (existsSync(f)) {
2196
+ try {
2197
+ await rm(f);
2198
+ removedEnvCount++;
2199
+ info(tr(`Removed: ${f}`, `已移除: ${f}`));
2200
+ } catch { /* ignore */ }
2201
+ }
2202
+ }
2203
+ if (removedEnvCount === 0) {
2204
+ info(tr("No environment files found.", "未找到环境文件。"));
2205
+ }
2206
+
2207
+ // Step 6: Write uninstall audit
2208
+ const auditDir = getUpgradeAuditDir();
2209
+ await mkdir(auditDir, { recursive: true });
2210
+ const auditData = {
2211
+ operation: "uninstall",
2212
+ createdAt: new Date().toISOString(),
2213
+ fromVersion: formatInstalledStateLabel(installedState),
2214
+ configBackupPath,
2215
+ pluginBackups,
2216
+ };
2217
+ await writeUpgradeAuditFile(auditData);
2218
+
2219
+ // Done
2220
+ console.log("");
2221
+ bold("═══════════════════════════════════════════════════════════");
2222
+ bold(` ${tr("Uninstall complete!", "卸载完成!")}`);
2223
+ bold("═══════════════════════════════════════════════════════════");
2224
+ console.log("");
2225
+
2226
+ info(tr("OpenViking server/runtime is preserved (not uninstalled).", "OpenViking 服务端/运行时已保留(未卸载)。"));
2227
+ console.log("");
2228
+
2229
+ info(tr("To restore the plugin configuration:", "如需恢复插件配置:"));
2230
+ console.log(` 1) ${tr("Stop gateway:", "停止 gateway:")} openclaw gateway stop`);
2231
+ console.log(` 2) ${tr("Restore config:", "恢复配置:")} ${IS_WIN ? "copy" : "cp"} "${configBackupPath}" "${configPath}"`);
2232
+ for (const pb of pluginBackups) {
2233
+ const liveDir = join(OPENCLAW_DIR, "extensions", pb.pluginId);
2234
+ console.log(` 3) ${tr("Restore plugin:", "恢复插件:")} ${IS_WIN ? "move" : "mv"} "${pb.backupDir}" "${liveDir}"`);
2235
+ }
2236
+ console.log("");
2237
+
2238
+ info(tr("To reinstall:", "重新安装:"));
2239
+ console.log(" ov-install");
2240
+ console.log("");
2241
+ }
2242
+
1681
2243
  async function main() {
1682
2244
  console.log("");
1683
2245
  bold(tr("🦣 OpenClaw OpenViking plugin installer", "🦣 OpenClaw OpenViking 插件安装"));
@@ -1688,6 +2250,10 @@ async function main() {
1688
2250
  await printCurrentVersionInfo();
1689
2251
  return;
1690
2252
  }
2253
+ if (uninstallPlugin) {
2254
+ await performUninstall();
2255
+ return;
2256
+ }
1691
2257
  if (rollbackLastUpgrade) {
1692
2258
  info(tr("Mode: rollback last plugin upgrade", "模式: 回滚最近一次插件升级"));
1693
2259
  if (pluginVersionExplicit) {
@@ -1721,7 +2287,7 @@ async function main() {
1721
2287
 
1722
2288
  await deployPluginFromRemote();
1723
2289
 
1724
- await configureOpenClawPlugin(
2290
+ const configResult = await configureOpenClawPlugin(
1725
2291
  upgradePluginOnly
1726
2292
  ? {
1727
2293
  runtimeConfig: upgradeRuntimeConfig,
@@ -1729,15 +2295,23 @@ async function main() {
1729
2295
  }
1730
2296
  : { preserveExistingConfig: false },
1731
2297
  );
1732
- await writeInstallStateFile({
1733
- operation: upgradePluginOnly ? "upgrade" : "install",
1734
- fromVersion: upgradeAudit?.fromVersion || "",
1735
- configBackupPath: upgradeAudit?.configBackupPath || "",
1736
- pluginBackups: upgradeAudit?.pluginBackups || [],
1737
- });
1738
- if (upgradeAudit) {
1739
- upgradeAudit.completedAt = new Date().toISOString();
1740
- await writeUpgradeAuditFile(upgradeAudit);
2298
+ const runtimeConfigOk = configResult?.runtimeConfigOk !== false;
2299
+ const runtimeConfigError = configResult?.error || "";
2300
+
2301
+ // Only mark the install as completed (state file + upgrade audit) when the
2302
+ // runtime config was actually applied. Plugin files are already on disk
2303
+ // either way, so subsequent runs can pick up from here.
2304
+ if (runtimeConfigOk) {
2305
+ await writeInstallStateFile({
2306
+ operation: upgradePluginOnly ? "upgrade" : "install",
2307
+ fromVersion: upgradeAudit?.fromVersion || "",
2308
+ configBackupPath: upgradeAudit?.configBackupPath || "",
2309
+ pluginBackups: upgradeAudit?.pluginBackups || [],
2310
+ });
2311
+ if (upgradeAudit) {
2312
+ upgradeAudit.completedAt = new Date().toISOString();
2313
+ await writeUpgradeAuditFile(upgradeAudit);
2314
+ }
1741
2315
  }
1742
2316
  let envFiles = getExistingEnvFiles();
1743
2317
  if (!upgradePluginOnly) {
@@ -1747,18 +2321,33 @@ async function main() {
1747
2321
  }
1748
2322
 
1749
2323
  console.log("");
1750
- bold("═══════════════════════════════════════════════════════════");
1751
- bold(` ${tr("Installation complete!", "安装完成!")}`);
1752
- bold("═══════════════════════════════════════════════════════════");
1753
- console.log("");
2324
+ if (runtimeConfigOk) {
2325
+ bold("═══════════════════════════════════════════════════════════");
2326
+ bold(` ${tr("Installation complete!", "安装完成!")}`);
2327
+ bold("═══════════════════════════════════════════════════════════");
2328
+ console.log("");
1754
2329
 
1755
- if (upgradeAudit) {
1756
- info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
1757
- info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
1758
- for (const pluginBackup of upgradeAudit.pluginBackups || []) {
1759
- info(tr(`Rollback plugin backup: ${pluginBackup.backupDir}`, `回滚插件备份: ${pluginBackup.backupDir}`));
2330
+ if (upgradeAudit) {
2331
+ info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
2332
+ info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
2333
+ for (const pluginBackup of upgradeAudit.pluginBackups || []) {
2334
+ info(tr(`Rollback plugin backup: ${pluginBackup.backupDir}`, `回滚插件备份: ${pluginBackup.backupDir}`));
2335
+ }
2336
+ info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
2337
+ console.log("");
1760
2338
  }
1761
- info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
2339
+ } else {
2340
+ bold("═══════════════════════════════════════════════════════════");
2341
+ bold(` ${tr(
2342
+ "Plugin files installed, but runtime configuration was NOT applied",
2343
+ "插件文件已安装,但运行时配置未生效",
2344
+ )}`);
2345
+ bold(` ${tr(`Reason: ${runtimeConfigError}`, `原因: ${runtimeConfigError}`)}`);
2346
+ bold(` ${tr(
2347
+ "Re-run: openclaw openviking setup --reconfigure",
2348
+ "重新运行: openclaw openviking setup --reconfigure",
2349
+ )}`);
2350
+ bold("═══════════════════════════════════════════════════════════");
1762
2351
  console.log("");
1763
2352
  }
1764
2353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-openviking-setup-helper",
3
- "version": "0.3.0-beta.9",
3
+ "version": "0.3.0",
4
4
  "description": "Setup helper for installing OpenViking memory plugin into OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {