openclaw-openviking-setup-helper 0.3.0-beta.8 → 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 +745 -155
  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,
@@ -1419,12 +1780,9 @@ async function configureOpenClawPlugin({
1419
1780
 
1420
1781
  const ocEnv = getOpenClawEnv();
1421
1782
  const needWorkdirFlag = OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR;
1422
- const workdirCmds = new Set(["config", "openviking", "status", "gateway", "logs"]);
1423
1783
 
1424
1784
  const oc = async (args) => {
1425
- const useWorkdir = needWorkdirFlag && args.length > 0 && workdirCmds.has(args[0]);
1426
- const fullArgs = useWorkdir ? ["--workdir", OPENCLAW_DIR, ...args] : args;
1427
- const result = await runCapture("openclaw", fullArgs, { env: ocEnv, shell: IS_WIN });
1785
+ const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
1428
1786
  if (result.code !== 0) {
1429
1787
  const detail = result.err || result.out;
1430
1788
  throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
@@ -1432,16 +1790,61 @@ async function configureOpenClawPlugin({
1432
1790
  return result;
1433
1791
  };
1434
1792
 
1793
+ // Direct file manipulation for config (reliable across all OpenClaw versions and --workdir scenarios).
1794
+ // OpenClaw CLI's --workdir / OPENCLAW_STATE_DIR support is inconsistent, so we read/write openclaw.json directly.
1795
+ const configPath = getOpenClawConfigPath();
1796
+ const readCfg = async () => {
1797
+ if (!existsSync(configPath)) return {};
1798
+ try { return JSON.parse(await readFile(configPath, "utf8")); } catch { return {}; }
1799
+ };
1800
+ const writeCfg = async (cfg) => {
1801
+ const out = JSON.stringify(cfg, null, 2) + "\n";
1802
+ const tmp = `${configPath}.ov-install-tmp.${process.pid}`;
1803
+ await writeFile(tmp, out, "utf8");
1804
+ await rename(tmp, configPath);
1805
+ };
1806
+ const ensurePluginRegistered = async (cfg) => {
1807
+ if (!cfg.plugins) cfg.plugins = {};
1808
+ const p = cfg.plugins;
1809
+ if (!p.entries) p.entries = {};
1810
+ if (!p.entries[pluginId]) p.entries[pluginId] = {};
1811
+ p.entries[pluginId].enabled = true;
1812
+ if (!p.entries[pluginId].config) p.entries[pluginId].config = {};
1813
+ if (!Array.isArray(p.allow)) p.allow = [];
1814
+ if (!p.allow.includes(pluginId)) p.allow.push(pluginId);
1815
+ return cfg;
1816
+ };
1817
+
1818
+ await cleanupConflictingPluginVariants();
1819
+
1435
1820
  if (!preserveExistingConfig) {
1436
1821
  await scrubStaleOpenClawPluginRegistration();
1437
1822
  }
1438
1823
 
1439
- // Enable plugin (files already deployed to extensions dir by deployPlugin)
1440
- await oc(["plugins", "enable", pluginId]);
1824
+ // Enable plugin: try CLI first (default path), fall back to direct file for --workdir
1825
+ if (!needWorkdirFlag) {
1826
+ try {
1827
+ await oc(["plugins", "enable", pluginId]);
1828
+ } catch (_e) {
1829
+ info(tr("plugins enable via CLI failed, registering directly", "CLI plugins enable 失败,直接注册"));
1830
+ const cfg = await readCfg();
1831
+ await ensurePluginRegistered(cfg);
1832
+ await writeCfg(cfg);
1833
+ }
1834
+ } else {
1835
+ info(tr("Using direct config write for non-default workdir", "非默认目录,使用直接配置写入"));
1836
+ const cfg = await readCfg();
1837
+ await ensurePluginRegistered(cfg);
1838
+ await writeCfg(cfg);
1839
+ }
1441
1840
 
1442
1841
  if (preserveExistingConfig) {
1443
1842
  if (claimSlot) {
1444
- await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
1843
+ const cfg = await readCfg();
1844
+ if (!cfg.plugins) cfg.plugins = {};
1845
+ if (!cfg.plugins.slots) cfg.plugins.slots = {};
1846
+ cfg.plugins.slots[pluginSlot] = pluginId;
1847
+ await writeCfg(cfg);
1445
1848
  }
1446
1849
  info(
1447
1850
  tr(
@@ -1449,34 +1852,62 @@ async function configureOpenClawPlugin({
1449
1852
  `已保留 ${pluginId} 的现有插件运行时配置`,
1450
1853
  ),
1451
1854
  );
1452
- return;
1855
+ return { runtimeConfigOk: true };
1453
1856
  }
1454
1857
 
1455
- // Legacy (memory) plugins still use direct config set
1456
- if (resolvedPluginKind === "memory") {
1457
- if (claimSlot) {
1458
- try { await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]); } catch (_e) {
1459
- warn(tr("Could not activate slot for legacy plugin", "无法为旧版插件激活 slot"));
1460
- }
1858
+ const writeConfigDirect = async (pluginConfig, slotValue) => {
1859
+ const cfg = await readCfg();
1860
+ await ensurePluginRegistered(cfg);
1861
+ Object.assign(cfg.plugins.entries[pluginId].config, pluginConfig);
1862
+ if (slotValue) {
1863
+ if (!cfg.plugins.slots) cfg.plugins.slots = {};
1864
+ cfg.plugins.slots[pluginSlot] = slotValue;
1461
1865
  }
1866
+ await writeCfg(cfg);
1867
+ };
1868
+
1869
+ // Legacy (memory) plugins: direct JSON write
1870
+ if (resolvedPluginKind === "memory") {
1462
1871
  const effectiveRuntimeConfig = runtimeConfig || {
1463
1872
  baseUrl: remoteBaseUrl,
1464
1873
  apiKey: remoteApiKey,
1465
1874
  agent_prefix: remoteAgentPrefix,
1466
1875
  };
1467
- const trySet = async (k, v) => {
1468
- try { await oc(["config", "set", `plugins.entries.${pluginId}.config.${k}`, v]); }
1469
- catch (_e) { warn(tr(`Could not set ${k} for legacy plugin`, `无法为旧版插件设置 ${k}`)); }
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",
1892
+ baseUrl: effectiveRuntimeConfig.baseUrl || remoteBaseUrl,
1893
+ targetUri: "viking://user/memories",
1894
+ autoRecall: true,
1895
+ autoCapture: true,
1896
+ apiKey: effectiveRuntimeConfig.apiKey || undefined,
1897
+ agentId: agentVal || undefined,
1470
1898
  };
1471
- await trySet("baseUrl", effectiveRuntimeConfig.baseUrl || remoteBaseUrl);
1472
- if (effectiveRuntimeConfig.apiKey) {
1473
- await trySet("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;
1474
1905
  }
1475
- await trySet("targetUri", "viking://user/memories");
1476
- try { await oc(["config", "set", `plugins.entries.${pluginId}.config.autoRecall`, "true", "--json"]); } catch (_e) { /* optional */ }
1477
- try { await oc(["config", "set", `plugins.entries.${pluginId}.config.autoCapture`, "true", "--json"]); } catch (_e) { /* optional */ }
1478
- info(tr("OpenClaw plugin configured (legacy mode)", "OpenClaw 插件配置完成(旧版模式)"));
1479
- return;
1906
+ if (!pluginConfig.baseUrl) pluginConfig.baseUrl = effectiveRuntimeConfig.baseUrl || remoteBaseUrl;
1907
+
1908
+ await writeConfigDirect(pluginConfig, claimSlot ? pluginId : null);
1909
+ info(tr("OpenClaw plugin configured (legacy mode, remote)", "OpenClaw 插件配置完成(旧版模式,远程连接)"));
1910
+ return { runtimeConfigOk: true };
1480
1911
  }
1481
1912
 
1482
1913
  // Current (context-engine) plugins: delegate to `openclaw openviking setup --json`
@@ -1486,38 +1917,60 @@ async function configureOpenClawPlugin({
1486
1917
  baseUrl: remoteBaseUrl,
1487
1918
  apiKey: remoteApiKey,
1488
1919
  agent_prefix: remoteAgentPrefix,
1920
+ accountId: remoteAccountId,
1921
+ userId: remoteUserId,
1489
1922
  };
1490
1923
 
1491
- const setupArgs = needWorkdirFlag
1492
- ? ["--workdir", OPENCLAW_DIR, "openviking", "setup"]
1493
- : ["openviking", "setup"];
1494
- setupArgs.push("--base-url", effectiveRuntimeConfig.baseUrl || remoteBaseUrl);
1495
- setupArgs.push("--json");
1496
- if (effectiveRuntimeConfig.apiKey) {
1497
- setupArgs.push("--api-key", effectiveRuntimeConfig.apiKey);
1498
- }
1499
- if (effectiveRuntimeConfig.agent_prefix) {
1500
- setupArgs.push("--agent-id", effectiveRuntimeConfig.agent_prefix);
1501
- }
1502
- if (claimSlot) {
1503
- setupArgs.push("--force-slot");
1504
- }
1505
- if (installYes) {
1506
- setupArgs.push("--allow-offline");
1507
- }
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 */ }
1508
1933
 
1509
- info(tr(
1510
- "Delegating configuration to: openclaw openviking setup --json",
1511
- "委托配置给: openclaw openviking setup --json",
1512
- ));
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
+ }
1513
1957
 
1514
- 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
+ }
1515
1970
 
1516
1971
  let parsed = null;
1517
- try {
1518
- parsed = JSON.parse(setupResult.out.trim());
1519
- } catch {
1520
- // If JSON parse fails, fall back to checking exit code
1972
+ if (setupResult) {
1973
+ parsed = parseJsonObjectFromOutput(`${setupResult.out || ""}\n${setupResult.err || ""}`);
1521
1974
  }
1522
1975
 
1523
1976
  if (parsed) {
@@ -1550,76 +2003,67 @@ async function configureOpenClawPlugin({
1550
2003
  `Setup failed: ${parsed.error || "unknown error"}`,
1551
2004
  `配置失败: ${parsed.error || "未知错误"}`,
1552
2005
  ));
2006
+ return {
2007
+ runtimeConfigOk: false,
2008
+ error: parsed.error || parsed.action || "unknown error",
2009
+ };
1553
2010
  }
1554
2011
  }
1555
- } else if (setupResult.code !== 0) {
1556
- // JSON parse failed and non-zero exit
2012
+ } else if (setupResult && setupResult.code !== 0) {
1557
2013
  warn(tr(
1558
- `openclaw openviking setup exited with code ${setupResult.code}. Falling back to direct config.`,
1559
- `openclaw openviking setup 退出码 ${setupResult.code},回退到直接配置。`,
2014
+ `openclaw openviking setup exited with code ${setupResult.code}. Falling back to direct JSON config write.`,
2015
+ `openclaw openviking setup 退出码 ${setupResult.code},回退到直接写入 JSON 配置。`,
1560
2016
  ));
1561
- // Fallback: direct config set (for backward compat if setup CLI not available)
1562
- // Each field is wrapped individually so one schema mismatch does not abort the rest.
1563
- let fallbackOk = true;
1564
- const tryConfigSet = async (key, value, altKey) => {
1565
- try {
1566
- await oc(["config", "set", `plugins.entries.${pluginId}.config.${key}`, value]);
1567
- } catch (_e) {
1568
- if (altKey) {
1569
- try {
1570
- await oc(["config", "set", `plugins.entries.${pluginId}.config.${altKey}`, value]);
1571
- return;
1572
- } catch (_e2) { /* fall through to warn */ }
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));
1573
2031
  }
1574
- fallbackOk = false;
1575
- warn(tr(
1576
- `Could not set config field "${key}": schema validation failed (plugin version may not support this field)`,
1577
- `无法设置配置字段 "${key}":schema 校验失败(插件版本可能不支持此字段)`,
1578
- ));
1579
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",
2039
+ baseUrl: effectiveRuntimeConfig.baseUrl || remoteBaseUrl,
2040
+ apiKey: effectiveRuntimeConfig.apiKey || "",
2041
+ accountId: effectiveRuntimeConfig.accountId || undefined,
2042
+ userId: effectiveRuntimeConfig.userId || undefined,
1580
2043
  };
1581
-
1582
- await tryConfigSet("baseUrl", effectiveRuntimeConfig.baseUrl || remoteBaseUrl);
1583
- if (effectiveRuntimeConfig.apiKey) {
1584
- await tryConfigSet("apiKey", effectiveRuntimeConfig.apiKey);
1585
- }
1586
- if (effectiveRuntimeConfig.agent_prefix) {
1587
- await tryConfigSet("agent_prefix", effectiveRuntimeConfig.agent_prefix, "agentId");
1588
- }
1589
- if (claimSlot) {
1590
- try {
1591
- await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
1592
- } catch (_e) {
1593
- fallbackOk = false;
1594
- warn(tr(
1595
- "Could not activate contextEngine slot",
1596
- "无法激活 contextEngine slot",
1597
- ));
1598
- }
1599
- }
1600
- if (fallbackOk) {
1601
- info(tr("OpenClaw plugin configured (fallback)", "OpenClaw 插件配置完成(回退模式)"));
2044
+ if (useAgentPrefix) {
2045
+ candidates.agent_prefix = agentVal;
1602
2046
  } else {
1603
- warn(tr(
1604
- "OpenClaw plugin partially configured (fallback). Some fields could not be set. Run `openclaw openviking setup` to complete configuration.",
1605
- "OpenClaw 插件部分配置完成(回退模式)。部分字段未能设置,请运行 `openclaw openviking setup` 完善配置。",
1606
- ));
2047
+ candidates.agentId = agentVal;
1607
2048
  }
1608
- if (effectiveRuntimeConfig.apiKey) {
1609
- warn(tr(
1610
- "Note: Root API key detection, accountId/userId tenant context, and server version compatibility checks are only available via `openclaw openviking setup`. If you are using a root API key, please upgrade the plugin and re-run setup.",
1611
- "提示:Root API Key 检测、accountId/userId 租户上下文、服务端版本兼容性检查仅在 `openclaw openviking setup` 中可用。如果您使用的是 root API key,请升级插件后重新执行 setup。",
1612
- ));
1613
- }
1614
- if (effectiveRuntimeConfig.apiKey) {
1615
- info(tr(
1616
- "Note: If using a root API key, run `openclaw openviking setup` to configure accountId/userId (required for tenant isolation).",
1617
- "提示:如果使用的是 root API key,请运行 `openclaw openviking setup` 配置 accountId/userId(多租户隔离必需)。",
1618
- ));
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;
1619
2055
  }
1620
- } else {
1621
- info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
2056
+ if (!pluginConfig.baseUrl) pluginConfig.baseUrl = effectiveRuntimeConfig.baseUrl || remoteBaseUrl;
2057
+ if (!("apiKey" in pluginConfig)) pluginConfig.apiKey = effectiveRuntimeConfig.apiKey || "";
2058
+
2059
+ await writeConfigDirect(pluginConfig, claimSlot ? pluginId : null);
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
+ ));
1622
2064
  }
2065
+
2066
+ return { runtimeConfigOk: true };
1623
2067
  }
1624
2068
 
1625
2069
  async function writeOpenvikingEnv() {
@@ -1677,6 +2121,125 @@ function getExistingEnvFiles() {
1677
2121
  return existsSync(envPath) ? { shellPath: envPath } : null;
1678
2122
  }
1679
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
+
1680
2243
  async function main() {
1681
2244
  console.log("");
1682
2245
  bold(tr("🦣 OpenClaw OpenViking plugin installer", "🦣 OpenClaw OpenViking 插件安装"));
@@ -1687,6 +2250,10 @@ async function main() {
1687
2250
  await printCurrentVersionInfo();
1688
2251
  return;
1689
2252
  }
2253
+ if (uninstallPlugin) {
2254
+ await performUninstall();
2255
+ return;
2256
+ }
1690
2257
  if (rollbackLastUpgrade) {
1691
2258
  info(tr("Mode: rollback last plugin upgrade", "模式: 回滚最近一次插件升级"));
1692
2259
  if (pluginVersionExplicit) {
@@ -1720,7 +2287,7 @@ async function main() {
1720
2287
 
1721
2288
  await deployPluginFromRemote();
1722
2289
 
1723
- await configureOpenClawPlugin(
2290
+ const configResult = await configureOpenClawPlugin(
1724
2291
  upgradePluginOnly
1725
2292
  ? {
1726
2293
  runtimeConfig: upgradeRuntimeConfig,
@@ -1728,15 +2295,23 @@ async function main() {
1728
2295
  }
1729
2296
  : { preserveExistingConfig: false },
1730
2297
  );
1731
- await writeInstallStateFile({
1732
- operation: upgradePluginOnly ? "upgrade" : "install",
1733
- fromVersion: upgradeAudit?.fromVersion || "",
1734
- configBackupPath: upgradeAudit?.configBackupPath || "",
1735
- pluginBackups: upgradeAudit?.pluginBackups || [],
1736
- });
1737
- if (upgradeAudit) {
1738
- upgradeAudit.completedAt = new Date().toISOString();
1739
- 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
+ }
1740
2315
  }
1741
2316
  let envFiles = getExistingEnvFiles();
1742
2317
  if (!upgradePluginOnly) {
@@ -1746,18 +2321,33 @@ async function main() {
1746
2321
  }
1747
2322
 
1748
2323
  console.log("");
1749
- bold("═══════════════════════════════════════════════════════════");
1750
- bold(` ${tr("Installation complete!", "安装完成!")}`);
1751
- bold("═══════════════════════════════════════════════════════════");
1752
- console.log("");
2324
+ if (runtimeConfigOk) {
2325
+ bold("═══════════════════════════════════════════════════════════");
2326
+ bold(` ${tr("Installation complete!", "安装完成!")}`);
2327
+ bold("═══════════════════════════════════════════════════════════");
2328
+ console.log("");
1753
2329
 
1754
- if (upgradeAudit) {
1755
- info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
1756
- info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
1757
- for (const pluginBackup of upgradeAudit.pluginBackups || []) {
1758
- 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("");
1759
2338
  }
1760
- 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("═══════════════════════════════════════════════════════════");
1761
2351
  console.log("");
1762
2352
  }
1763
2353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-openviking-setup-helper",
3
- "version": "0.3.0-beta.8",
3
+ "version": "0.3.0",
4
4
  "description": "Setup helper for installing OpenViking memory plugin into OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {