openclaw-openviking-setup-helper 0.3.0-beta.9 → 0.3.1-beta.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 +1251 -218
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -3,36 +3,43 @@
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
- * Direct run:
13
- * node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ] [ --upgrade-plugin ]
14
- * [ --plugin-version=TAG ]
15
- *
16
- * Environment variables:
17
- * REPO, PLUGIN_VERSION (or BRANCH), OPENVIKING_INSTALL_YES, SKIP_OPENCLAW
18
- * NPM_REGISTRY
19
- */
12
+ * Direct run:
13
+ * node install.js [ --base-url URL ] [ --api-key KEY ] [ --zh ] [ --workdir PATH ] [ --upgrade-plugin ]
14
+ * [ --plugin-version=VERSION ] [ --plugin-source=npm|github ]
15
+ *
16
+ * Environment variables:
17
+ * PLUGIN_SOURCE, PLUGIN_NPM_PACKAGE, REPO, PLUGIN_VERSION (or BRANCH),
18
+ * OPENVIKING_BASE_URL, OPENVIKING_API_KEY, SKIP_OPENCLAW, NPM_REGISTRY
19
+ */
20
20
 
21
21
  import { spawn } from "node:child_process";
22
- import { cp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
23
- import { existsSync, readdirSync } from "node:fs";
24
- import { basename, dirname, join } from "node:path";
25
- import { createInterface } from "node:readline";
26
- import { fileURLToPath } from "node:url";
22
+ import { cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
23
+ import { existsSync, readdirSync } from "node:fs";
24
+ import { basename, dirname, join } from "node:path";
25
+ import { tmpdir } from "node:os";
26
+ import { createInterface } from "node:readline";
27
+ import { fileURLToPath } from "node:url";
27
28
 
28
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
29
30
 
30
- let REPO = process.env.REPO || "volcengine/OpenViking";
31
- // PLUGIN_VERSION takes precedence over BRANCH (legacy). If omitted, resolve the latest tag from GitHub.
32
- const pluginVersionEnv = (process.env.PLUGIN_VERSION || process.env.BRANCH || "").trim();
33
- let PLUGIN_VERSION = pluginVersionEnv;
34
- let pluginVersionExplicit = Boolean(pluginVersionEnv);
31
+ let REPO = process.env.REPO || "volcengine/OpenViking";
32
+ const DEFAULT_PLUGIN_NPM_PACKAGE = "@openviking/openclaw-plugin";
33
+ let pluginNpmPackage = (process.env.PLUGIN_NPM_PACKAGE || DEFAULT_PLUGIN_NPM_PACKAGE).trim();
34
+ let pluginSource = (process.env.PLUGIN_SOURCE || "npm").trim().toLowerCase();
35
+ let pluginSourceExplicit = Boolean(process.env.PLUGIN_SOURCE);
36
+ // PLUGIN_VERSION takes precedence over BRANCH (legacy). If omitted, resolve the latest npm dist-tag or GitHub tag.
37
+ const pluginVersionEnv = (process.env.PLUGIN_VERSION || process.env.BRANCH || "").trim();
38
+ let PLUGIN_VERSION = pluginVersionEnv;
39
+ let pluginVersionExplicit = Boolean(pluginVersionEnv);
35
40
  const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmmirror.com";
41
+ const DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION = "2026.5.3";
42
+ const OPENCLAW_SHORT_VERSION_YEAR = 2026;
36
43
 
37
44
  const IS_WIN = process.platform === "win32";
38
45
  const HOME = process.env.HOME || process.env.USERPROFILE || "";
@@ -47,8 +54,9 @@ const FALLBACK_LEGACY = {
47
54
  id: "memory-openviking",
48
55
  kind: "memory",
49
56
  slot: "memory",
50
- required: ["index.ts", "config.ts", "openclaw.plugin.json", "package.json"],
51
- optional: ["package-lock.json", ".gitignore"],
57
+ minOpenclawVersion: "2026.3.7",
58
+ required: ["index.ts", "config.ts", "client.ts", "openclaw.plugin.json", "package.json"],
59
+ optional: ["package-lock.json", ".gitignore", "memory-ranking.ts", "text-utils.ts", "process-manager.ts", "tsconfig.json"],
52
60
  };
53
61
 
54
62
  // Must match examples/openclaw-plugin/install-manifest.json (npm only installs package deps, not these .ts files).
@@ -57,15 +65,19 @@ const FALLBACK_CURRENT = {
57
65
  id: "openviking",
58
66
  kind: "context-engine",
59
67
  slot: "contextEngine",
68
+ minOpenclawVersion: "2026.4.8",
60
69
  required: ["index.ts", "config.ts", "package.json", "openclaw.plugin.json"],
61
70
  optional: [
62
71
  "context-engine.ts",
72
+ "auto-recall.ts",
63
73
  "client.ts",
64
74
  "process-manager.ts",
65
75
  "memory-ranking.ts",
66
76
  "text-utils.ts",
67
77
  "tool-call-id.ts",
68
78
  "session-transcript-repair.ts",
79
+ "runtime-utils.ts",
80
+ "commands/setup.ts",
69
81
  "tsconfig.json",
70
82
  "package-lock.json",
71
83
  ".gitignore",
@@ -85,21 +97,35 @@ let resolvedPluginSlot = "";
85
97
  let resolvedFilesRequired = [];
86
98
  let resolvedFilesOptional = [];
87
99
  let resolvedNpmOmitDev = true;
100
+ let resolvedNpmBuild = false;
101
+ let resolvedNpmBuildMinOpenclawVersion = DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
102
+ let resolvedNpmBuildScript = "build";
103
+ let resolvedNpmPruneAfterBuild = true;
88
104
  let resolvedMinOpenclawVersion = "";
89
- let resolvedMinOpenvikingVersion = "";
90
- let resolvedPluginReleaseId = "";
105
+ let resolvedMinOpenvikingVersion = "";
106
+ let resolvedPluginReleaseId = "";
107
+ let detectedOpenClawVersion = "";
108
+ let npmPackageTempDir = "";
109
+ let npmPackageExtractDir = "";
91
110
 
92
- let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
111
+ let nonInteractive = false;
93
112
  let langZh = false;
94
113
  let workdirExplicit = false;
95
114
  let upgradePluginOnly = false;
96
115
  let rollbackLastUpgrade = false;
97
- let showCurrentVersion = false;
116
+ let showCurrentVersion = false;
117
+ let uninstallPlugin = false;
118
+ let forceSlotExplicit = false;
119
+ let allowOfflineExplicit = false;
98
120
 
99
121
  const selectedMode = "remote";
122
+ const baseUrlFromEnv = !!process.env.OPENVIKING_BASE_URL;
100
123
  let remoteBaseUrl = (process.env.OPENVIKING_BASE_URL || "http://127.0.0.1:1933").trim();
101
124
  let remoteApiKey = (process.env.OPENVIKING_API_KEY || "").trim();
102
125
  let remoteAgentPrefix = (process.env.OPENVIKING_AGENT_PREFIX || "").trim();
126
+ let remoteAccountId = (process.env.OPENVIKING_ACCOUNT_ID || "").trim();
127
+ let remoteUserId = (process.env.OPENVIKING_USER_ID || "").trim();
128
+ let baseUrlExplicit = baseUrlFromEnv;
103
129
  let upgradeRuntimeConfig = null;
104
130
  let installedUpgradeState = null;
105
131
  let upgradeAudit = null;
@@ -108,8 +134,11 @@ const argv = process.argv.slice(2);
108
134
  for (let i = 0; i < argv.length; i++) {
109
135
  const arg = argv[i];
110
136
  if (arg === "-y" || arg === "--yes") {
111
- installYes = true;
112
- continue;
137
+ err(tr(
138
+ "-y/--yes has been removed. Use --base-url <URL> [--api-key <KEY>] for non-interactive mode.",
139
+ "-y/--yes 已移除。使用 --base-url <URL> [--api-key <KEY>] 进入非交互模式。",
140
+ ));
141
+ process.exit(1);
113
142
  }
114
143
  if (arg === "--zh") {
115
144
  langZh = true;
@@ -127,7 +156,19 @@ for (let i = 0; i < argv.length; i++) {
127
156
  rollbackLastUpgrade = true;
128
157
  continue;
129
158
  }
130
- if (arg === "--workdir") {
159
+ if (arg === "--uninstall" || arg === "--remove") {
160
+ uninstallPlugin = true;
161
+ continue;
162
+ }
163
+ if (arg === "--force-slot") {
164
+ forceSlotExplicit = true;
165
+ continue;
166
+ }
167
+ if (arg === "--allow-offline") {
168
+ allowOfflineExplicit = true;
169
+ continue;
170
+ }
171
+ if (arg === "--workdir") {
131
172
  const workdir = argv[i + 1]?.trim();
132
173
  if (!workdir) {
133
174
  console.error("--workdir requires a path");
@@ -144,59 +185,174 @@ for (let i = 0; i < argv.length; i++) {
144
185
  console.error("--plugin-version requires a value");
145
186
  process.exit(1);
146
187
  }
147
- PLUGIN_VERSION = version;
148
- pluginVersionExplicit = true;
149
- continue;
150
- }
188
+ PLUGIN_VERSION = version;
189
+ pluginVersionExplicit = true;
190
+ continue;
191
+ }
151
192
  if (arg === "--plugin-version") {
152
193
  const version = argv[i + 1]?.trim();
153
194
  if (!version) {
154
195
  console.error("--plugin-version requires a value");
155
196
  process.exit(1);
156
197
  }
157
- PLUGIN_VERSION = version;
158
- pluginVersionExplicit = true;
198
+ PLUGIN_VERSION = version;
199
+ pluginVersionExplicit = true;
200
+ i += 1;
201
+ continue;
202
+ }
203
+ if (arg.startsWith("--github-repo=")) {
204
+ REPO = arg.slice("--github-repo=".length).trim();
205
+ if (!pluginSourceExplicit) {
206
+ pluginSource = "github";
207
+ }
208
+ continue;
209
+ }
210
+ if (arg === "--github-repo") {
211
+ const repo = argv[i + 1]?.trim();
212
+ if (!repo) {
213
+ console.error("--github-repo requires a value (e.g. owner/repo)");
214
+ process.exit(1);
215
+ }
216
+ REPO = repo;
217
+ if (!pluginSourceExplicit) {
218
+ pluginSource = "github";
219
+ }
220
+ i += 1;
221
+ continue;
222
+ }
223
+ if (arg.startsWith("--plugin-source=") || arg.startsWith("--source=")) {
224
+ const value = arg.includes("--plugin-source=")
225
+ ? arg.slice("--plugin-source=".length).trim()
226
+ : arg.slice("--source=".length).trim();
227
+ pluginSource = value.toLowerCase();
228
+ pluginSourceExplicit = true;
229
+ continue;
230
+ }
231
+ if (arg === "--plugin-source" || arg === "--source") {
232
+ const value = argv[i + 1]?.trim();
233
+ if (!value) {
234
+ console.error(`${arg} requires a value (npm or github)`);
235
+ process.exit(1);
236
+ }
237
+ pluginSource = value.toLowerCase();
238
+ pluginSourceExplicit = true;
239
+ i += 1;
240
+ continue;
241
+ }
242
+ if (arg.startsWith("--plugin-package=") || arg.startsWith("--npm-package=")) {
243
+ const value = arg.includes("--plugin-package=")
244
+ ? arg.slice("--plugin-package=".length).trim()
245
+ : arg.slice("--npm-package=".length).trim();
246
+ if (!value) {
247
+ console.error("--plugin-package requires a package name");
248
+ process.exit(1);
249
+ }
250
+ pluginNpmPackage = value;
251
+ continue;
252
+ }
253
+ if (arg === "--plugin-package" || arg === "--npm-package") {
254
+ const value = argv[i + 1]?.trim();
255
+ if (!value) {
256
+ console.error(`${arg} requires a package name`);
257
+ process.exit(1);
258
+ }
259
+ pluginNpmPackage = value;
260
+ i += 1;
261
+ continue;
262
+ }
263
+ if (arg === "--base-url") {
264
+ const val = argv[i + 1]?.trim();
265
+ if (!val) { console.error("--base-url requires a URL"); process.exit(1); }
266
+ remoteBaseUrl = val;
267
+ baseUrlExplicit = true;
159
268
  i += 1;
160
269
  continue;
161
270
  }
162
- if (arg.startsWith("--github-repo=")) {
163
- REPO = arg.slice("--github-repo=".length).trim();
271
+ if (arg.startsWith("--base-url=")) {
272
+ remoteBaseUrl = arg.slice("--base-url=".length).trim();
273
+ baseUrlExplicit = true;
164
274
  continue;
165
275
  }
166
- if (arg === "--github-repo") {
167
- const repo = argv[i + 1]?.trim();
168
- if (!repo) {
169
- console.error("--github-repo requires a value (e.g. owner/repo)");
170
- process.exit(1);
171
- }
172
- REPO = repo;
276
+ if (arg === "--api-key") {
277
+ const val = argv[i + 1]?.trim();
278
+ if (!val) { console.error("--api-key requires a value"); process.exit(1); }
279
+ remoteApiKey = val;
173
280
  i += 1;
174
281
  continue;
175
282
  }
283
+ if (arg.startsWith("--api-key=")) {
284
+ remoteApiKey = arg.slice("--api-key=".length).trim();
285
+ continue;
286
+ }
287
+ if (arg === "--agent-prefix") {
288
+ const val = argv[i + 1]?.trim();
289
+ if (!val) { console.error("--agent-prefix requires a value"); process.exit(1); }
290
+ remoteAgentPrefix = val;
291
+ i += 1;
292
+ continue;
293
+ }
294
+ if (arg.startsWith("--agent-prefix=")) {
295
+ remoteAgentPrefix = arg.slice("--agent-prefix=".length).trim();
296
+ continue;
297
+ }
298
+ if (arg === "--account-id") {
299
+ const val = argv[i + 1]?.trim();
300
+ if (!val) { console.error("--account-id requires a value"); process.exit(1); }
301
+ remoteAccountId = val;
302
+ i += 1;
303
+ continue;
304
+ }
305
+ if (arg.startsWith("--account-id=")) {
306
+ remoteAccountId = arg.slice("--account-id=".length).trim();
307
+ continue;
308
+ }
309
+ if (arg === "--user-id") {
310
+ const val = argv[i + 1]?.trim();
311
+ if (!val) { console.error("--user-id requires a value"); process.exit(1); }
312
+ remoteUserId = val;
313
+ i += 1;
314
+ continue;
315
+ }
316
+ if (arg.startsWith("--user-id=")) {
317
+ remoteUserId = arg.slice("--user-id=".length).trim();
318
+ continue;
319
+ }
176
320
  if (arg === "-h" || arg === "--help") {
177
321
  printHelp();
178
322
  process.exit(0);
179
323
  }
180
324
  }
181
325
 
326
+ nonInteractive = baseUrlExplicit;
327
+
182
328
  function setOpenClawDir(dir) {
183
329
  OPENCLAW_DIR = dir;
184
330
  }
185
331
 
186
332
  function printHelp() {
187
333
  console.log("Usage: node install.js [ OPTIONS ]");
188
- console.log("");
189
- console.log("Options:");
190
- console.log(" --github-repo=OWNER/REPO GitHub repository (default: volcengine/OpenViking)");
191
- console.log(" --plugin-version=TAG Plugin version (Git tag, e.g. v0.2.9, default: latest tag)");
334
+ console.log("");
335
+ console.log("Options:");
336
+ console.log(" --plugin-source=npm|github");
337
+ console.log(" Plugin download source (default: npm)");
338
+ console.log(" --plugin-package=NAME npm plugin package (default: @openviking/openclaw-plugin)");
339
+ console.log(" --github-repo=OWNER/REPO GitHub repository (implies --plugin-source=github unless source is set)");
340
+ console.log(" --plugin-version=VERSION Plugin version (npm version/tag or Git tag; default: npm latest)");
192
341
  console.log(" --workdir PATH OpenClaw config directory (default: ~/.openclaw)");
193
342
  console.log(" --current-version Print installed plugin version and exit");
194
343
  console.log(" --update, --upgrade-plugin");
195
344
  console.log(" Upgrade only the plugin to the requested --plugin-version; keeps existing plugin runtime config");
196
345
  console.log(" --rollback, --rollback-last-upgrade");
197
346
  console.log(" Roll back the last plugin upgrade using the saved audit/backup files");
198
- console.log(" -y, --yes Non-interactive (use defaults)");
199
- console.log(" --zh Chinese prompts");
347
+ console.log(" --uninstall, --remove Uninstall OpenViking plugin from OpenClaw (backup config, remove plugin entries)");
348
+ console.log(" --base-url=URL OpenViking server URL (default: $OPENVIKING_BASE_URL or http://127.0.0.1:1933)");
349
+ console.log(" --api-key=KEY OpenViking API key (default: $OPENVIKING_API_KEY)");
350
+ console.log(" --agent-prefix=PREFIX Agent routing prefix (default: $OPENVIKING_AGENT_PREFIX)");
351
+ console.log(" --account-id=ID Account ID for root API key (default: $OPENVIKING_ACCOUNT_ID)");
352
+ console.log(" --user-id=ID User ID for root API key (default: $OPENVIKING_USER_ID)");
353
+ console.log(" --force-slot Explicitly replace an existing contextEngine slot owner");
354
+ console.log(" --allow-offline Explicitly save config when the OpenViking server is unreachable");
355
+ console.log(" --zh Chinese prompts");
200
356
  console.log(" -h, --help This help");
201
357
  console.log("");
202
358
  console.log("Examples:");
@@ -206,11 +362,11 @@ function printHelp() {
206
362
  console.log(" # Show installed versions");
207
363
  console.log(" node install.js --current-version");
208
364
  console.log("");
209
- console.log(" # Install a specific release version");
210
- console.log(" node install.js --plugin-version=v0.2.9");
211
- console.log("");
212
- console.log(" # Install from a fork repository");
213
- console.log(" node install.js --github-repo=yourname/OpenViking --plugin-version=dev-branch");
365
+ console.log(" # Install a specific release version");
366
+ console.log(" node install.js --plugin-version=2026.5.8");
367
+ console.log("");
368
+ console.log(" # Install from a fork repository");
369
+ console.log(" node install.js --github-repo=yourname/OpenViking --plugin-version=dev-branch");
214
370
  console.log("");
215
371
  console.log(" # Install specific plugin version");
216
372
  console.log(" node install.js --plugin-version=v0.2.8");
@@ -221,8 +377,8 @@ function printHelp() {
221
377
  console.log(" # Roll back the last plugin upgrade");
222
378
  console.log(" node install.js --rollback");
223
379
  console.log("");
224
- console.log("Env: REPO, PLUGIN_VERSION, SKIP_OPENCLAW, NPM_REGISTRY");
225
- }
380
+ console.log("Env: PLUGIN_SOURCE, PLUGIN_NPM_PACKAGE, REPO, PLUGIN_VERSION, SKIP_OPENCLAW, NPM_REGISTRY");
381
+ }
226
382
 
227
383
  function formatCliArg(value) {
228
384
  if (!value) {
@@ -315,15 +471,62 @@ function runCapture(cmd, args, opts = {}) {
315
471
  });
316
472
  }
317
473
 
318
- function question(prompt, defaultValue = "") {
319
- const rl = createInterface({ input: process.stdin, output: process.stdout });
320
- const suffix = defaultValue ? ` [${defaultValue}]` : "";
321
- return new Promise((resolve) => {
322
- rl.question(`${prompt}${suffix}: `, (answer) => {
474
+ function question(prompt, defaultValue = "") {
475
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
476
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
477
+ return new Promise((resolve) => {
478
+ rl.question(`${prompt}${suffix}: `, (answer) => {
323
479
  rl.close();
324
480
  resolve((answer ?? defaultValue).trim() || defaultValue);
325
481
  });
326
- });
482
+ });
483
+ }
484
+
485
+ function isYes(answer) {
486
+ const normalized = String(answer || "").trim().toLowerCase();
487
+ return normalized === "y" || normalized === "yes";
488
+ }
489
+
490
+ function isValidAgentPrefixInput(value) {
491
+ const trimmed = String(value || "").trim();
492
+ return !trimmed || /^[a-zA-Z0-9_-]+$/.test(trimmed);
493
+ }
494
+
495
+ function parseJsonObjectFromOutput(output) {
496
+ const text = String(output || "").trim();
497
+ if (!text) return null;
498
+ try {
499
+ return JSON.parse(text);
500
+ } catch {
501
+ // OpenClaw may print plugin registration logs before --json output.
502
+ }
503
+ for (let index = text.lastIndexOf("{"); index >= 0; index = text.lastIndexOf("{", index - 1)) {
504
+ try {
505
+ const parsed = JSON.parse(text.slice(index).trim());
506
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
507
+ return parsed;
508
+ }
509
+ } catch {
510
+ // Keep scanning earlier braces until the outer JSON object is found.
511
+ }
512
+ }
513
+ return null;
514
+ }
515
+
516
+ async function questionAgentPrefix(defaultValue = "") {
517
+ while (true) {
518
+ const answer = (await question(
519
+ tr("Agent Prefix (optional)", "Agent Prefix(可选)"),
520
+ defaultValue,
521
+ )).trim();
522
+ if (isValidAgentPrefixInput(answer)) {
523
+ return answer;
524
+ }
525
+ warn(tr(
526
+ "Agent Prefix may only contain letters, digits, underscores, and hyphens, or be empty.",
527
+ "Agent Prefix 只能包含字母、数字、下划线和连字符,或留空。",
528
+ ));
529
+ }
327
530
  }
328
531
 
329
532
  function detectOpenClawInstances() {
@@ -349,7 +552,7 @@ async function selectWorkdir() {
349
552
  setOpenClawDir(instances[0]);
350
553
  return;
351
554
  }
352
- if (installYes) return;
555
+ if (nonInteractive) return;
353
556
 
354
557
  console.log("");
355
558
  bold(tr("Found multiple OpenClaw instances:", "发现多个 OpenClaw 实例:"));
@@ -369,10 +572,10 @@ async function selectWorkdir() {
369
572
  }
370
573
 
371
574
  async function collectRemoteConfig() {
372
- if (installYes) return;
575
+ if (nonInteractive) return;
373
576
  remoteBaseUrl = await question(tr("OpenViking server URL", "OpenViking 服务器地址"), remoteBaseUrl);
374
577
  remoteApiKey = await question(tr("API Key (optional)", "API Key(可选)"), remoteApiKey);
375
- remoteAgentPrefix = await question(tr("Agent Prefix (optional)", "Agent Prefix(可选)"), remoteAgentPrefix);
578
+ remoteAgentPrefix = await questionAgentPrefix(remoteAgentPrefix);
376
579
  }
377
580
 
378
581
  async function checkOpenClaw() {
@@ -414,6 +617,51 @@ function versionGte(v1, v2) {
414
617
  return a3 >= b3;
415
618
  }
416
619
 
620
+ function parseOpenClawPolicyVersion(value) {
621
+ const parts = String(value || "")
622
+ .match(/\d+/g)
623
+ ?.map((part) => Number.parseInt(part, 10) || 0) || [];
624
+ if (parts.length === 0) return [0, 0, 0];
625
+ if (parts[0] >= 2000) {
626
+ return [parts[0], parts[1] || 0, parts[2] || 0];
627
+ }
628
+ return [OPENCLAW_SHORT_VERSION_YEAR, parts[0] || 0, parts[1] || 0];
629
+ }
630
+
631
+ function openClawPolicyVersionGte(v1, v2) {
632
+ const a = parseOpenClawPolicyVersion(v1);
633
+ const b = parseOpenClawPolicyVersion(v2);
634
+ for (let i = 0; i < 3; i++) {
635
+ if (a[i] !== b[i]) return a[i] > b[i];
636
+ }
637
+ return true;
638
+ }
639
+
640
+ function applyOpenClawBuildPolicy(openClawVersion) {
641
+ if (!resolvedNpmBuild || !resolvedNpmBuildMinOpenclawVersion) {
642
+ return;
643
+ }
644
+ if (!openClawVersion || openClawVersion === "0.0.0") {
645
+ warn(tr(
646
+ "Could not determine OpenClaw version; keeping plugin source build enabled.",
647
+ "无法确定 OpenClaw 版本,保持插件源码构建开启。",
648
+ ));
649
+ return;
650
+ }
651
+ if (openClawPolicyVersionGte(openClawVersion, resolvedNpmBuildMinOpenclawVersion)) {
652
+ info(tr(
653
+ `OpenClaw ${openClawVersion} requires plugin source build (>= ${resolvedNpmBuildMinOpenclawVersion})`,
654
+ `OpenClaw ${openClawVersion} 需要插件源码构建(>= ${resolvedNpmBuildMinOpenclawVersion})`,
655
+ ));
656
+ return;
657
+ }
658
+ resolvedNpmBuild = false;
659
+ info(tr(
660
+ `OpenClaw ${openClawVersion} is below ${resolvedNpmBuildMinOpenclawVersion}; skipping plugin source build`,
661
+ `OpenClaw ${openClawVersion} 低于 ${resolvedNpmBuildMinOpenclawVersion},跳过插件源码构建`,
662
+ ));
663
+ }
664
+
417
665
  function isSemverLike(value) {
418
666
  return /^v?\d+(\.\d+){1,2}$/.test(value);
419
667
  }
@@ -431,16 +679,46 @@ if (upgradePluginOnly && rollbackLastUpgrade) {
431
679
  process.exit(1);
432
680
  }
433
681
 
434
- // Detect OpenClaw version
435
- async function detectOpenClawVersion() {
682
+ if (uninstallPlugin && (upgradePluginOnly || rollbackLastUpgrade)) {
683
+ console.error("--uninstall cannot be used with --upgrade-plugin or --rollback");
684
+ process.exit(1);
685
+ }
686
+
687
+ if (!["npm", "github"].includes(pluginSource)) {
688
+ console.error("--plugin-source must be either npm or github");
689
+ process.exit(1);
690
+ }
691
+
692
+ function looksLikeLegacyGitHubRef(value) {
693
+ const ref = String(value || "").trim();
694
+ if (!ref) return false;
695
+ if (/^v\d+(\.\d+){1,2}([-.].*)?$/i.test(ref)) return true;
696
+ if (["main", "master"].includes(ref.toLowerCase())) return true;
697
+ return false;
698
+ }
699
+
700
+ if (!pluginSourceExplicit && pluginVersionExplicit && looksLikeLegacyGitHubRef(PLUGIN_VERSION)) {
701
+ pluginSource = "github";
702
+ }
703
+
704
+ // Detect OpenClaw version
705
+ async function detectOpenClawVersion() {
706
+ if (detectedOpenClawVersion) {
707
+ return detectedOpenClawVersion;
708
+ }
436
709
  try {
437
710
  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];
711
+ const output = `${result.out || ""}\n${result.err || ""}`;
712
+ if (result.code === 0 && output) {
713
+ const match = output.match(/\d+\.\d+(\.\d+)?/);
714
+ if (match) {
715
+ detectedOpenClawVersion = match[0];
716
+ return detectedOpenClawVersion;
717
+ }
441
718
  }
442
719
  } catch {}
443
- return "0.0.0";
720
+ detectedOpenClawVersion = "0.0.0";
721
+ return detectedOpenClawVersion;
444
722
  }
445
723
 
446
724
  // Try to fetch a URL, return response text or null
@@ -476,10 +754,10 @@ function compareSemverDesc(a, b) {
476
754
  return versionGte(a, b) ? -1 : 1;
477
755
  }
478
756
 
479
- function pickLatestPluginTag(tagNames) {
480
- const normalized = tagNames
481
- .map((tag) => String(tag ?? "").trim())
482
- .filter(Boolean);
757
+ function pickLatestPluginTag(tagNames) {
758
+ const normalized = tagNames
759
+ .map((tag) => String(tag ?? "").trim())
760
+ .filter(Boolean);
483
761
 
484
762
  const semverTags = normalized
485
763
  .filter((tag) => isSemverLike(tag))
@@ -488,9 +766,118 @@ function pickLatestPluginTag(tagNames) {
488
766
  if (semverTags.length > 0) {
489
767
  return semverTags[0];
490
768
  }
491
-
492
- return normalized[0] || "";
493
- }
769
+
770
+ return normalized[0] || "";
771
+ }
772
+
773
+ function npmPackageSpec(version = PLUGIN_VERSION) {
774
+ return version ? `${pluginNpmPackage}@${version}` : pluginNpmPackage;
775
+ }
776
+
777
+ function parseNpmJsonOutput(output) {
778
+ const text = String(output || "").trim();
779
+ if (!text) return null;
780
+ try {
781
+ return JSON.parse(text);
782
+ } catch {
783
+ const firstArray = text.indexOf("[");
784
+ const lastArray = text.lastIndexOf("]");
785
+ if (firstArray >= 0 && lastArray > firstArray) {
786
+ try {
787
+ return JSON.parse(text.slice(firstArray, lastArray + 1));
788
+ } catch {}
789
+ }
790
+ const firstObject = text.indexOf("{");
791
+ const lastObject = text.lastIndexOf("}");
792
+ if (firstObject >= 0 && lastObject > firstObject) {
793
+ try {
794
+ return JSON.parse(text.slice(firstObject, lastObject + 1));
795
+ } catch {}
796
+ }
797
+ }
798
+ return null;
799
+ }
800
+
801
+ async function resolveDefaultPluginVersionFromNpm() {
802
+ info(tr(
803
+ `No plugin version specified; resolving latest npm version from ${pluginNpmPackage}...`,
804
+ `No plugin version specified; resolving latest npm version from ${pluginNpmPackage}...`,
805
+ ));
806
+
807
+ const result = await runCapture("npm", [
808
+ "view",
809
+ `${pluginNpmPackage}@latest`,
810
+ "version",
811
+ "--json",
812
+ "--registry",
813
+ NPM_REGISTRY,
814
+ ], { shell: IS_WIN });
815
+
816
+ if (result.code === 0) {
817
+ const parsed = parseNpmJsonOutput(result.out);
818
+ const version = typeof parsed === "string" ? parsed : String(result.out || "").trim().replace(/^"|"$/g, "");
819
+ if (version) {
820
+ PLUGIN_VERSION = version;
821
+ info(tr(
822
+ `Resolved default plugin version to npm latest: ${PLUGIN_VERSION}`,
823
+ `Resolved default plugin version to npm latest: ${PLUGIN_VERSION}`,
824
+ ));
825
+ return true;
826
+ }
827
+ }
828
+
829
+ warn(tr(
830
+ `Could not resolve npm latest for ${pluginNpmPackage}${result.err ? `: ${result.err}` : ""}`,
831
+ `Could not resolve npm latest for ${pluginNpmPackage}${result.err ? `: ${result.err}` : ""}`,
832
+ ));
833
+ return false;
834
+ }
835
+
836
+ async function ensureNpmPackageExtracted() {
837
+ if (npmPackageExtractDir && existsSync(npmPackageExtractDir)) {
838
+ return npmPackageExtractDir;
839
+ }
840
+
841
+ npmPackageTempDir = await mkdtemp(join(tmpdir(), "ov-plugin-npm-"));
842
+ info(tr(
843
+ `Downloading plugin package from npm: ${npmPackageSpec()}`,
844
+ `Downloading plugin package from npm: ${npmPackageSpec()}`,
845
+ ));
846
+
847
+ const packResult = await runCapture("npm", [
848
+ "pack",
849
+ npmPackageSpec(),
850
+ "--pack-destination",
851
+ npmPackageTempDir,
852
+ "--json",
853
+ "--registry",
854
+ NPM_REGISTRY,
855
+ ], { shell: IS_WIN });
856
+
857
+ if (packResult.code !== 0) {
858
+ throw new Error(`npm pack failed for ${npmPackageSpec()}${packResult.err ? `: ${packResult.err}` : ""}`);
859
+ }
860
+
861
+ const parsed = parseNpmJsonOutput(packResult.out);
862
+ const first = Array.isArray(parsed) ? parsed[0] : parsed;
863
+ const filename = first?.filename || readdirSync(npmPackageTempDir).find((name) => name.endsWith(".tgz"));
864
+ if (!filename) {
865
+ throw new Error(`npm pack did not produce a tarball for ${npmPackageSpec()}`);
866
+ }
867
+
868
+ const tarballPath = join(npmPackageTempDir, filename);
869
+ const extractRoot = join(npmPackageTempDir, "extract");
870
+ await mkdir(extractRoot, { recursive: true });
871
+ await run("tar", ["-xzf", tarballPath, "-C", extractRoot], { silent: true, shell: IS_WIN });
872
+
873
+ const packageDir = join(extractRoot, "package");
874
+ if (!existsSync(packageDir)) {
875
+ throw new Error(`npm package ${npmPackageSpec()} did not contain the expected package directory`);
876
+ }
877
+
878
+ npmPackageExtractDir = packageDir;
879
+ return npmPackageExtractDir;
880
+ }
494
881
 
495
882
  function parseGitLsRemoteTags(output) {
496
883
  return String(output ?? "")
@@ -502,13 +889,23 @@ function parseGitLsRemoteTags(output) {
502
889
  .filter(Boolean);
503
890
  }
504
891
 
505
- async function resolveDefaultPluginVersion() {
506
- if (PLUGIN_VERSION) {
507
- return;
508
- }
509
-
510
- info(tr(
511
- `No plugin version specified; resolving latest tag from ${REPO}...`,
892
+ async function resolveDefaultPluginVersion() {
893
+ if (PLUGIN_VERSION) {
894
+ return;
895
+ }
896
+
897
+ if (pluginSource === "npm") {
898
+ if (await resolveDefaultPluginVersionFromNpm()) {
899
+ return;
900
+ }
901
+ warn(tr(
902
+ "Falling back to GitHub tag resolution.",
903
+ "Falling back to GitHub tag resolution.",
904
+ ));
905
+ }
906
+
907
+ info(tr(
908
+ `No plugin version specified; resolving latest tag from ${REPO}...`,
512
909
  `未指定插件版本,正在解析 ${REPO} 的最新 tag...`,
513
910
  ));
514
911
 
@@ -580,15 +977,126 @@ async function resolveDefaultPluginVersion() {
580
977
  if (failures.length > 0) {
581
978
  warn(failures.join(" | "));
582
979
  }
583
- process.exit(1);
584
- }
585
-
586
- // Resolve plugin configuration from manifest or fallback
587
- async function resolvePluginConfig() {
588
- const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
980
+ process.exit(1);
981
+ }
982
+
983
+ function applyManifestConfig(manifestData) {
984
+ resolvedPluginId = manifestData.plugin?.id || "";
985
+ resolvedPluginKind = manifestData.plugin?.kind || "";
986
+ resolvedPluginSlot = manifestData.plugin?.slot || "";
987
+ resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
988
+ resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
989
+ resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
990
+ const npmConfig = manifestData.npm && typeof manifestData.npm === "object"
991
+ ? manifestData.npm
992
+ : {};
993
+ resolvedNpmOmitDev = npmConfig.omitDev !== false;
994
+ resolvedNpmBuild = npmConfig.build === true || npmConfig.buildFromSource === true;
995
+ resolvedNpmBuildMinOpenclawVersion =
996
+ typeof npmConfig.buildMinOpenclawVersion === "string" && npmConfig.buildMinOpenclawVersion.trim()
997
+ ? npmConfig.buildMinOpenclawVersion.trim()
998
+ : DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
999
+ resolvedNpmBuildScript = typeof npmConfig.buildScript === "string" && npmConfig.buildScript.trim()
1000
+ ? npmConfig.buildScript.trim()
1001
+ : "build";
1002
+ resolvedNpmPruneAfterBuild = npmConfig.pruneAfterBuild !== false;
1003
+ resolvedFilesRequired = manifestData.files?.required || [];
1004
+ resolvedFilesOptional = manifestData.files?.optional || [];
1005
+ }
1006
+
1007
+ function hasPrebuiltRuntimeOutputs(packageDir) {
1008
+ return existsSync(join(packageDir, "dist", "index.js"));
1009
+ }
1010
+
1011
+ async function resolvePluginConfigFromNpm() {
1012
+ info(tr(
1013
+ `Resolving plugin configuration from npm package: ${npmPackageSpec()}`,
1014
+ `Resolving plugin configuration from npm package: ${npmPackageSpec()}`,
1015
+ ));
1016
+
1017
+ const packageDir = await ensureNpmPackageExtracted();
1018
+ const manifestPath = join(packageDir, "install-manifest.json");
1019
+ const packageJsonPath = join(packageDir, "package.json");
1020
+ let manifestData = null;
1021
+ let packageJson = null;
1022
+
1023
+ if (existsSync(packageJsonPath)) {
1024
+ try {
1025
+ packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
1026
+ resolvedPluginReleaseId = packageJson.version || "";
1027
+ } catch {}
1028
+ }
1029
+
1030
+ if (existsSync(manifestPath)) {
1031
+ try {
1032
+ manifestData = JSON.parse(await readFile(manifestPath, "utf8"));
1033
+ info(tr("Found manifest in npm package", "Found manifest in npm package"));
1034
+ } catch {}
1035
+ }
1036
+
1037
+ resolvedPluginDir = ".";
1038
+ if (manifestData) {
1039
+ applyManifestConfig(manifestData);
1040
+ } else {
1041
+ const pkgName = packageJson?.name || "";
1042
+ const fallback = pkgName && pkgName !== DEFAULT_PLUGIN_NPM_PACKAGE ? FALLBACK_LEGACY : FALLBACK_CURRENT;
1043
+ resolvedPluginId = fallback.id;
1044
+ resolvedPluginKind = fallback.kind;
1045
+ resolvedPluginSlot = fallback.slot;
1046
+ resolvedFilesRequired = fallback.required;
1047
+ resolvedFilesOptional = fallback.optional;
1048
+ resolvedNpmOmitDev = true;
1049
+ resolvedNpmBuild = false;
1050
+ resolvedNpmBuildMinOpenclawVersion = DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
1051
+ resolvedNpmBuildScript = "build";
1052
+ resolvedNpmPruneAfterBuild = true;
1053
+ resolvedMinOpenclawVersion = (packageJson?.engines?.openclaw || "").replace(/^>=?\s*/, "").trim()
1054
+ || fallback.minOpenclawVersion
1055
+ || "2026.3.7";
1056
+ resolvedMinOpenvikingVersion = "";
1057
+ }
1058
+
1059
+ if (hasPrebuiltRuntimeOutputs(packageDir)) {
1060
+ resolvedNpmBuild = false;
1061
+ info(tr(
1062
+ "npm package contains prebuilt runtime output; skipping source build.",
1063
+ "npm package contains prebuilt runtime output; skipping source build.",
1064
+ ));
1065
+ }
1066
+
1067
+ PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId || "openviking");
1068
+ info(tr(`Plugin: ${resolvedPluginId} (${resolvedPluginKind})`, `Plugin: ${resolvedPluginId} (${resolvedPluginKind})`));
1069
+ }
1070
+
1071
+ // Resolve plugin configuration from manifest or fallback
1072
+ async function resolvePluginConfig() {
1073
+ if (pluginSource === "npm") {
1074
+ try {
1075
+ await resolvePluginConfigFromNpm();
1076
+ return;
1077
+ } catch (error) {
1078
+ warn(tr(
1079
+ `npm plugin resolution failed: ${error?.message || error}`,
1080
+ `npm plugin resolution failed: ${error?.message || error}`,
1081
+ ));
1082
+ warn(tr(
1083
+ "Falling back to GitHub plugin download.",
1084
+ "Falling back to GitHub plugin download.",
1085
+ ));
1086
+ pluginSource = "github";
1087
+ }
1088
+ }
1089
+
1090
+ const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
589
1091
 
590
1092
  info(tr(`Resolving plugin configuration for version: ${PLUGIN_VERSION}`, `正在解析插件配置,版本: ${PLUGIN_VERSION}`));
591
1093
 
1094
+ resolvedNpmOmitDev = true;
1095
+ resolvedNpmBuild = false;
1096
+ resolvedNpmBuildMinOpenclawVersion = DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
1097
+ resolvedNpmBuildScript = "build";
1098
+ resolvedNpmPruneAfterBuild = true;
1099
+
592
1100
  let pluginDir = "";
593
1101
  let manifestData = null;
594
1102
 
@@ -630,7 +1138,19 @@ async function resolvePluginConfig() {
630
1138
  resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
631
1139
  resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
632
1140
  resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
633
- resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
1141
+ const npmConfig = manifestData.npm && typeof manifestData.npm === "object"
1142
+ ? manifestData.npm
1143
+ : {};
1144
+ resolvedNpmOmitDev = npmConfig.omitDev !== false;
1145
+ resolvedNpmBuild = npmConfig.build === true || npmConfig.buildFromSource === true;
1146
+ resolvedNpmBuildMinOpenclawVersion =
1147
+ typeof npmConfig.buildMinOpenclawVersion === "string" && npmConfig.buildMinOpenclawVersion.trim()
1148
+ ? npmConfig.buildMinOpenclawVersion.trim()
1149
+ : DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
1150
+ resolvedNpmBuildScript = typeof npmConfig.buildScript === "string" && npmConfig.buildScript.trim()
1151
+ ? npmConfig.buildScript.trim()
1152
+ : "build";
1153
+ resolvedNpmPruneAfterBuild = npmConfig.pruneAfterBuild !== false;
634
1154
  resolvedFilesRequired = manifestData.files?.required || [];
635
1155
  resolvedFilesOptional = manifestData.files?.optional || [];
636
1156
  } else {
@@ -665,6 +1185,10 @@ async function resolvePluginConfig() {
665
1185
  resolvedFilesRequired = fallback.required;
666
1186
  resolvedFilesOptional = fallback.optional;
667
1187
  resolvedNpmOmitDev = true;
1188
+ resolvedNpmBuild = false;
1189
+ resolvedNpmBuildMinOpenclawVersion = DEFAULT_NPM_BUILD_MIN_OPENCLAW_VERSION;
1190
+ resolvedNpmBuildScript = "build";
1191
+ resolvedNpmPruneAfterBuild = true;
668
1192
 
669
1193
  // If no compatVer from package.json, try main branch manifest
670
1194
  if (!compatVer && PLUGIN_VERSION !== "main") {
@@ -681,7 +1205,7 @@ async function resolvePluginConfig() {
681
1205
  }
682
1206
  }
683
1207
 
684
- resolvedMinOpenclawVersion = compatVer || "2026.3.7";
1208
+ resolvedMinOpenclawVersion = compatVer || fallback.minOpenclawVersion || "2026.3.7";
685
1209
  resolvedMinOpenvikingVersion = "";
686
1210
  }
687
1211
 
@@ -699,6 +1223,7 @@ async function checkOpenClawCompatibility() {
699
1223
 
700
1224
  const ocVersion = await detectOpenClawVersion();
701
1225
  info(tr(`Detected OpenClaw version: ${ocVersion}`, `检测到 OpenClaw 版本: ${ocVersion}`));
1226
+ applyOpenClawBuildPolicy(ocVersion);
702
1227
 
703
1228
  // If no minimum version required, pass
704
1229
  if (!resolvedMinOpenclawVersion) {
@@ -711,7 +1236,7 @@ async function checkOpenClawCompatibility() {
711
1236
  }
712
1237
 
713
1238
  // Check compatibility
714
- if (!versionGte(ocVersion, resolvedMinOpenclawVersion)) {
1239
+ if (!openClawPolicyVersionGte(ocVersion, resolvedMinOpenclawVersion)) {
715
1240
  err(tr(
716
1241
  `OpenClaw ${ocVersion} does not support this plugin (requires >= ${resolvedMinOpenclawVersion})`,
717
1242
  `OpenClaw ${ocVersion} 不支持此插件(需要 >= ${resolvedMinOpenclawVersion})`
@@ -875,11 +1400,17 @@ function extractRuntimeConfigFromPluginEntry(entryConfig) {
875
1400
  runtime.apiKey = entryConfig.apiKey;
876
1401
  }
877
1402
  const prefix = entryConfig.agent_prefix || entryConfig.agentId;
878
- if (typeof prefix === "string" && prefix.trim()) {
879
- runtime.agent_prefix = prefix.trim();
880
- }
881
- return runtime;
882
- }
1403
+ if (typeof prefix === "string" && prefix.trim()) {
1404
+ runtime.agent_prefix = prefix.trim();
1405
+ }
1406
+ if (typeof entryConfig.accountId === "string" && entryConfig.accountId.trim()) {
1407
+ runtime.accountId = entryConfig.accountId.trim();
1408
+ }
1409
+ if (typeof entryConfig.userId === "string" && entryConfig.userId.trim()) {
1410
+ runtime.userId = entryConfig.userId.trim();
1411
+ }
1412
+ return runtime;
1413
+ }
883
1414
 
884
1415
  async function backupOpenClawConfig(configPath) {
885
1416
  await mkdir(getUpgradeAuditDir(), { recursive: true });
@@ -1169,10 +1700,12 @@ async function prepareStrongPluginUpgrade() {
1169
1700
  `检测到已安装 OpenViking 插件状态: ${installedState.generation}`,
1170
1701
  ),
1171
1702
  );
1172
- remoteBaseUrl = upgradeRuntimeConfig.baseUrl || remoteBaseUrl;
1173
- remoteApiKey = upgradeRuntimeConfig.apiKey || "";
1174
- remoteAgentPrefix = upgradeRuntimeConfig.agent_prefix || "";
1175
- info(tr(`Upgrade runtime mode: ${selectedMode} (remote OpenViking server)`, `升级运行模式: ${selectedMode}(远程 OpenViking 服务)`));
1703
+ remoteBaseUrl = upgradeRuntimeConfig.baseUrl || remoteBaseUrl;
1704
+ remoteApiKey = upgradeRuntimeConfig.apiKey || "";
1705
+ remoteAgentPrefix = upgradeRuntimeConfig.agent_prefix || "";
1706
+ remoteAccountId = upgradeRuntimeConfig.accountId || "";
1707
+ remoteUserId = upgradeRuntimeConfig.userId || "";
1708
+ info(tr(`Upgrade runtime mode: ${selectedMode} (remote OpenViking server)`, `升级运行模式: ${selectedMode}(远程 OpenViking 服务)`));
1176
1709
 
1177
1710
  info(tr(`Upgrade path: ${fromVersion} -> ${toVersion}`, `升级路径: ${fromVersion} -> ${toVersion}`));
1178
1711
 
@@ -1270,9 +1803,134 @@ async function downloadPluginFile(destDir, fileName, url, required, index, total
1270
1803
  process.exit(1);
1271
1804
  }
1272
1805
 
1273
- async function downloadPlugin(destDir) {
1274
- const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
1275
- const pluginDir = resolvedPluginDir;
1806
+ function runtimeOutputCandidatesForEntry(entry) {
1807
+ const normalized = String(entry || "").replace(/\\/g, "/").replace(/^\.\//, "");
1808
+ if (!normalized.endsWith(".ts")) {
1809
+ return [];
1810
+ }
1811
+ const withoutExt = normalized.slice(0, -3);
1812
+ return [
1813
+ `dist/${withoutExt}.js`,
1814
+ `dist/${withoutExt}.mjs`,
1815
+ `dist/${withoutExt}.cjs`,
1816
+ `${withoutExt}.js`,
1817
+ `${withoutExt}.mjs`,
1818
+ `${withoutExt}.cjs`,
1819
+ ];
1820
+ }
1821
+
1822
+ async function assertBuiltRuntimeOutputs(destDir) {
1823
+ let pkg = null;
1824
+ try {
1825
+ pkg = JSON.parse(await readFile(join(destDir, "package.json"), "utf8"));
1826
+ } catch {
1827
+ return;
1828
+ }
1829
+
1830
+ const entries = [];
1831
+ const extensions = pkg?.openclaw?.extensions;
1832
+ if (Array.isArray(extensions)) {
1833
+ for (const entry of extensions) {
1834
+ if (typeof entry === "string") entries.push(entry);
1835
+ }
1836
+ }
1837
+ if (typeof pkg?.openclaw?.setupEntry === "string") {
1838
+ entries.push(pkg.openclaw.setupEntry);
1839
+ }
1840
+
1841
+ const missing = [];
1842
+ for (const entry of entries) {
1843
+ const candidates = runtimeOutputCandidatesForEntry(entry);
1844
+ if (candidates.length === 0) continue;
1845
+ const found = candidates.some((candidate) => existsSync(join(destDir, ...candidate.split("/"))));
1846
+ if (!found) {
1847
+ missing.push(`${entry} (expected one of: ${candidates.join(", ")})`);
1848
+ }
1849
+ }
1850
+
1851
+ if (missing.length === 0) {
1852
+ return;
1853
+ }
1854
+
1855
+ err(tr(
1856
+ `Plugin build did not create required runtime output:\n - ${missing.join("\n - ")}`,
1857
+ `插件构建未生成必需的运行时产物:\n - ${missing.join("\n - ")}`,
1858
+ ));
1859
+ process.exit(1);
1860
+ }
1861
+
1862
+ async function installPluginNpmDependencies(destDir) {
1863
+ if (!resolvedNpmBuild) {
1864
+ info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
1865
+ const npmArgs = resolvedNpmOmitDev
1866
+ ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1867
+ : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1868
+ await run("npm", npmArgs, { cwd: destDir, silent: false });
1869
+ return;
1870
+ }
1871
+
1872
+ info(tr(
1873
+ "Installing plugin npm dependencies for source build...",
1874
+ "正在安装插件源码构建所需的 npm 依赖...",
1875
+ ));
1876
+ await run("npm", [
1877
+ "install",
1878
+ "--include=dev",
1879
+ "--no-audit",
1880
+ "--no-fund",
1881
+ "--registry",
1882
+ NPM_REGISTRY,
1883
+ ], { cwd: destDir, silent: false });
1884
+
1885
+ info(tr(
1886
+ `Building plugin runtime output with npm run ${resolvedNpmBuildScript}...`,
1887
+ `正在执行 npm run ${resolvedNpmBuildScript} 构建插件运行时产物...`,
1888
+ ));
1889
+ await run("npm", ["run", resolvedNpmBuildScript], { cwd: destDir, silent: false });
1890
+ await assertBuiltRuntimeOutputs(destDir);
1891
+
1892
+ if (resolvedNpmOmitDev && resolvedNpmPruneAfterBuild) {
1893
+ info(tr("Pruning plugin dev dependencies...", "正在裁剪插件开发依赖..."));
1894
+ await run("npm", [
1895
+ "prune",
1896
+ "--omit=dev",
1897
+ "--no-audit",
1898
+ "--no-fund",
1899
+ ], { cwd: destDir, silent: false });
1900
+ }
1901
+ }
1902
+
1903
+ async function copyNpmPackageToDest(destDir) {
1904
+ const packageDir = await ensureNpmPackageExtracted();
1905
+ await mkdir(destDir, { recursive: true });
1906
+ const entries = readdirSync(packageDir, { withFileTypes: true });
1907
+ for (const entry of entries) {
1908
+ await cp(join(packageDir, entry.name), join(destDir, entry.name), { recursive: true, force: true });
1909
+ }
1910
+ }
1911
+
1912
+ async function cleanupNpmPackageTemp() {
1913
+ if (!npmPackageTempDir) return;
1914
+ await rm(npmPackageTempDir, { recursive: true, force: true });
1915
+ npmPackageTempDir = "";
1916
+ npmPackageExtractDir = "";
1917
+ }
1918
+
1919
+ async function downloadPlugin(destDir) {
1920
+ if (pluginSource === "npm") {
1921
+ await mkdir(destDir, { recursive: true });
1922
+ info(tr(
1923
+ `Installing plugin from npm package ${npmPackageSpec()}...`,
1924
+ `Installing plugin from npm package ${npmPackageSpec()}...`,
1925
+ ));
1926
+ await copyNpmPackageToDest(destDir);
1927
+ await installPluginNpmDependencies(destDir);
1928
+ info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `Plugin deployed: ${PLUGIN_DEST}`));
1929
+ return;
1930
+ }
1931
+
1932
+ const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
1933
+ const pluginDir = resolvedPluginDir;
1276
1934
  const total = resolvedFilesRequired.length + resolvedFilesOptional.length;
1277
1935
 
1278
1936
  await mkdir(destDir, { recursive: true });
@@ -1296,12 +1954,7 @@ async function downloadPlugin(destDir) {
1296
1954
  await downloadPluginFile(destDir, name, url, false, i, total);
1297
1955
  }
1298
1956
 
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 });
1957
+ await installPluginNpmDependencies(destDir);
1305
1958
  info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
1306
1959
  }
1307
1960
 
@@ -1334,16 +1987,18 @@ async function finalizePluginDeployment(stagingDir) {
1334
1987
  return info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
1335
1988
  }
1336
1989
 
1337
- async function deployPluginFromRemote() {
1338
- const stagingDir = await createPluginStagingDir();
1339
- try {
1340
- await downloadPlugin(stagingDir);
1341
- await finalizePluginDeployment(stagingDir);
1342
- } catch (error) {
1343
- await rm(stagingDir, { recursive: true, force: true });
1344
- throw error;
1345
- }
1346
- }
1990
+ async function deployPluginFromRemote() {
1991
+ const stagingDir = await createPluginStagingDir();
1992
+ try {
1993
+ await downloadPlugin(stagingDir);
1994
+ await finalizePluginDeployment(stagingDir);
1995
+ } catch (error) {
1996
+ await rm(stagingDir, { recursive: true, force: true });
1997
+ throw error;
1998
+ } finally {
1999
+ await cleanupNpmPackageTemp();
2000
+ }
2001
+ }
1347
2002
 
1348
2003
  /** Same as INSTALL*.md manual cleanup: stale entries block `plugins.slots.*` validation after reinstall. */
1349
2004
  function resolvedPluginSlotFallback() {
@@ -1407,6 +2062,111 @@ async function scrubStaleOpenClawPluginRegistration() {
1407
2062
  await rename(tmp, configPath);
1408
2063
  }
1409
2064
 
2065
+ async function cleanupConflictingPluginVariants() {
2066
+ const configPath = getOpenClawConfigPath();
2067
+ if (!existsSync(configPath)) return;
2068
+ let cfg;
2069
+ try {
2070
+ cfg = JSON.parse(await readFile(configPath, "utf8"));
2071
+ } catch { return; }
2072
+ if (!cfg.plugins) return;
2073
+ const p = cfg.plugins;
2074
+ let changed = false;
2075
+ for (const variant of PLUGIN_VARIANTS) {
2076
+ if (variant.id === resolvedPluginId) continue;
2077
+ if (p.entries && Object.prototype.hasOwnProperty.call(p.entries, variant.id)) {
2078
+ info(tr(`Removing conflicting plugin variant: ${variant.id}`, `正在移除冲突的插件变体: ${variant.id}`));
2079
+ delete p.entries[variant.id];
2080
+ changed = true;
2081
+ }
2082
+ if (Array.isArray(p.allow)) {
2083
+ const next = p.allow.filter((id) => id !== variant.id);
2084
+ if (next.length !== p.allow.length) {
2085
+ p.allow = next;
2086
+ changed = true;
2087
+ }
2088
+ }
2089
+ if (p.installs && Object.prototype.hasOwnProperty.call(p.installs, variant.id)) {
2090
+ delete p.installs[variant.id];
2091
+ changed = true;
2092
+ }
2093
+ if (p.slots && p.slots[variant.slot] === variant.id) {
2094
+ p.slots[variant.slot] = variant.slotFallback || "none";
2095
+ changed = true;
2096
+ }
2097
+ if (p.load && Array.isArray(p.load.paths)) {
2098
+ const norm = (s) => String(s).replace(/\\/g, "/");
2099
+ const extNeedle = `/extensions/${variant.id}`;
2100
+ const next = p.load.paths.filter((path) => {
2101
+ if (typeof path !== "string") return true;
2102
+ return !norm(path).includes(extNeedle);
2103
+ });
2104
+ if (next.length !== p.load.paths.length) {
2105
+ p.load.paths = next;
2106
+ changed = true;
2107
+ }
2108
+ }
2109
+ const variantDir = join(OPENCLAW_DIR, "extensions", variant.id);
2110
+ if (existsSync(variantDir)) {
2111
+ info(tr(`Removing conflicting plugin directory: ${variantDir}`, `正在移除冲突的插件目录: ${variantDir}`));
2112
+ await rm(variantDir, { recursive: true, force: true });
2113
+ }
2114
+ }
2115
+ if (!changed) return;
2116
+ const out = JSON.stringify(cfg, null, 2) + "\n";
2117
+ const tmp = `${configPath}.ov-install-tmp.${process.pid}`;
2118
+ await writeFile(tmp, out, "utf8");
2119
+ await rename(tmp, configPath);
2120
+ info(tr("Conflicting plugin variants cleaned up", "冲突的插件变体已清理"));
2121
+ }
2122
+
2123
+ function normalizeOpenClawLoadPath(filePath) {
2124
+ return String(filePath || "")
2125
+ .replace(/\\/g, "/")
2126
+ .replace(/\/+$/g, "");
2127
+ }
2128
+
2129
+ async function ensureOpenClawPluginLoadPath() {
2130
+ const configPath = getOpenClawConfigPath();
2131
+ let cfg = {};
2132
+ if (existsSync(configPath)) {
2133
+ try {
2134
+ cfg = JSON.parse(await readFile(configPath, "utf8"));
2135
+ } catch {
2136
+ return;
2137
+ }
2138
+ }
2139
+
2140
+ const pluginPath = PLUGIN_DEST;
2141
+ const normalizedPluginPath = normalizeOpenClawLoadPath(pluginPath);
2142
+ const plugins = cfg.plugins && typeof cfg.plugins === "object" && !Array.isArray(cfg.plugins)
2143
+ ? cfg.plugins
2144
+ : {};
2145
+ const load = plugins.load && typeof plugins.load === "object" && !Array.isArray(plugins.load)
2146
+ ? plugins.load
2147
+ : {};
2148
+ const paths = Array.isArray(load.paths) ? load.paths : [];
2149
+ if (paths.some((item) => normalizeOpenClawLoadPath(item) === normalizedPluginPath)) {
2150
+ return;
2151
+ }
2152
+
2153
+ const next = {
2154
+ ...cfg,
2155
+ plugins: {
2156
+ ...plugins,
2157
+ load: {
2158
+ ...load,
2159
+ paths: [...paths, pluginPath],
2160
+ },
2161
+ },
2162
+ };
2163
+ await mkdir(dirname(configPath), { recursive: true });
2164
+ const tmp = `${configPath}.ov-install-tmp.${process.pid}`;
2165
+ await writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, "utf8");
2166
+ await rename(tmp, configPath);
2167
+ info(tr(`Added OpenClaw plugin load path: ${pluginPath}`, `已添加 OpenClaw 插件加载路径: ${pluginPath}`));
2168
+ }
2169
+
1410
2170
  async function configureOpenClawPlugin({
1411
2171
  preserveExistingConfig = false,
1412
2172
  runtimeConfig = null,
@@ -1447,16 +2207,21 @@ async function configureOpenClawPlugin({
1447
2207
  const p = cfg.plugins;
1448
2208
  if (!p.entries) p.entries = {};
1449
2209
  if (!p.entries[pluginId]) p.entries[pluginId] = {};
2210
+ p.entries[pluginId].enabled = true;
1450
2211
  if (!p.entries[pluginId].config) p.entries[pluginId].config = {};
1451
2212
  if (!Array.isArray(p.allow)) p.allow = [];
1452
2213
  if (!p.allow.includes(pluginId)) p.allow.push(pluginId);
1453
2214
  return cfg;
1454
2215
  };
1455
2216
 
2217
+ await cleanupConflictingPluginVariants();
2218
+
1456
2219
  if (!preserveExistingConfig) {
1457
2220
  await scrubStaleOpenClawPluginRegistration();
1458
2221
  }
1459
2222
 
2223
+ await ensureOpenClawPluginLoadPath();
2224
+
1460
2225
  // Enable plugin: try CLI first (default path), fall back to direct file for --workdir
1461
2226
  if (!needWorkdirFlag) {
1462
2227
  try {
@@ -1488,7 +2253,7 @@ async function configureOpenClawPlugin({
1488
2253
  `已保留 ${pluginId} 的现有插件运行时配置`,
1489
2254
  ),
1490
2255
  );
1491
- return;
2256
+ return { runtimeConfigOk: true };
1492
2257
  }
1493
2258
 
1494
2259
  const writeConfigDirect = async (pluginConfig, slotValue) => {
@@ -1507,19 +2272,43 @@ async function configureOpenClawPlugin({
1507
2272
  const effectiveRuntimeConfig = runtimeConfig || {
1508
2273
  baseUrl: remoteBaseUrl,
1509
2274
  apiKey: remoteApiKey,
2275
+ agent_prefix: remoteAgentPrefix,
1510
2276
  };
1511
- const pluginConfig = {
2277
+
2278
+ let allowedPropsLegacy = null;
2279
+ try {
2280
+ const manifestPath = join(PLUGIN_DEST, "openclaw.plugin.json");
2281
+ if (existsSync(manifestPath)) {
2282
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
2283
+ const schema = manifest?.configSchema;
2284
+ if (schema?.properties && typeof schema.properties === "object") {
2285
+ allowedPropsLegacy = new Set(Object.keys(schema.properties));
2286
+ }
2287
+ }
2288
+ } catch { /* ignore parse errors */ }
2289
+
2290
+ const agentVal = effectiveRuntimeConfig.agent_prefix || "";
2291
+ const candidates = {
2292
+ mode: "remote",
1512
2293
  baseUrl: effectiveRuntimeConfig.baseUrl || remoteBaseUrl,
1513
2294
  targetUri: "viking://user/memories",
1514
2295
  autoRecall: true,
1515
2296
  autoCapture: true,
2297
+ apiKey: effectiveRuntimeConfig.apiKey || undefined,
2298
+ agentId: agentVal || undefined,
1516
2299
  };
1517
- if (effectiveRuntimeConfig.apiKey) {
1518
- pluginConfig.apiKey = effectiveRuntimeConfig.apiKey;
2300
+
2301
+ const pluginConfig = {};
2302
+ for (const [key, val] of Object.entries(candidates)) {
2303
+ if (val === undefined) continue;
2304
+ if (allowedPropsLegacy && !allowedPropsLegacy.has(key)) continue;
2305
+ pluginConfig[key] = val;
1519
2306
  }
2307
+ if (!pluginConfig.baseUrl) pluginConfig.baseUrl = effectiveRuntimeConfig.baseUrl || remoteBaseUrl;
2308
+
1520
2309
  await writeConfigDirect(pluginConfig, claimSlot ? pluginId : null);
1521
- info(tr("OpenClaw plugin configured (legacy mode)", "OpenClaw 插件配置完成(旧版模式)"));
1522
- return;
2310
+ info(tr("OpenClaw plugin configured (legacy mode, remote)", "OpenClaw 插件配置完成(旧版模式,远程连接)"));
2311
+ return { runtimeConfigOk: true };
1523
2312
  }
1524
2313
 
1525
2314
  // Current (context-engine) plugins: delegate to `openclaw openviking setup --json`
@@ -1529,41 +2318,96 @@ async function configureOpenClawPlugin({
1529
2318
  baseUrl: remoteBaseUrl,
1530
2319
  apiKey: remoteApiKey,
1531
2320
  agent_prefix: remoteAgentPrefix,
2321
+ accountId: remoteAccountId,
2322
+ userId: remoteUserId,
1532
2323
  };
1533
2324
 
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
- }
1551
-
1552
- info(tr(
1553
- "Delegating configuration to: openclaw openviking setup --json",
1554
- "委托配置给: openclaw openviking setup --json",
1555
- ));
1556
-
1557
- const setupResult = await runCapture("openclaw", setupArgs, { env: ocEnv, shell: IS_WIN });
1558
-
1559
- let parsed = null;
2325
+ // Detect if the installed plugin supports `setup --json` by checking the deployed setup.ts
2326
+ let setupJsonSupported = false;
1560
2327
  try {
1561
- parsed = JSON.parse(setupResult.out.trim());
1562
- } catch {
1563
- // If JSON parse fails, fall back to checking exit code
1564
- }
1565
-
1566
- if (parsed) {
2328
+ const setupTsPath = join(PLUGIN_DEST, "commands", "setup.ts");
2329
+ if (existsSync(setupTsPath)) {
2330
+ const setupSrc = await readFile(setupTsPath, "utf8");
2331
+ setupJsonSupported = setupSrc.includes('"--json"') || setupSrc.includes("'--json'");
2332
+ }
2333
+ } catch { /* ignore read errors */ }
2334
+
2335
+ let setupResult = null;
2336
+ let parsed = null;
2337
+ const runSetupJson = async (extraArgs = []) => {
2338
+ const setupArgs = ["openviking", "setup"];
2339
+ setupArgs.push("--base-url", effectiveRuntimeConfig.baseUrl || remoteBaseUrl);
2340
+ setupArgs.push("--json");
2341
+ if (effectiveRuntimeConfig.apiKey) {
2342
+ setupArgs.push("--api-key", effectiveRuntimeConfig.apiKey);
2343
+ }
2344
+ if (effectiveRuntimeConfig.agent_prefix) {
2345
+ setupArgs.push("--agent-prefix", effectiveRuntimeConfig.agent_prefix);
2346
+ }
2347
+ if (effectiveRuntimeConfig.accountId) {
2348
+ setupArgs.push("--account-id", effectiveRuntimeConfig.accountId);
2349
+ }
2350
+ if (effectiveRuntimeConfig.userId) {
2351
+ setupArgs.push("--user-id", effectiveRuntimeConfig.userId);
2352
+ }
2353
+ if (forceSlotExplicit) {
2354
+ setupArgs.push("--force-slot");
2355
+ }
2356
+ if (allowOfflineExplicit) {
2357
+ setupArgs.push("--allow-offline");
2358
+ }
2359
+ setupArgs.push(...extraArgs);
2360
+
2361
+ const result = await runCapture("openclaw", setupArgs, { env: ocEnv, shell: IS_WIN });
2362
+ return {
2363
+ result,
2364
+ parsed: parseJsonObjectFromOutput(`${result.out || ""}\n${result.err || ""}`),
2365
+ };
2366
+ };
2367
+
2368
+ if (setupJsonSupported) {
2369
+ info(tr(
2370
+ "Delegating configuration to: openclaw openviking setup --json",
2371
+ "委托配置给: openclaw openviking setup --json",
2372
+ ));
2373
+
2374
+ ({ result: setupResult, parsed } = await runSetupJson());
2375
+ } else {
2376
+ info(tr(
2377
+ "Installed plugin does not support setup --json, using direct config write",
2378
+ "已安装的插件不支持 setup --json,使用直接配置写入",
2379
+ ));
2380
+ }
2381
+
2382
+ if (parsed && !parsed.success && !nonInteractive) {
2383
+ if (parsed.action === "slot_blocked" && !forceSlotExplicit) {
2384
+ const answer = await question(
2385
+ tr(
2386
+ `contextEngine slot is owned by "${parsed.slot?.previousOwner}". Replace it with OpenViking? (y/N)`,
2387
+ `contextEngine slot is owned by "${parsed.slot?.previousOwner}". Replace it with OpenViking? (y/N)`,
2388
+ ),
2389
+ );
2390
+ if (isYes(answer)) {
2391
+ ({ result: setupResult, parsed } = await runSetupJson(["--force-slot"]));
2392
+ }
2393
+ } else if (
2394
+ typeof parsed.error === "string" &&
2395
+ parsed.error.includes("Server unreachable") &&
2396
+ !allowOfflineExplicit
2397
+ ) {
2398
+ const answer = await question(
2399
+ tr(
2400
+ "OpenViking server is unreachable. Save config offline anyway? (y/N)",
2401
+ "OpenViking server is unreachable. Save config offline anyway? (y/N)",
2402
+ ),
2403
+ );
2404
+ if (isYes(answer)) {
2405
+ ({ result: setupResult, parsed } = await runSetupJson(["--allow-offline"]));
2406
+ }
2407
+ }
2408
+ }
2409
+
2410
+ if (parsed) {
1567
2411
  if (parsed.success) {
1568
2412
  info(tr("OpenClaw plugin configured via setup", "OpenClaw 插件通过 setup 配置完成"));
1569
2413
  if (parsed.health?.ok) {
@@ -1581,46 +2425,84 @@ async function configureOpenClawPlugin({
1581
2425
  if (parsed.slot?.activated) {
1582
2426
  info(tr(`contextEngine slot activated`, `contextEngine slot 已激活`));
1583
2427
  }
1584
- } else {
1585
- // Setup returned success: false
1586
- if (parsed.action === "slot_blocked") {
1587
- warn(tr(
1588
- `Config saved but contextEngine slot is owned by "${parsed.slot?.previousOwner}". Use --force-slot to override.`,
1589
- `配置已保存,但 contextEngine slot "${parsed.slot?.previousOwner}" 占用。使用 --force-slot 覆盖。`,
1590
- ));
1591
- } else {
1592
- err(tr(
1593
- `Setup failed: ${parsed.error || "unknown error"}`,
1594
- `配置失败: ${parsed.error || "未知错误"}`,
1595
- ));
2428
+ } else {
2429
+ // Setup returned success: false
2430
+ const setupError = parsed.error || parsed.action || "unknown error";
2431
+ if (parsed.action === "slot_blocked") {
2432
+ warn(tr(
2433
+ `Config saved but contextEngine slot is owned by "${parsed.slot?.previousOwner}". Use --force-slot to override.`,
2434
+ `配置已保存,但 contextEngine slot 被 "${parsed.slot?.previousOwner}" 占用。使用 --force-slot 覆盖。`,
2435
+ ));
2436
+ } else {
2437
+ err(tr(
2438
+ `Setup failed: ${setupError}`,
2439
+ `配置失败: ${setupError}`,
2440
+ ));
2441
+ }
2442
+ return {
2443
+ runtimeConfigOk: false,
2444
+ error: setupError,
2445
+ };
2446
+ }
2447
+ } else if (setupJsonSupported) {
2448
+ const setupError = setupResult
2449
+ ? `openclaw openviking setup did not return JSON (exit code ${setupResult.code})`
2450
+ : "openclaw openviking setup did not run";
2451
+ err(tr(`Setup failed: ${setupError}`, `配置失败: ${setupError}`));
2452
+ return {
2453
+ runtimeConfigOk: false,
2454
+ error: setupError,
2455
+ };
2456
+ }
2457
+
2458
+ if (!setupJsonSupported) {
2459
+ // Direct write: only used when the installed plugin doesn't support `setup --json` (old version).
2460
+ // Read the deployed configSchema to determine which fields are allowed, avoiding
2461
+ // "additionalProperties" validation failures when writing new fields to old schemas.
2462
+ let allowedProps = null;
2463
+ try {
2464
+ const manifestPath = join(PLUGIN_DEST, "openclaw.plugin.json");
2465
+ if (existsSync(manifestPath)) {
2466
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
2467
+ const schema = manifest?.configSchema;
2468
+ if (schema?.properties && typeof schema.properties === "object") {
2469
+ allowedProps = new Set(Object.keys(schema.properties));
2470
+ }
1596
2471
  }
1597
- }
1598
- } else if (setupResult.code !== 0) {
1599
- // JSON parse failed and non-zero exit — setup CLI not available (old plugin version)
1600
- warn(tr(
1601
- `openclaw openviking setup exited with code ${setupResult.code}. Falling back to direct JSON config write.`,
1602
- `openclaw openviking setup 退出码 ${setupResult.code},回退到直接写入 JSON 配置。`,
1603
- ));
1604
- const pluginConfig = {
2472
+ } catch { /* ignore parse errors, write all fields */ }
2473
+
2474
+ const agentVal = effectiveRuntimeConfig.agent_prefix || "";
2475
+ const useAgentPrefix = !allowedProps || allowedProps.has("agent_prefix");
2476
+ const candidates = {
2477
+ mode: "remote",
1605
2478
  baseUrl: effectiveRuntimeConfig.baseUrl || remoteBaseUrl,
2479
+ apiKey: effectiveRuntimeConfig.apiKey || "",
2480
+ accountId: effectiveRuntimeConfig.accountId || undefined,
2481
+ userId: effectiveRuntimeConfig.userId || undefined,
1606
2482
  };
1607
- if (effectiveRuntimeConfig.apiKey) {
1608
- pluginConfig.apiKey = effectiveRuntimeConfig.apiKey;
2483
+ if (useAgentPrefix) {
2484
+ candidates.agent_prefix = agentVal;
2485
+ } else {
2486
+ candidates.agentId = agentVal;
1609
2487
  }
1610
- if (effectiveRuntimeConfig.agent_prefix) {
1611
- pluginConfig.agent_prefix = effectiveRuntimeConfig.agent_prefix;
2488
+
2489
+ const pluginConfig = {};
2490
+ for (const [key, val] of Object.entries(candidates)) {
2491
+ if (val === undefined) continue;
2492
+ if (allowedProps && !allowedProps.has(key)) continue;
2493
+ pluginConfig[key] = val;
1612
2494
  }
2495
+ if (!pluginConfig.baseUrl) pluginConfig.baseUrl = effectiveRuntimeConfig.baseUrl || remoteBaseUrl;
2496
+ if (!("apiKey" in pluginConfig)) pluginConfig.apiKey = effectiveRuntimeConfig.apiKey || "";
2497
+
1613
2498
  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 插件配置完成"));
2499
+ info(tr(
2500
+ `OpenClaw plugin configured (direct write): baseUrl=${pluginConfig.baseUrl}, apiKey=${pluginConfig.apiKey ? "***" : "(empty)"}`,
2501
+ `OpenClaw 插件配置完成(直接写入): baseUrl=${pluginConfig.baseUrl}, apiKey=${pluginConfig.apiKey ? "***" : "(空)"}`,
2502
+ ));
1623
2503
  }
2504
+
2505
+ return { runtimeConfigOk: true };
1624
2506
  }
1625
2507
 
1626
2508
  async function writeOpenvikingEnv() {
@@ -1678,6 +2560,125 @@ function getExistingEnvFiles() {
1678
2560
  return existsSync(envPath) ? { shellPath: envPath } : null;
1679
2561
  }
1680
2562
 
2563
+ async function performUninstall() {
2564
+ info(tr("Mode: uninstall plugin", "模式: 卸载插件"));
2565
+ info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
2566
+
2567
+ const configPath = getOpenClawConfigPath();
2568
+ if (!existsSync(configPath)) {
2569
+ info(tr(
2570
+ "No openclaw.json found. Nothing to uninstall.",
2571
+ "未找到 openclaw.json,无需卸载。",
2572
+ ));
2573
+ return;
2574
+ }
2575
+
2576
+ const installedState = await detectInstalledPluginState();
2577
+ if (installedState.generation === "none") {
2578
+ info(tr(
2579
+ "No OpenViking plugin entries found in openclaw.json. Nothing to uninstall.",
2580
+ "openclaw.json 中未找到 OpenViking 插件配置,无需卸载。",
2581
+ ));
2582
+ return;
2583
+ }
2584
+
2585
+ info(tr(
2586
+ `Detected installed plugin: ${formatInstalledStateLabel(installedState)}`,
2587
+ `检测到已安装插件: ${formatInstalledStateLabel(installedState)}`,
2588
+ ));
2589
+
2590
+ if (!nonInteractive) {
2591
+ const answer = await question(
2592
+ tr("Confirm uninstall? (y/N)", "确认卸载?(y/N)"),
2593
+ "N",
2594
+ );
2595
+ if (!isYes(answer)) {
2596
+ info(tr("Cancelled.", "已取消。"));
2597
+ return;
2598
+ }
2599
+ }
2600
+
2601
+ // Step 1: Stop gateway
2602
+ info(tr("Step 1: Stopping OpenClaw gateway...", "步骤 1: 停止 OpenClaw gateway..."));
2603
+ await stopOpenClawGatewayForUpgrade();
2604
+
2605
+ // Step 2: Backup config
2606
+ info(tr("Step 2: Backing up configuration...", "步骤 2: 备份配置..."));
2607
+ const configBackupPath = await backupOpenClawConfig(configPath);
2608
+ info(tr(`Config backed up to: ${configBackupPath}`, `配置已备份至: ${configBackupPath}`));
2609
+
2610
+ // Step 3: Clean plugin config from openclaw.json
2611
+ info(tr("Step 3: Cleaning plugin configuration...", "步骤 3: 清理插件配置..."));
2612
+ await cleanupInstalledPluginConfig(installedState);
2613
+
2614
+ // Step 4: Backup and remove plugin directories
2615
+ info(tr("Step 4: Backing up plugin directories...", "步骤 4: 备份插件目录..."));
2616
+ const pluginBackups = [];
2617
+ for (const detection of installedState.detections) {
2618
+ const backupDir = await backupPluginDirectory(detection.variant);
2619
+ if (backupDir) {
2620
+ pluginBackups.push({ pluginId: detection.variant.id, backupDir });
2621
+ }
2622
+ }
2623
+
2624
+ // Step 5: Remove env files
2625
+ info(tr("Step 5: Removing environment files...", "步骤 5: 移除环境文件..."));
2626
+ const envFilesToRemove = IS_WIN
2627
+ ? [
2628
+ join(OPENCLAW_DIR, "openviking.env.bat"),
2629
+ join(OPENCLAW_DIR, "openviking.env.ps1"),
2630
+ ]
2631
+ : [join(OPENCLAW_DIR, "openviking.env")];
2632
+ let removedEnvCount = 0;
2633
+ for (const f of envFilesToRemove) {
2634
+ if (existsSync(f)) {
2635
+ try {
2636
+ await rm(f);
2637
+ removedEnvCount++;
2638
+ info(tr(`Removed: ${f}`, `已移除: ${f}`));
2639
+ } catch { /* ignore */ }
2640
+ }
2641
+ }
2642
+ if (removedEnvCount === 0) {
2643
+ info(tr("No environment files found.", "未找到环境文件。"));
2644
+ }
2645
+
2646
+ // Step 6: Write uninstall audit
2647
+ const auditDir = getUpgradeAuditDir();
2648
+ await mkdir(auditDir, { recursive: true });
2649
+ const auditData = {
2650
+ operation: "uninstall",
2651
+ createdAt: new Date().toISOString(),
2652
+ fromVersion: formatInstalledStateLabel(installedState),
2653
+ configBackupPath,
2654
+ pluginBackups,
2655
+ };
2656
+ await writeUpgradeAuditFile(auditData);
2657
+
2658
+ // Done
2659
+ console.log("");
2660
+ bold("═══════════════════════════════════════════════════════════");
2661
+ bold(` ${tr("Uninstall complete!", "卸载完成!")}`);
2662
+ bold("═══════════════════════════════════════════════════════════");
2663
+ console.log("");
2664
+
2665
+ info(tr("OpenViking server/runtime is preserved (not uninstalled).", "OpenViking 服务端/运行时已保留(未卸载)。"));
2666
+ console.log("");
2667
+
2668
+ info(tr("To restore the plugin configuration:", "如需恢复插件配置:"));
2669
+ console.log(` 1) ${tr("Stop gateway:", "停止 gateway:")} openclaw gateway stop`);
2670
+ console.log(` 2) ${tr("Restore config:", "恢复配置:")} ${IS_WIN ? "copy" : "cp"} "${configBackupPath}" "${configPath}"`);
2671
+ for (const pb of pluginBackups) {
2672
+ const liveDir = join(OPENCLAW_DIR, "extensions", pb.pluginId);
2673
+ console.log(` 3) ${tr("Restore plugin:", "恢复插件:")} ${IS_WIN ? "move" : "mv"} "${pb.backupDir}" "${liveDir}"`);
2674
+ }
2675
+ console.log("");
2676
+
2677
+ info(tr("To reinstall:", "重新安装:"));
2678
+ console.log(" ov-install");
2679
+ console.log("");
2680
+ }
2681
+
1681
2682
  async function main() {
1682
2683
  console.log("");
1683
2684
  bold(tr("🦣 OpenClaw OpenViking plugin installer", "🦣 OpenClaw OpenViking 插件安装"));
@@ -1688,6 +2689,10 @@ async function main() {
1688
2689
  await printCurrentVersionInfo();
1689
2690
  return;
1690
2691
  }
2692
+ if (uninstallPlugin) {
2693
+ await performUninstall();
2694
+ return;
2695
+ }
1691
2696
  if (rollbackLastUpgrade) {
1692
2697
  info(tr("Mode: rollback last plugin upgrade", "模式: 回滚最近一次插件升级"));
1693
2698
  if (pluginVersionExplicit) {
@@ -1696,11 +2701,16 @@ async function main() {
1696
2701
  await rollbackLastUpgradeOperation();
1697
2702
  return;
1698
2703
  }
1699
- await resolveDefaultPluginVersion();
1700
- validateRequestedPluginVersion();
1701
- info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
1702
- info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
1703
- info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
2704
+ await resolveDefaultPluginVersion();
2705
+ validateRequestedPluginVersion();
2706
+ info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
2707
+ info(tr(`Plugin source: ${pluginSource}`, `Plugin source: ${pluginSource}`));
2708
+ if (pluginSource === "npm") {
2709
+ info(tr(`Plugin package: ${pluginNpmPackage}`, `Plugin package: ${pluginNpmPackage}`));
2710
+ } else {
2711
+ info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
2712
+ }
2713
+ info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
1704
2714
 
1705
2715
  if (upgradePluginOnly) {
1706
2716
  info(tr("Mode: plugin upgrade only", "模式: 仅升级插件"));
@@ -1721,7 +2731,7 @@ async function main() {
1721
2731
 
1722
2732
  await deployPluginFromRemote();
1723
2733
 
1724
- await configureOpenClawPlugin(
2734
+ const configResult = await configureOpenClawPlugin(
1725
2735
  upgradePluginOnly
1726
2736
  ? {
1727
2737
  runtimeConfig: upgradeRuntimeConfig,
@@ -1729,15 +2739,23 @@ async function main() {
1729
2739
  }
1730
2740
  : { preserveExistingConfig: false },
1731
2741
  );
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);
2742
+ const runtimeConfigOk = configResult?.runtimeConfigOk !== false;
2743
+ const runtimeConfigError = configResult?.error || "";
2744
+
2745
+ // Only mark the install as completed (state file + upgrade audit) when the
2746
+ // runtime config was actually applied. Plugin files are already on disk
2747
+ // either way, so subsequent runs can pick up from here.
2748
+ if (runtimeConfigOk) {
2749
+ await writeInstallStateFile({
2750
+ operation: upgradePluginOnly ? "upgrade" : "install",
2751
+ fromVersion: upgradeAudit?.fromVersion || "",
2752
+ configBackupPath: upgradeAudit?.configBackupPath || "",
2753
+ pluginBackups: upgradeAudit?.pluginBackups || [],
2754
+ });
2755
+ if (upgradeAudit) {
2756
+ upgradeAudit.completedAt = new Date().toISOString();
2757
+ await writeUpgradeAuditFile(upgradeAudit);
2758
+ }
1741
2759
  }
1742
2760
  let envFiles = getExistingEnvFiles();
1743
2761
  if (!upgradePluginOnly) {
@@ -1747,18 +2765,33 @@ async function main() {
1747
2765
  }
1748
2766
 
1749
2767
  console.log("");
1750
- bold("═══════════════════════════════════════════════════════════");
1751
- bold(` ${tr("Installation complete!", "安装完成!")}`);
1752
- bold("═══════════════════════════════════════════════════════════");
1753
- console.log("");
2768
+ if (runtimeConfigOk) {
2769
+ bold("═══════════════════════════════════════════════════════════");
2770
+ bold(` ${tr("Installation complete!", "安装完成!")}`);
2771
+ bold("═══════════════════════════════════════════════════════════");
2772
+ console.log("");
1754
2773
 
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}`));
2774
+ if (upgradeAudit) {
2775
+ info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
2776
+ info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
2777
+ for (const pluginBackup of upgradeAudit.pluginBackups || []) {
2778
+ info(tr(`Rollback plugin backup: ${pluginBackup.backupDir}`, `回滚插件备份: ${pluginBackup.backupDir}`));
2779
+ }
2780
+ info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
2781
+ console.log("");
1760
2782
  }
1761
- info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
2783
+ } else {
2784
+ bold("═══════════════════════════════════════════════════════════");
2785
+ bold(` ${tr(
2786
+ "Plugin files installed, but runtime configuration was NOT applied",
2787
+ "插件文件已安装,但运行时配置未生效",
2788
+ )}`);
2789
+ bold(` ${tr(`Reason: ${runtimeConfigError}`, `原因: ${runtimeConfigError}`)}`);
2790
+ bold(` ${tr(
2791
+ "Re-run: openclaw openviking setup --reconfigure",
2792
+ "重新运行: openclaw openviking setup --reconfigure",
2793
+ )}`);
2794
+ bold("═══════════════════════════════════════════════════════════");
1762
2795
  console.log("");
1763
2796
  }
1764
2797