openclaw-openviking-setup-helper 0.3.0-beta.2 → 0.3.0-beta.4

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 (3) hide show
  1. package/install.js +1717 -0
  2. package/package.json +5 -20
  3. package/dist/install.js +0 -5735
package/install.js ADDED
@@ -0,0 +1,1717 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenClaw OpenViking plugin installer (remote OpenViking server — does not install Python/OpenViking server).
4
+ *
5
+ * One-liner (after npm publish; use package name + bin name):
6
+ * npx -p openclaw-openviking-setup-helper ov-install [ -y ] [ --zh ] [ --workdir PATH ]
7
+ * Or install globally then run:
8
+ * npm i -g openclaw-openviking-setup-helper
9
+ * ov-install
10
+ * openclaw-openviking-install
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
+ */
20
+
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";
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+
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);
35
+ const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmmirror.com";
36
+
37
+ const IS_WIN = process.platform === "win32";
38
+ const HOME = process.env.HOME || process.env.USERPROFILE || "";
39
+
40
+ const DEFAULT_OPENCLAW_DIR = join(HOME, ".openclaw");
41
+ let OPENCLAW_DIR = DEFAULT_OPENCLAW_DIR;
42
+ let PLUGIN_DEST = ""; // Will be set after resolving plugin config
43
+
44
+ // Fallback configs for old versions without manifest
45
+ const FALLBACK_LEGACY = {
46
+ dir: "openclaw-memory-plugin",
47
+ id: "memory-openviking",
48
+ kind: "memory",
49
+ slot: "memory",
50
+ required: ["index.ts", "config.ts", "openclaw.plugin.json", "package.json"],
51
+ optional: ["package-lock.json", ".gitignore"],
52
+ };
53
+
54
+ // Must match examples/openclaw-plugin/install-manifest.json (npm only installs package deps, not these .ts files).
55
+ const FALLBACK_CURRENT = {
56
+ dir: "openclaw-plugin",
57
+ id: "openviking",
58
+ kind: "context-engine",
59
+ slot: "contextEngine",
60
+ required: ["index.ts", "config.ts", "package.json", "openclaw.plugin.json"],
61
+ optional: [
62
+ "context-engine.ts",
63
+ "client.ts",
64
+ "process-manager.ts",
65
+ "memory-ranking.ts",
66
+ "text-utils.ts",
67
+ "tool-call-id.ts",
68
+ "session-transcript-repair.ts",
69
+ "tsconfig.json",
70
+ "package-lock.json",
71
+ ".gitignore",
72
+ ],
73
+ };
74
+
75
+ const PLUGIN_VARIANTS = [
76
+ { ...FALLBACK_LEGACY, generation: "legacy", slotFallback: "none" },
77
+ { ...FALLBACK_CURRENT, generation: "current", slotFallback: "legacy" },
78
+ ];
79
+
80
+ // Resolved plugin config (set by resolvePluginConfig)
81
+ let resolvedPluginDir = "";
82
+ let resolvedPluginId = "";
83
+ let resolvedPluginKind = "";
84
+ let resolvedPluginSlot = "";
85
+ let resolvedFilesRequired = [];
86
+ let resolvedFilesOptional = [];
87
+ let resolvedNpmOmitDev = true;
88
+ let resolvedMinOpenclawVersion = "";
89
+ let resolvedMinOpenvikingVersion = "";
90
+ let resolvedPluginReleaseId = "";
91
+
92
+ let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
93
+ let langZh = false;
94
+ let workdirExplicit = false;
95
+ let upgradePluginOnly = false;
96
+ let rollbackLastUpgrade = false;
97
+ let showCurrentVersion = false;
98
+
99
+ const selectedMode = "remote";
100
+ let remoteBaseUrl = "http://127.0.0.1:1933";
101
+ let remoteApiKey = "";
102
+ let remoteAgentPrefix = "";
103
+ let upgradeRuntimeConfig = null;
104
+ let installedUpgradeState = null;
105
+ let upgradeAudit = null;
106
+
107
+ const argv = process.argv.slice(2);
108
+ for (let i = 0; i < argv.length; i++) {
109
+ const arg = argv[i];
110
+ if (arg === "-y" || arg === "--yes") {
111
+ installYes = true;
112
+ continue;
113
+ }
114
+ if (arg === "--zh") {
115
+ langZh = true;
116
+ continue;
117
+ }
118
+ if (arg === "--current-version") {
119
+ showCurrentVersion = true;
120
+ continue;
121
+ }
122
+ if (arg === "--upgrade-plugin" || arg === "--update" || arg === "--upgrade") {
123
+ upgradePluginOnly = true;
124
+ continue;
125
+ }
126
+ if (arg === "--rollback" || arg === "--rollback-last-upgrade") {
127
+ rollbackLastUpgrade = true;
128
+ continue;
129
+ }
130
+ if (arg === "--workdir") {
131
+ const workdir = argv[i + 1]?.trim();
132
+ if (!workdir) {
133
+ console.error("--workdir requires a path");
134
+ process.exit(1);
135
+ }
136
+ setOpenClawDir(workdir);
137
+ workdirExplicit = true;
138
+ i += 1;
139
+ continue;
140
+ }
141
+ if (arg.startsWith("--plugin-version=")) {
142
+ const version = arg.slice("--plugin-version=".length).trim();
143
+ if (!version) {
144
+ console.error("--plugin-version requires a value");
145
+ process.exit(1);
146
+ }
147
+ PLUGIN_VERSION = version;
148
+ pluginVersionExplicit = true;
149
+ continue;
150
+ }
151
+ if (arg === "--plugin-version") {
152
+ const version = argv[i + 1]?.trim();
153
+ if (!version) {
154
+ console.error("--plugin-version requires a value");
155
+ process.exit(1);
156
+ }
157
+ PLUGIN_VERSION = version;
158
+ pluginVersionExplicit = true;
159
+ i += 1;
160
+ continue;
161
+ }
162
+ if (arg.startsWith("--github-repo=")) {
163
+ REPO = arg.slice("--github-repo=".length).trim();
164
+ continue;
165
+ }
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;
173
+ i += 1;
174
+ continue;
175
+ }
176
+ if (arg === "-h" || arg === "--help") {
177
+ printHelp();
178
+ process.exit(0);
179
+ }
180
+ }
181
+
182
+ function setOpenClawDir(dir) {
183
+ OPENCLAW_DIR = dir;
184
+ }
185
+
186
+ function printHelp() {
187
+ 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)");
192
+ console.log(" --workdir PATH OpenClaw config directory (default: ~/.openclaw)");
193
+ console.log(" --current-version Print installed plugin version and exit");
194
+ console.log(" --update, --upgrade-plugin");
195
+ console.log(" Upgrade only the plugin to the requested --plugin-version; keeps existing plugin runtime config");
196
+ console.log(" --rollback, --rollback-last-upgrade");
197
+ 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");
200
+ console.log(" -h, --help This help");
201
+ console.log("");
202
+ console.log("Examples:");
203
+ console.log(" # Install latest version");
204
+ console.log(" node install.js");
205
+ console.log("");
206
+ console.log(" # Show installed versions");
207
+ console.log(" node install.js --current-version");
208
+ 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");
214
+ console.log("");
215
+ console.log(" # Install specific plugin version");
216
+ console.log(" node install.js --plugin-version=v0.2.8");
217
+ console.log("");
218
+ console.log(" # Upgrade only the plugin files from main branch");
219
+ console.log(" node install.js --update --plugin-version=main");
220
+ console.log("");
221
+ console.log(" # Roll back the last plugin upgrade");
222
+ console.log(" node install.js --rollback");
223
+ console.log("");
224
+ console.log("Env: REPO, PLUGIN_VERSION, SKIP_OPENCLAW, NPM_REGISTRY");
225
+ }
226
+
227
+ function formatCliArg(value) {
228
+ if (!value) {
229
+ return "";
230
+ }
231
+ return /[\s"]/u.test(value) ? JSON.stringify(value) : value;
232
+ }
233
+
234
+ function getLegacyInstallCommandHint() {
235
+ const override = process.env.OPENVIKING_INSTALL_LEGACY_HINT?.trim();
236
+ if (override) {
237
+ return override;
238
+ }
239
+
240
+ const invokedScript = process.argv[1] ? basename(process.argv[1]) : "";
241
+ const args = ["--plugin-version", "<legacy-version>"];
242
+ if (workdirExplicit || OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
243
+ args.push("--workdir", formatCliArg(OPENCLAW_DIR));
244
+ }
245
+ if (REPO !== "volcengine/OpenViking") {
246
+ args.push("--github-repo", formatCliArg(REPO));
247
+ }
248
+ if (langZh) {
249
+ args.push("--zh");
250
+ }
251
+
252
+ if (invokedScript === "install.js") {
253
+ return `node install.js ${args.join(" ")}`;
254
+ }
255
+
256
+ return `ov-install ${args.join(" ")}`;
257
+ }
258
+
259
+ function tr(en, zh) {
260
+ return langZh ? zh : en;
261
+ }
262
+
263
+ function info(msg) {
264
+ console.log(`[INFO] ${msg}`);
265
+ }
266
+
267
+ function warn(msg) {
268
+ console.log(`[WARN] ${msg}`);
269
+ }
270
+
271
+ function err(msg) {
272
+ console.log(`[ERROR] ${msg}`);
273
+ }
274
+
275
+ function bold(msg) {
276
+ console.log(msg);
277
+ }
278
+
279
+ function run(cmd, args, opts = {}) {
280
+ return new Promise((resolve, reject) => {
281
+ const child = spawn(cmd, args, {
282
+ stdio: opts.silent ? "pipe" : "inherit",
283
+ shell: opts.shell ?? true,
284
+ ...opts,
285
+ });
286
+ child.on("error", reject);
287
+ child.on("close", (code) => {
288
+ if (code === 0) resolve();
289
+ else reject(new Error(`exit ${code}`));
290
+ });
291
+ });
292
+ }
293
+
294
+ function runCapture(cmd, args, opts = {}) {
295
+ return new Promise((resolve) => {
296
+ const child = spawn(cmd, args, {
297
+ stdio: ["ignore", "pipe", "pipe"],
298
+ shell: opts.shell ?? false,
299
+ ...opts,
300
+ });
301
+ let out = "";
302
+ let errOut = "";
303
+ child.stdout?.on("data", (chunk) => {
304
+ out += String(chunk);
305
+ });
306
+ child.stderr?.on("data", (chunk) => {
307
+ errOut += String(chunk);
308
+ });
309
+ child.on("error", (error) => {
310
+ resolve({ code: -1, out: "", err: String(error) });
311
+ });
312
+ child.on("close", (code) => {
313
+ resolve({ code, out: out.trim(), err: errOut.trim() });
314
+ });
315
+ });
316
+ }
317
+
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) => {
323
+ rl.close();
324
+ resolve((answer ?? defaultValue).trim() || defaultValue);
325
+ });
326
+ });
327
+ }
328
+
329
+ function detectOpenClawInstances() {
330
+ const instances = [];
331
+ try {
332
+ const entries = readdirSync(HOME, { withFileTypes: true });
333
+ for (const entry of entries) {
334
+ if (!entry.isDirectory()) continue;
335
+ if (entry.name === ".openclaw" || entry.name.startsWith(".openclaw-")) {
336
+ instances.push(join(HOME, entry.name));
337
+ }
338
+ }
339
+ } catch {}
340
+ return instances.sort();
341
+ }
342
+
343
+ async function selectWorkdir() {
344
+ if (workdirExplicit) return;
345
+
346
+ const instances = detectOpenClawInstances();
347
+ if (instances.length <= 1) return;
348
+ if (showCurrentVersion) {
349
+ setOpenClawDir(instances[0]);
350
+ return;
351
+ }
352
+ if (installYes) return;
353
+
354
+ console.log("");
355
+ bold(tr("Found multiple OpenClaw instances:", "发现多个 OpenClaw 实例:"));
356
+ for (let i = 0; i < instances.length; i++) {
357
+ console.log(` ${i + 1}) ${instances[i]}`);
358
+ }
359
+ console.log("");
360
+
361
+ const answer = await question(tr("Select instance number", "选择实例编号"), "1");
362
+ const index = Number.parseInt(answer, 10) - 1;
363
+ if (index >= 0 && index < instances.length) {
364
+ setOpenClawDir(instances[index]);
365
+ } else {
366
+ warn(tr("Invalid selection, using default", "无效选择,使用默认"));
367
+ setOpenClawDir(instances[0]);
368
+ }
369
+ }
370
+
371
+ async function collectRemoteConfig() {
372
+ if (installYes) return;
373
+ remoteBaseUrl = await question(tr("OpenViking server URL", "OpenViking 服务器地址"), remoteBaseUrl);
374
+ remoteApiKey = await question(tr("API Key (optional)", "API Key(可选)"), remoteApiKey);
375
+ remoteAgentPrefix = await question(tr("Agent Prefix (optional)", "Agent Prefix(可选)"), remoteAgentPrefix);
376
+ }
377
+
378
+ async function checkOpenClaw() {
379
+ if (process.env.SKIP_OPENCLAW === "1") {
380
+ info(tr("Skipping OpenClaw check (SKIP_OPENCLAW=1)", "跳过 OpenClaw 校验 (SKIP_OPENCLAW=1)"));
381
+ return;
382
+ }
383
+
384
+ info(tr("Checking OpenClaw...", "正在校验 OpenClaw..."));
385
+ const result = await runCapture("openclaw", ["--version"], { shell: IS_WIN });
386
+ if (result.code === 0) {
387
+ info(tr("OpenClaw detected ✓", "OpenClaw 已安装 ✓"));
388
+ return;
389
+ }
390
+
391
+ err(tr("OpenClaw not found. Install it manually, then rerun this script.", "未检测到 OpenClaw,请先手动安装后再执行本脚本"));
392
+ console.log("");
393
+ console.log(tr("Recommended command:", "推荐命令:"));
394
+ console.log(` npm install -g openclaw --registry ${NPM_REGISTRY}`);
395
+ console.log("");
396
+ console.log(" openclaw --version");
397
+ console.log(" openclaw onboard");
398
+ console.log("");
399
+ process.exit(1);
400
+ }
401
+
402
+ // Compare versions: returns true if v1 >= v2
403
+ function versionGte(v1, v2) {
404
+ const parseVersion = (v) => {
405
+ const cleaned = v.replace(/^v/, "").replace(/-.*$/, "");
406
+ const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10) || 0);
407
+ while (parts.length < 3) parts.push(0);
408
+ return parts;
409
+ };
410
+ const [a1, a2, a3] = parseVersion(v1);
411
+ const [b1, b2, b3] = parseVersion(v2);
412
+ if (a1 !== b1) return a1 > b1;
413
+ if (a2 !== b2) return a2 > b2;
414
+ return a3 >= b3;
415
+ }
416
+
417
+ function isSemverLike(value) {
418
+ return /^v?\d+(\.\d+){1,2}$/.test(value);
419
+ }
420
+
421
+ function validateRequestedPluginVersion() {
422
+ if (!isSemverLike(PLUGIN_VERSION)) return;
423
+ if (versionGte(PLUGIN_VERSION, "v0.2.7") && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
424
+ err(tr("Plugin version v0.2.7 does not exist.", "插件版本 v0.2.7 不存在。"));
425
+ process.exit(1);
426
+ }
427
+ }
428
+
429
+ if (upgradePluginOnly && rollbackLastUpgrade) {
430
+ console.error("--update/--upgrade-plugin and --rollback cannot be used together");
431
+ process.exit(1);
432
+ }
433
+
434
+ // Detect OpenClaw version
435
+ async function detectOpenClawVersion() {
436
+ try {
437
+ 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];
441
+ }
442
+ } catch {}
443
+ return "0.0.0";
444
+ }
445
+
446
+ // Try to fetch a URL, return response text or null
447
+ async function tryFetch(url, timeout = 15000) {
448
+ try {
449
+ const controller = new AbortController();
450
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
451
+ const response = await fetch(url, { signal: controller.signal });
452
+ clearTimeout(timeoutId);
453
+ if (response.ok) {
454
+ return await response.text();
455
+ }
456
+ } catch {}
457
+ return null;
458
+ }
459
+
460
+ // Check if a remote file exists
461
+ async function testRemoteFile(url) {
462
+ try {
463
+ const controller = new AbortController();
464
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
465
+ const response = await fetch(url, { method: "HEAD", signal: controller.signal });
466
+ clearTimeout(timeoutId);
467
+ return response.ok;
468
+ } catch {}
469
+ return false;
470
+ }
471
+
472
+ function compareSemverDesc(a, b) {
473
+ if (versionGte(a, b) && versionGte(b, a)) {
474
+ return 0;
475
+ }
476
+ return versionGte(a, b) ? -1 : 1;
477
+ }
478
+
479
+ function pickLatestPluginTag(tagNames) {
480
+ const normalized = tagNames
481
+ .map((tag) => String(tag ?? "").trim())
482
+ .filter(Boolean);
483
+
484
+ const semverTags = normalized
485
+ .filter((tag) => isSemverLike(tag))
486
+ .sort(compareSemverDesc);
487
+
488
+ if (semverTags.length > 0) {
489
+ return semverTags[0];
490
+ }
491
+
492
+ return normalized[0] || "";
493
+ }
494
+
495
+ function parseGitLsRemoteTags(output) {
496
+ return String(output ?? "")
497
+ .split(/\r?\n/)
498
+ .map((line) => {
499
+ const match = line.match(/refs\/tags\/(.+)$/);
500
+ return match?.[1]?.trim() || "";
501
+ })
502
+ .filter(Boolean);
503
+ }
504
+
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}...`,
512
+ `未指定插件版本,正在解析 ${REPO} 的最新 tag...`,
513
+ ));
514
+
515
+ const failures = [];
516
+ const apiUrl = `https://api.github.com/repos/${REPO}/tags?per_page=100`;
517
+
518
+ try {
519
+ const controller = new AbortController();
520
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
521
+ const response = await fetch(apiUrl, {
522
+ headers: {
523
+ Accept: "application/vnd.github+json",
524
+ "User-Agent": "openviking-setup-helper",
525
+ "X-GitHub-Api-Version": "2022-11-28",
526
+ },
527
+ signal: controller.signal,
528
+ });
529
+ clearTimeout(timeoutId);
530
+
531
+ if (response.ok) {
532
+ const payload = await response.json().catch(() => null);
533
+ if (Array.isArray(payload)) {
534
+ const latestTag = pickLatestPluginTag(payload.map((item) => item?.name || ""));
535
+ if (latestTag) {
536
+ PLUGIN_VERSION = latestTag;
537
+ info(tr(
538
+ `Resolved default plugin version to latest tag: ${PLUGIN_VERSION}`,
539
+ `已将默认插件版本解析为最新 tag: ${PLUGIN_VERSION}`,
540
+ ));
541
+ return;
542
+ }
543
+ } else {
544
+ failures.push("GitHub tags API returned an unexpected payload");
545
+ }
546
+ } else {
547
+ failures.push(`GitHub tags API returned HTTP ${response.status}`);
548
+ }
549
+ } catch (error) {
550
+ failures.push(`GitHub tags API failed: ${String(error)}`);
551
+ }
552
+
553
+ const gitRef = `https://github.com/${REPO}.git`;
554
+ const gitResult = await runCapture("git", ["ls-remote", "--tags", "--refs", gitRef], {
555
+ shell: IS_WIN,
556
+ });
557
+ if (gitResult.code === 0 && gitResult.out) {
558
+ const latestTag = pickLatestPluginTag(parseGitLsRemoteTags(gitResult.out));
559
+ if (latestTag) {
560
+ PLUGIN_VERSION = latestTag;
561
+ info(tr(
562
+ `Resolved default plugin version via git tags: ${PLUGIN_VERSION}`,
563
+ `已通过 git tag 解析默认插件版本: ${PLUGIN_VERSION}`,
564
+ ));
565
+ return;
566
+ }
567
+ failures.push("git ls-remote returned no usable tags");
568
+ } else {
569
+ failures.push(`git ls-remote failed${gitResult.err ? `: ${gitResult.err}` : ""}`);
570
+ }
571
+
572
+ err(tr(
573
+ `Could not resolve the latest tag for ${REPO}.`,
574
+ `无法解析 ${REPO} 的最新 tag。`,
575
+ ));
576
+ console.log(tr(
577
+ "Please rerun with --plugin-version <tag>, or use --plugin-version main to track the branch head explicitly.",
578
+ "请使用 --plugin-version <tag> 重新执行;如果需要显式跟踪分支头,请使用 --plugin-version main。",
579
+ ));
580
+ if (failures.length > 0) {
581
+ warn(failures.join(" | "));
582
+ }
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}`;
589
+
590
+ info(tr(`Resolving plugin configuration for version: ${PLUGIN_VERSION}`, `正在解析插件配置,版本: ${PLUGIN_VERSION}`));
591
+
592
+ let pluginDir = "";
593
+ let manifestData = null;
594
+
595
+ // Try to detect plugin directory and download manifest
596
+ const manifestCurrent = await tryFetch(`${ghRaw}/examples/openclaw-plugin/install-manifest.json`);
597
+ if (manifestCurrent) {
598
+ pluginDir = "openclaw-plugin";
599
+ try {
600
+ manifestData = JSON.parse(manifestCurrent);
601
+ } catch {}
602
+ info(tr("Found manifest in openclaw-plugin", "在 openclaw-plugin 中找到 manifest"));
603
+ } else {
604
+ const manifestLegacy = await tryFetch(`${ghRaw}/examples/openclaw-memory-plugin/install-manifest.json`);
605
+ if (manifestLegacy) {
606
+ pluginDir = "openclaw-memory-plugin";
607
+ try {
608
+ manifestData = JSON.parse(manifestLegacy);
609
+ } catch {}
610
+ info(tr("Found manifest in openclaw-memory-plugin", "在 openclaw-memory-plugin 中找到 manifest"));
611
+ } else if (await testRemoteFile(`${ghRaw}/examples/openclaw-plugin/index.ts`)) {
612
+ pluginDir = "openclaw-plugin";
613
+ info(tr("No manifest found, using fallback for openclaw-plugin", "未找到 manifest,使用 openclaw-plugin 回退配置"));
614
+ } else if (await testRemoteFile(`${ghRaw}/examples/openclaw-memory-plugin/index.ts`)) {
615
+ pluginDir = "openclaw-memory-plugin";
616
+ info(tr("No manifest found, using fallback for openclaw-memory-plugin", "未找到 manifest,使用 openclaw-memory-plugin 回退配置"));
617
+ } else {
618
+ err(tr(`Cannot find plugin directory for version: ${PLUGIN_VERSION}`, `无法找到版本 ${PLUGIN_VERSION} 的插件目录`));
619
+ process.exit(1);
620
+ }
621
+ }
622
+
623
+ resolvedPluginDir = pluginDir;
624
+ resolvedPluginReleaseId = "";
625
+
626
+ if (manifestData) {
627
+ resolvedPluginId = manifestData.plugin?.id || "";
628
+ resolvedPluginKind = manifestData.plugin?.kind || "";
629
+ resolvedPluginSlot = manifestData.plugin?.slot || "";
630
+ resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
631
+ resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
632
+ resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
633
+ resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
634
+ resolvedFilesRequired = manifestData.files?.required || [];
635
+ resolvedFilesOptional = manifestData.files?.optional || [];
636
+ } else {
637
+ // No manifest — determine plugin identity by package.json name
638
+ let fallbackKey = pluginDir === "openclaw-memory-plugin" ? "legacy" : "current";
639
+ let compatVer = "";
640
+
641
+ const pkgJson = await tryFetch(`${ghRaw}/examples/${pluginDir}/package.json`);
642
+ if (pkgJson) {
643
+ try {
644
+ const pkg = JSON.parse(pkgJson);
645
+ const pkgName = pkg.name || "";
646
+ resolvedPluginReleaseId = pkg.version || "";
647
+ if (pkgName && pkgName !== "@openclaw/openviking") {
648
+ fallbackKey = "legacy";
649
+ info(tr(`Detected legacy plugin by package name: ${pkgName}`, `通过 package.json 名称检测到旧版插件: ${pkgName}`));
650
+ } else if (pkgName) {
651
+ fallbackKey = "current";
652
+ }
653
+ compatVer = (pkg.engines?.openclaw || "").replace(/^>=?\s*/, "").trim();
654
+ if (compatVer) {
655
+ info(tr(`Read minOpenclawVersion from package.json engines.openclaw: >=${compatVer}`, `从 package.json engines.openclaw 读取到最低版本: >=${compatVer}`));
656
+ }
657
+ } catch {}
658
+ }
659
+
660
+ const fallback = fallbackKey === "legacy" ? FALLBACK_LEGACY : FALLBACK_CURRENT;
661
+ resolvedPluginDir = pluginDir;
662
+ resolvedPluginId = fallback.id;
663
+ resolvedPluginKind = fallback.kind;
664
+ resolvedPluginSlot = fallback.slot;
665
+ resolvedFilesRequired = fallback.required;
666
+ resolvedFilesOptional = fallback.optional;
667
+ resolvedNpmOmitDev = true;
668
+
669
+ // If no compatVer from package.json, try main branch manifest
670
+ if (!compatVer && PLUGIN_VERSION !== "main") {
671
+ const mainRaw = `https://raw.githubusercontent.com/${REPO}/main`;
672
+ const mainManifest = await tryFetch(`${mainRaw}/examples/openclaw-plugin/install-manifest.json`);
673
+ if (mainManifest) {
674
+ try {
675
+ const m = JSON.parse(mainManifest);
676
+ compatVer = m.compatibility?.minOpenclawVersion || "";
677
+ if (compatVer) {
678
+ info(tr(`Read minOpenclawVersion from main branch manifest: >=${compatVer}`, `从 main 分支 manifest 读取到最低版本: >=${compatVer}`));
679
+ }
680
+ } catch {}
681
+ }
682
+ }
683
+
684
+ resolvedMinOpenclawVersion = compatVer || "2026.3.7";
685
+ resolvedMinOpenvikingVersion = "";
686
+ }
687
+
688
+ // Set plugin destination
689
+ PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId);
690
+
691
+ info(tr(`Plugin: ${resolvedPluginId} (${resolvedPluginKind})`, `插件: ${resolvedPluginId} (${resolvedPluginKind})`));
692
+ }
693
+
694
+ // Check OpenClaw version compatibility
695
+ async function checkOpenClawCompatibility() {
696
+ if (process.env.SKIP_OPENCLAW === "1") {
697
+ return;
698
+ }
699
+
700
+ const ocVersion = await detectOpenClawVersion();
701
+ info(tr(`Detected OpenClaw version: ${ocVersion}`, `检测到 OpenClaw 版本: ${ocVersion}`));
702
+
703
+ // If no minimum version required, pass
704
+ if (!resolvedMinOpenclawVersion) {
705
+ return;
706
+ }
707
+
708
+ // If user explicitly requested an old version, pass
709
+ if (isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
710
+ return;
711
+ }
712
+
713
+ // Check compatibility
714
+ if (!versionGte(ocVersion, resolvedMinOpenclawVersion)) {
715
+ err(tr(
716
+ `OpenClaw ${ocVersion} does not support this plugin (requires >= ${resolvedMinOpenclawVersion})`,
717
+ `OpenClaw ${ocVersion} 不支持此插件(需要 >= ${resolvedMinOpenclawVersion})`
718
+ ));
719
+ console.log("");
720
+ bold(tr("Please choose one of the following options:", "请选择以下方案之一:"));
721
+ console.log("");
722
+ console.log(` ${tr("Option 1: Upgrade OpenClaw", "方案 1:升级 OpenClaw")}`);
723
+ console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
724
+ console.log("");
725
+ console.log(` ${tr("Option 2: Install a legacy plugin release compatible with your current OpenClaw version", "方案 2:安装与当前 OpenClaw 版本兼容的旧版插件")}`);
726
+ console.log(` ${getLegacyInstallCommandHint()}`);
727
+ console.log("");
728
+ process.exit(1);
729
+ }
730
+ }
731
+
732
+ function getOpenClawConfigPath() {
733
+ return join(OPENCLAW_DIR, "openclaw.json");
734
+ }
735
+
736
+ function getOpenClawEnv() {
737
+ if (OPENCLAW_DIR === DEFAULT_OPENCLAW_DIR) {
738
+ return { ...process.env };
739
+ }
740
+ return { ...process.env, OPENCLAW_STATE_DIR: OPENCLAW_DIR };
741
+ }
742
+
743
+ async function readJsonFileIfExists(filePath) {
744
+ if (!existsSync(filePath)) return null;
745
+ const raw = await readFile(filePath, "utf8");
746
+ return JSON.parse(raw);
747
+ }
748
+
749
+ function getInstallStatePathForPlugin(pluginId) {
750
+ return join(OPENCLAW_DIR, "extensions", pluginId, ".ov-install-state.json");
751
+ }
752
+
753
+ async function printCurrentVersionInfo() {
754
+ const state = await readJsonFileIfExists(getInstallStatePathForPlugin("openviking"));
755
+ const pluginRequestedRef = state?.requestedRef || "";
756
+ const pluginReleaseId = state?.releaseId || "";
757
+ const pluginInstalledAt = state?.installedAt || "";
758
+
759
+ console.log("");
760
+ bold(tr("Installed versions", "当前已安装版本"));
761
+ console.log("");
762
+ console.log(`Target: ${OPENCLAW_DIR}`);
763
+ console.log(`Plugin: ${pluginReleaseId || pluginRequestedRef || "not installed"}`);
764
+ if (pluginRequestedRef && pluginReleaseId && pluginRequestedRef !== pluginReleaseId) {
765
+ console.log(`Plugin requested ref: ${pluginRequestedRef}`);
766
+ }
767
+ console.log(tr("OpenViking server: not installed by this tool (use a remote URL in plugin config)", "OpenViking 服务端:本工具不安装;请在插件配置中填写远程服务地址"));
768
+ if (pluginInstalledAt) {
769
+ console.log(`Installed at: ${pluginInstalledAt}`);
770
+ }
771
+ }
772
+
773
+ function getUpgradeAuditDir() {
774
+ return join(OPENCLAW_DIR, ".openviking-upgrade-backup");
775
+ }
776
+
777
+ function getUpgradeAuditPath() {
778
+ return join(getUpgradeAuditDir(), "last-upgrade.json");
779
+ }
780
+
781
+ function getOpenClawConfigBackupPath() {
782
+ return join(getUpgradeAuditDir(), "openclaw.json.bak");
783
+ }
784
+
785
+ function getPluginVariantById(pluginId) {
786
+ return PLUGIN_VARIANTS.find((variant) => variant.id === pluginId) || null;
787
+ }
788
+
789
+ function detectPluginPresence(config, variant) {
790
+ const plugins = config?.plugins;
791
+ const reasons = [];
792
+ if (!plugins) {
793
+ return { variant, present: false, reasons };
794
+ }
795
+
796
+ if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
797
+ reasons.push("entry");
798
+ }
799
+ if (plugins.slots?.[variant.slot] === variant.id) {
800
+ reasons.push("slot");
801
+ }
802
+ if (Array.isArray(plugins.allow) && plugins.allow.includes(variant.id)) {
803
+ reasons.push("allow");
804
+ }
805
+ if (
806
+ Array.isArray(plugins.load?.paths)
807
+ && plugins.load.paths.some((item) => typeof item === "string" && (item.includes(variant.id) || item.includes(variant.dir)))
808
+ ) {
809
+ reasons.push("loadPath");
810
+ }
811
+ if (existsSync(join(OPENCLAW_DIR, "extensions", variant.id))) {
812
+ reasons.push("dir");
813
+ }
814
+
815
+ return { variant, present: reasons.length > 0, reasons };
816
+ }
817
+
818
+ async function detectInstalledPluginState() {
819
+ const configPath = getOpenClawConfigPath();
820
+ const config = await readJsonFileIfExists(configPath);
821
+ const detections = [];
822
+ for (const variant of PLUGIN_VARIANTS) {
823
+ const detection = detectPluginPresence(config, variant);
824
+ if (!detection.present) continue;
825
+ detection.installState = await readJsonFileIfExists(getInstallStatePathForPlugin(variant.id));
826
+ detections.push(detection);
827
+ }
828
+
829
+ let generation = "none";
830
+ if (detections.length === 1) {
831
+ generation = detections[0].variant.generation;
832
+ } else if (detections.length > 1) {
833
+ generation = "mixed";
834
+ }
835
+
836
+ return {
837
+ config,
838
+ configPath,
839
+ detections,
840
+ generation,
841
+ };
842
+ }
843
+
844
+ function formatInstalledDetectionLabel(detection) {
845
+ const requestedRef = detection.installState?.requestedRef;
846
+ const releaseId = detection.installState?.releaseId;
847
+ if (requestedRef) return `${detection.variant.id}@${requestedRef}`;
848
+ if (releaseId) return `${detection.variant.id}#${releaseId}`;
849
+ return `${detection.variant.id} (${detection.variant.generation}, exact version unknown)`;
850
+ }
851
+
852
+ function formatInstalledStateLabel(installedState) {
853
+ if (!installedState?.detections?.length) {
854
+ return "not-installed";
855
+ }
856
+ return installedState.detections.map(formatInstalledDetectionLabel).join(" + ");
857
+ }
858
+
859
+ function formatTargetVersionLabel() {
860
+ const base = `${resolvedPluginId || "openviking"}@${PLUGIN_VERSION}`;
861
+ if (resolvedPluginReleaseId && resolvedPluginReleaseId !== PLUGIN_VERSION) {
862
+ return `${base} (${resolvedPluginReleaseId})`;
863
+ }
864
+ return base;
865
+ }
866
+
867
+ function extractRuntimeConfigFromPluginEntry(entryConfig) {
868
+ if (!entryConfig || typeof entryConfig !== "object") return null;
869
+
870
+ const runtime = {};
871
+ if (typeof entryConfig.baseUrl === "string" && entryConfig.baseUrl.trim()) {
872
+ runtime.baseUrl = entryConfig.baseUrl.trim();
873
+ }
874
+ if (typeof entryConfig.apiKey === "string" && entryConfig.apiKey.trim()) {
875
+ runtime.apiKey = entryConfig.apiKey;
876
+ }
877
+ if (typeof entryConfig.agent_prefix === "string" && entryConfig.agent_prefix.trim()) {
878
+ runtime.agent_prefix = entryConfig.agent_prefix.trim();
879
+ }
880
+ return runtime;
881
+ }
882
+
883
+ async function backupOpenClawConfig(configPath) {
884
+ await mkdir(getUpgradeAuditDir(), { recursive: true });
885
+ const backupPath = getOpenClawConfigBackupPath();
886
+ const configText = await readFile(configPath, "utf8");
887
+ await writeFile(backupPath, configText, "utf8");
888
+ return backupPath;
889
+ }
890
+
891
+ async function writeUpgradeAuditFile(data) {
892
+ await mkdir(getUpgradeAuditDir(), { recursive: true });
893
+ await writeFile(getUpgradeAuditPath(), `${JSON.stringify(data, null, 2)}\n`, "utf8");
894
+ }
895
+
896
+ async function writeInstallStateFile({ operation, fromVersion, configBackupPath, pluginBackups }) {
897
+ const installStatePath = getInstallStatePathForPlugin(resolvedPluginId || "openviking");
898
+ const state = {
899
+ pluginId: resolvedPluginId || "openviking",
900
+ generation: getPluginVariantById(resolvedPluginId || "openviking")?.generation || "unknown",
901
+ requestedRef: PLUGIN_VERSION,
902
+ releaseId: resolvedPluginReleaseId || "",
903
+ operation,
904
+ fromVersion: fromVersion || "",
905
+ configBackupPath: configBackupPath || "",
906
+ pluginBackups: pluginBackups || [],
907
+ installedAt: new Date().toISOString(),
908
+ repo: REPO,
909
+ };
910
+ await writeFile(installStatePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
911
+ }
912
+
913
+ async function moveDirWithFallback(sourceDir, destDir) {
914
+ try {
915
+ await rename(sourceDir, destDir);
916
+ } catch {
917
+ await cp(sourceDir, destDir, { recursive: true, force: true });
918
+ await rm(sourceDir, { recursive: true, force: true });
919
+ }
920
+ }
921
+
922
+ async function rollbackLastUpgradeOperation() {
923
+ const auditPath = getUpgradeAuditPath();
924
+ const audit = await readJsonFileIfExists(auditPath);
925
+ if (!audit) {
926
+ err(
927
+ tr(
928
+ `No rollback audit file found at ${auditPath}.`,
929
+ `未找到回滚审计文件: ${auditPath}`,
930
+ ),
931
+ );
932
+ process.exit(1);
933
+ }
934
+
935
+ if (audit.rolledBackAt) {
936
+ warn(
937
+ tr(
938
+ `The last recorded upgrade was already rolled back at ${audit.rolledBackAt}.`,
939
+ `最近一次升级已在 ${audit.rolledBackAt} 回滚。`,
940
+ ),
941
+ );
942
+ }
943
+
944
+ const configBackupPath = audit.configBackupPath || getOpenClawConfigBackupPath();
945
+ if (!existsSync(configBackupPath)) {
946
+ err(
947
+ tr(
948
+ `Rollback config backup is missing: ${configBackupPath}`,
949
+ `回滚配置备份缺失: ${configBackupPath}`,
950
+ ),
951
+ );
952
+ process.exit(1);
953
+ }
954
+
955
+ const pluginBackups = Array.isArray(audit.pluginBackups) ? audit.pluginBackups : [];
956
+ if (pluginBackups.length === 0) {
957
+ err(tr("Rollback audit file contains no plugin backups.", "回滚审计文件中没有插件备份信息。"));
958
+ process.exit(1);
959
+ }
960
+ for (const pluginBackup of pluginBackups) {
961
+ if (!pluginBackup?.pluginId || !pluginBackup?.backupDir || !existsSync(pluginBackup.backupDir)) {
962
+ err(
963
+ tr(
964
+ `Rollback plugin backup is missing: ${pluginBackup?.backupDir || "<unknown>"}`,
965
+ `回滚插件备份缺失: ${pluginBackup?.backupDir || "<unknown>"}`,
966
+ ),
967
+ );
968
+ process.exit(1);
969
+ }
970
+ }
971
+
972
+ info(tr(`Rolling back last upgrade: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`, `开始回滚最近一次升级: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`));
973
+ await stopOpenClawGatewayForUpgrade();
974
+
975
+ const configText = await readFile(configBackupPath, "utf8");
976
+ await writeFile(getOpenClawConfigPath(), configText, "utf8");
977
+ info(tr(`Restored openclaw.json from backup: ${configBackupPath}`, `已从备份恢复 openclaw.json: ${configBackupPath}`));
978
+
979
+ const extensionsDir = join(OPENCLAW_DIR, "extensions");
980
+ await mkdir(extensionsDir, { recursive: true });
981
+ for (const variant of PLUGIN_VARIANTS) {
982
+ const liveDir = join(extensionsDir, variant.id);
983
+ if (existsSync(liveDir)) {
984
+ await rm(liveDir, { recursive: true, force: true });
985
+ }
986
+ }
987
+
988
+ for (const pluginBackup of pluginBackups) {
989
+ if (!pluginBackup?.pluginId || !pluginBackup?.backupDir) continue;
990
+ if (!existsSync(pluginBackup.backupDir)) {
991
+ err(
992
+ tr(
993
+ `Rollback plugin backup is missing: ${pluginBackup.backupDir}`,
994
+ `回滚插件备份缺失: ${pluginBackup.backupDir}`,
995
+ ),
996
+ );
997
+ process.exit(1);
998
+ }
999
+ const destDir = join(extensionsDir, pluginBackup.pluginId);
1000
+ await moveDirWithFallback(pluginBackup.backupDir, destDir);
1001
+ info(tr(`Restored plugin directory: ${destDir}`, `已恢复插件目录: ${destDir}`));
1002
+ }
1003
+
1004
+ audit.rolledBackAt = new Date().toISOString();
1005
+ audit.rollbackConfigPath = configBackupPath;
1006
+ await writeUpgradeAuditFile(audit);
1007
+
1008
+ console.log("");
1009
+ bold(tr("Rollback complete!", "回滚完成!"));
1010
+ console.log("");
1011
+ info(tr(`Rollback audit file: ${auditPath}`, `回滚审计文件: ${auditPath}`));
1012
+ info(tr("Run `openclaw gateway` and `openclaw status` to verify the restored plugin state.", "请运行 `openclaw gateway` 和 `openclaw status` 验证恢复后的插件状态。"));
1013
+ }
1014
+
1015
+ function prepareUpgradeRuntimeConfig(installedState) {
1016
+ const plugins = installedState.config?.plugins ?? {};
1017
+ const candidateOrder = installedState.detections
1018
+ .map((item) => item.variant)
1019
+ .sort((left, right) => (right.generation === "current" ? 1 : 0) - (left.generation === "current" ? 1 : 0));
1020
+
1021
+ let runtime = null;
1022
+ for (const variant of candidateOrder) {
1023
+ const entryConfig = extractRuntimeConfigFromPluginEntry(plugins.entries?.[variant.id]?.config);
1024
+ if (entryConfig) {
1025
+ runtime = entryConfig;
1026
+ break;
1027
+ }
1028
+ }
1029
+
1030
+ if (!runtime) {
1031
+ runtime = {};
1032
+ }
1033
+
1034
+ delete runtime.mode;
1035
+ runtime.baseUrl = runtime.baseUrl || remoteBaseUrl;
1036
+ return runtime;
1037
+ }
1038
+
1039
+ function removePluginConfig(config, variant) {
1040
+ const plugins = config?.plugins;
1041
+ if (!plugins) return false;
1042
+
1043
+ let changed = false;
1044
+
1045
+ if (Array.isArray(plugins.allow)) {
1046
+ const nextAllow = plugins.allow.filter((item) => item !== variant.id);
1047
+ changed = changed || nextAllow.length !== plugins.allow.length;
1048
+ plugins.allow = nextAllow;
1049
+ }
1050
+
1051
+ if (Array.isArray(plugins.load?.paths)) {
1052
+ const nextPaths = plugins.load.paths.filter(
1053
+ (item) => typeof item !== "string" || (!item.includes(variant.id) && !item.includes(variant.dir)),
1054
+ );
1055
+ changed = changed || nextPaths.length !== plugins.load.paths.length;
1056
+ plugins.load.paths = nextPaths;
1057
+ }
1058
+
1059
+ if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
1060
+ delete plugins.entries[variant.id];
1061
+ changed = true;
1062
+ }
1063
+
1064
+ if (plugins.slots?.[variant.slot] === variant.id) {
1065
+ plugins.slots[variant.slot] = variant.slotFallback;
1066
+ changed = true;
1067
+ }
1068
+
1069
+ return changed;
1070
+ }
1071
+
1072
+ async function prunePreviousUpgradeBackups(disabledDir, variant, keepDir) {
1073
+ if (!existsSync(disabledDir)) return;
1074
+
1075
+ const prefix = `${variant.id}-upgrade-backup-`;
1076
+ const keepName = keepDir ? keepDir.split(/[\\/]/).pop() : "";
1077
+ const entries = readdirSync(disabledDir, { withFileTypes: true });
1078
+ for (const entry of entries) {
1079
+ if (!entry.isDirectory()) continue;
1080
+ if (!entry.name.startsWith(prefix)) continue;
1081
+ if (keepName && entry.name === keepName) continue;
1082
+ await rm(join(disabledDir, entry.name), { recursive: true, force: true });
1083
+ }
1084
+ }
1085
+
1086
+ async function backupPluginDirectory(variant) {
1087
+ const pluginDir = join(OPENCLAW_DIR, "extensions", variant.id);
1088
+ if (!existsSync(pluginDir)) return null;
1089
+
1090
+ const disabledDir = join(OPENCLAW_DIR, "disabled-extensions");
1091
+ const backupDir = join(disabledDir, `${variant.id}-upgrade-backup-${Date.now()}`);
1092
+ await mkdir(disabledDir, { recursive: true });
1093
+ try {
1094
+ await rename(pluginDir, backupDir);
1095
+ } catch {
1096
+ await cp(pluginDir, backupDir, { recursive: true, force: true });
1097
+ await rm(pluginDir, { recursive: true, force: true });
1098
+ }
1099
+ info(tr(`Backed up plugin directory: ${backupDir}`, `已备份插件目录: ${backupDir}`));
1100
+ await prunePreviousUpgradeBackups(disabledDir, variant, backupDir);
1101
+ return backupDir;
1102
+ }
1103
+
1104
+ async function stopOpenClawGatewayForUpgrade() {
1105
+ const result = await runCapture("openclaw", ["gateway", "stop"], {
1106
+ env: getOpenClawEnv(),
1107
+ shell: IS_WIN,
1108
+ });
1109
+ if (result.code === 0) {
1110
+ info(tr("Stopped OpenClaw gateway before plugin upgrade", "升级插件前已停止 OpenClaw gateway"));
1111
+ } else {
1112
+ warn(tr("OpenClaw gateway may not be running; continuing", "OpenClaw gateway 可能未在运行,继续执行"));
1113
+ }
1114
+ }
1115
+
1116
+ function shouldClaimTargetSlot(installedState) {
1117
+ const currentOwner = installedState.config?.plugins?.slots?.[resolvedPluginSlot];
1118
+ if (!currentOwner || currentOwner === "none" || currentOwner === "legacy" || currentOwner === resolvedPluginId) {
1119
+ return true;
1120
+ }
1121
+ const currentOwnerVariant = getPluginVariantById(currentOwner);
1122
+ if (currentOwnerVariant && installedState.detections.some((item) => item.variant.id === currentOwnerVariant.id)) {
1123
+ return true;
1124
+ }
1125
+ return false;
1126
+ }
1127
+
1128
+ async function cleanupInstalledPluginConfig(installedState) {
1129
+ if (!installedState.config || !installedState.config.plugins) {
1130
+ warn(tr("openclaw.json has no plugins section; skipped targeted plugin cleanup", "openclaw.json 中没有 plugins 配置,已跳过定向插件清理"));
1131
+ return;
1132
+ }
1133
+
1134
+ const nextConfig = structuredClone(installedState.config);
1135
+ let changed = false;
1136
+ for (const detection of installedState.detections) {
1137
+ changed = removePluginConfig(nextConfig, detection.variant) || changed;
1138
+ }
1139
+
1140
+ if (!changed) {
1141
+ info(tr("No OpenViking plugin config changes were required", "无需修改 OpenViking 插件配置"));
1142
+ return;
1143
+ }
1144
+
1145
+ await writeFile(installedState.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
1146
+ info(tr("Cleaned existing OpenViking plugin config only", "已仅清理 OpenViking 自身插件配置"));
1147
+ }
1148
+
1149
+ async function prepareStrongPluginUpgrade() {
1150
+ const installedState = await detectInstalledPluginState();
1151
+ if (installedState.generation === "none") {
1152
+ err(
1153
+ tr(
1154
+ "Plugin upgrade mode requires an existing OpenViking plugin entry in openclaw.json.",
1155
+ "插件升级模式要求 openclaw.json 中已经存在 OpenViking 插件记录。",
1156
+ ),
1157
+ );
1158
+ process.exit(1);
1159
+ }
1160
+
1161
+ installedUpgradeState = installedState;
1162
+ upgradeRuntimeConfig = prepareUpgradeRuntimeConfig(installedState);
1163
+ const fromVersion = formatInstalledStateLabel(installedState);
1164
+ const toVersion = formatTargetVersionLabel();
1165
+ info(
1166
+ tr(
1167
+ `Detected installed OpenViking plugin state: ${installedState.generation}`,
1168
+ `检测到已安装 OpenViking 插件状态: ${installedState.generation}`,
1169
+ ),
1170
+ );
1171
+ remoteBaseUrl = upgradeRuntimeConfig.baseUrl || remoteBaseUrl;
1172
+ remoteApiKey = upgradeRuntimeConfig.apiKey || "";
1173
+ remoteAgentPrefix = upgradeRuntimeConfig.agent_prefix || "";
1174
+ info(tr(`Upgrade runtime mode: ${selectedMode} (remote OpenViking server)`, `升级运行模式: ${selectedMode}(远程 OpenViking 服务)`));
1175
+
1176
+ info(tr(`Upgrade path: ${fromVersion} -> ${toVersion}`, `升级路径: ${fromVersion} -> ${toVersion}`));
1177
+
1178
+ await stopOpenClawGatewayForUpgrade();
1179
+ const configBackupPath = await backupOpenClawConfig(installedState.configPath);
1180
+ info(tr(`Backed up openclaw.json: ${configBackupPath}`, `已备份 openclaw.json: ${configBackupPath}`));
1181
+ const pluginBackups = [];
1182
+ for (const detection of installedState.detections) {
1183
+ const backupDir = await backupPluginDirectory(detection.variant);
1184
+ if (backupDir) {
1185
+ pluginBackups.push({ pluginId: detection.variant.id, backupDir });
1186
+ }
1187
+ }
1188
+ upgradeAudit = {
1189
+ operation: "upgrade",
1190
+ createdAt: new Date().toISOString(),
1191
+ fromVersion,
1192
+ toVersion,
1193
+ configBackupPath,
1194
+ pluginBackups,
1195
+ runtimeMode: selectedMode,
1196
+ };
1197
+ await writeUpgradeAuditFile(upgradeAudit);
1198
+ await cleanupInstalledPluginConfig(installedState);
1199
+
1200
+ info(
1201
+ tr(
1202
+ "Upgrade will preserve existing plugin server connection settings where possible and re-apply minimal remote plugin config.",
1203
+ "升级将尽可能保留已有的插件服务端连接信息,并只回填最少的远程插件配置。",
1204
+ ),
1205
+ );
1206
+ info(tr(`Upgrade audit file: ${getUpgradeAuditPath()}`, `升级审计文件: ${getUpgradeAuditPath()}`));
1207
+ }
1208
+
1209
+ async function downloadPluginFile(destDir, fileName, url, required, index, total) {
1210
+ const maxRetries = 3;
1211
+ const destPath = join(destDir, fileName);
1212
+
1213
+ process.stdout.write(` [${index}/${total}] ${fileName} `);
1214
+
1215
+ let lastStatus = 0;
1216
+ let saw404 = false;
1217
+
1218
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
1219
+ try {
1220
+ const response = await fetch(url);
1221
+ lastStatus = response.status;
1222
+ if (response.ok) {
1223
+ const buffer = Buffer.from(await response.arrayBuffer());
1224
+ if (buffer.length === 0) {
1225
+ lastStatus = 0;
1226
+ } else {
1227
+ await mkdir(dirname(destPath), { recursive: true });
1228
+ await writeFile(destPath, buffer);
1229
+ console.log(" OK");
1230
+ return;
1231
+ }
1232
+ } else if (!required && response.status === 404) {
1233
+ saw404 = true;
1234
+ break;
1235
+ }
1236
+ } catch {
1237
+ lastStatus = 0;
1238
+ }
1239
+
1240
+ if (attempt < maxRetries) {
1241
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1242
+ }
1243
+ }
1244
+
1245
+ if (saw404 || lastStatus === 404) {
1246
+ if (fileName === ".gitignore") {
1247
+ await mkdir(dirname(destPath), { recursive: true });
1248
+ await writeFile(destPath, "node_modules/\n", "utf8");
1249
+ console.log(" OK");
1250
+ return;
1251
+ }
1252
+ console.log(tr(" skip", " 跳过"));
1253
+ return;
1254
+ }
1255
+
1256
+ if (!required) {
1257
+ console.log("");
1258
+ err(
1259
+ tr(
1260
+ `Optional file failed after ${maxRetries} retries (HTTP ${lastStatus || "network"}): ${url}`,
1261
+ `可选文件已重试 ${maxRetries} 次仍失败(HTTP ${lastStatus || "网络错误"}): ${url}`,
1262
+ ),
1263
+ );
1264
+ process.exit(1);
1265
+ }
1266
+
1267
+ console.log("");
1268
+ err(tr(`Download failed after ${maxRetries} retries: ${url}`, `下载失败(已重试 ${maxRetries} 次): ${url}`));
1269
+ process.exit(1);
1270
+ }
1271
+
1272
+ async function downloadPlugin(destDir) {
1273
+ const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
1274
+ const pluginDir = resolvedPluginDir;
1275
+ const total = resolvedFilesRequired.length + resolvedFilesOptional.length;
1276
+
1277
+ await mkdir(destDir, { recursive: true });
1278
+
1279
+ info(tr(`Downloading plugin from ${REPO}@${PLUGIN_VERSION} (${total} files)...`, `正在从 ${REPO}@${PLUGIN_VERSION} 下载插件(共 ${total} 个文件)...`));
1280
+
1281
+ let i = 0;
1282
+ // Download required files
1283
+ for (const name of resolvedFilesRequired) {
1284
+ if (!name) continue;
1285
+ i++;
1286
+ const url = `${ghRaw}/examples/${pluginDir}/${name}`;
1287
+ await downloadPluginFile(destDir, name, url, true, i, total);
1288
+ }
1289
+
1290
+ // Download optional files
1291
+ for (const name of resolvedFilesOptional) {
1292
+ if (!name) continue;
1293
+ i++;
1294
+ const url = `${ghRaw}/examples/${pluginDir}/${name}`;
1295
+ await downloadPluginFile(destDir, name, url, false, i, total);
1296
+ }
1297
+
1298
+ // npm install
1299
+ info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
1300
+ const npmArgs = resolvedNpmOmitDev
1301
+ ? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
1302
+ : ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
1303
+ await run("npm", npmArgs, { cwd: destDir, silent: false });
1304
+ info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
1305
+ }
1306
+
1307
+ async function createPluginStagingDir() {
1308
+ const pluginId = resolvedPluginId || "openviking";
1309
+ const extensionsDir = join(OPENCLAW_DIR, "extensions");
1310
+ await mkdir(extensionsDir, { recursive: true });
1311
+ const stagingPrefix = `.${pluginId}.staging-`;
1312
+ try {
1313
+ const entries = readdirSync(extensionsDir, { withFileTypes: true });
1314
+ for (const entry of entries) {
1315
+ if (entry.isDirectory() && entry.name.startsWith(stagingPrefix)) {
1316
+ await rm(join(extensionsDir, entry.name), { recursive: true, force: true });
1317
+ }
1318
+ }
1319
+ } catch {}
1320
+ const stagingDir = join(extensionsDir, `${stagingPrefix}${process.pid}-${Date.now()}`);
1321
+ await mkdir(stagingDir, { recursive: true });
1322
+ return stagingDir;
1323
+ }
1324
+
1325
+ async function finalizePluginDeployment(stagingDir) {
1326
+ await rm(PLUGIN_DEST, { recursive: true, force: true });
1327
+ try {
1328
+ await rename(stagingDir, PLUGIN_DEST);
1329
+ } catch {
1330
+ await cp(stagingDir, PLUGIN_DEST, { recursive: true, force: true });
1331
+ await rm(stagingDir, { recursive: true, force: true });
1332
+ }
1333
+ return info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
1334
+ }
1335
+
1336
+ async function deployPluginFromRemote() {
1337
+ const stagingDir = await createPluginStagingDir();
1338
+ try {
1339
+ await downloadPlugin(stagingDir);
1340
+ await finalizePluginDeployment(stagingDir);
1341
+ } catch (error) {
1342
+ await rm(stagingDir, { recursive: true, force: true });
1343
+ throw error;
1344
+ }
1345
+ }
1346
+
1347
+ /** Same as INSTALL*.md manual cleanup: stale entries block `plugins.slots.*` validation after reinstall. */
1348
+ function resolvedPluginSlotFallback() {
1349
+ if (resolvedPluginId === "memory-openviking") return "none";
1350
+ if (resolvedPluginId === "openviking") return "legacy";
1351
+ return "none";
1352
+ }
1353
+
1354
+ async function scrubStaleOpenClawPluginRegistration() {
1355
+ const configPath = getOpenClawConfigPath();
1356
+ if (!existsSync(configPath)) return;
1357
+ const pluginId = resolvedPluginId;
1358
+ const slot = resolvedPluginSlot;
1359
+ const slotFallback = resolvedPluginSlotFallback();
1360
+ let raw;
1361
+ try {
1362
+ raw = await readFile(configPath, "utf8");
1363
+ } catch {
1364
+ return;
1365
+ }
1366
+ let cfg;
1367
+ try {
1368
+ cfg = JSON.parse(raw);
1369
+ } catch {
1370
+ return;
1371
+ }
1372
+ if (!cfg.plugins) return;
1373
+ const p = cfg.plugins;
1374
+ let changed = false;
1375
+ if (p.entries && Object.prototype.hasOwnProperty.call(p.entries, pluginId)) {
1376
+ delete p.entries[pluginId];
1377
+ changed = true;
1378
+ }
1379
+ if (Array.isArray(p.allow)) {
1380
+ const next = p.allow.filter((id) => id !== pluginId);
1381
+ if (next.length !== p.allow.length) {
1382
+ p.allow = next;
1383
+ changed = true;
1384
+ }
1385
+ }
1386
+ if (p.load && Array.isArray(p.load.paths)) {
1387
+ const norm = (s) => String(s).replace(/\\/g, "/");
1388
+ const extNeedle = `/extensions/${pluginId}`;
1389
+ const next = p.load.paths.filter((path) => {
1390
+ if (typeof path !== "string") return true;
1391
+ return !norm(path).includes(extNeedle);
1392
+ });
1393
+ if (next.length !== p.load.paths.length) {
1394
+ p.load.paths = next;
1395
+ changed = true;
1396
+ }
1397
+ }
1398
+ if (p.slots && p.slots[slot] === pluginId) {
1399
+ p.slots[slot] = slotFallback;
1400
+ changed = true;
1401
+ }
1402
+ if (!changed) return;
1403
+ const out = JSON.stringify(cfg, null, 2) + "\n";
1404
+ const tmp = `${configPath}.ov-install-tmp.${process.pid}`;
1405
+ await writeFile(tmp, out, "utf8");
1406
+ await rename(tmp, configPath);
1407
+ }
1408
+
1409
+ async function configureOpenClawPlugin({
1410
+ preserveExistingConfig = false,
1411
+ runtimeConfig = null,
1412
+ claimSlot = true,
1413
+ } = {}) {
1414
+ info(tr("Configuring OpenClaw plugin...", "正在配置 OpenClaw 插件..."));
1415
+
1416
+ const pluginId = resolvedPluginId;
1417
+ const pluginSlot = resolvedPluginSlot;
1418
+
1419
+ const ocEnv = getOpenClawEnv();
1420
+
1421
+ const oc = async (args) => {
1422
+ const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
1423
+ if (result.code !== 0) {
1424
+ const detail = result.err || result.out;
1425
+ throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
1426
+ }
1427
+ return result;
1428
+ };
1429
+
1430
+ if (!preserveExistingConfig) {
1431
+ await scrubStaleOpenClawPluginRegistration();
1432
+ }
1433
+
1434
+ // Enable plugin (files already deployed to extensions dir by deployPlugin)
1435
+ await oc(["plugins", "enable", pluginId]);
1436
+
1437
+ if (preserveExistingConfig) {
1438
+ if (claimSlot) {
1439
+ await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
1440
+ }
1441
+ info(
1442
+ tr(
1443
+ `Preserved existing plugin runtime config for ${pluginId}`,
1444
+ `已保留 ${pluginId} 的现有插件运行时配置`,
1445
+ ),
1446
+ );
1447
+ return;
1448
+ }
1449
+
1450
+ // Legacy (memory) plugins still use direct config set
1451
+ if (resolvedPluginKind === "memory") {
1452
+ if (claimSlot) {
1453
+ await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
1454
+ }
1455
+ const effectiveRuntimeConfig = runtimeConfig || {
1456
+ baseUrl: remoteBaseUrl,
1457
+ apiKey: remoteApiKey,
1458
+ agent_prefix: remoteAgentPrefix,
1459
+ };
1460
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, effectiveRuntimeConfig.baseUrl || remoteBaseUrl]);
1461
+ if (effectiveRuntimeConfig.apiKey) {
1462
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, effectiveRuntimeConfig.apiKey]);
1463
+ }
1464
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.targetUri`, "viking://user/memories"]);
1465
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.autoRecall`, "true", "--json"]);
1466
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.autoCapture`, "true", "--json"]);
1467
+ info(tr("OpenClaw plugin configured (legacy mode)", "OpenClaw 插件配置完成(旧版模式)"));
1468
+ return;
1469
+ }
1470
+
1471
+ // Current (context-engine) plugins: delegate to `openclaw openviking setup --json`
1472
+ // This reuses the same validation logic (health check, version compat, root key
1473
+ // detection, slot protection, ensureInstallRecord) from commands/setup.ts
1474
+ const effectiveRuntimeConfig = runtimeConfig || {
1475
+ baseUrl: remoteBaseUrl,
1476
+ apiKey: remoteApiKey,
1477
+ agent_prefix: remoteAgentPrefix,
1478
+ };
1479
+
1480
+ const setupArgs = [
1481
+ "openviking", "setup",
1482
+ "--base-url", effectiveRuntimeConfig.baseUrl || remoteBaseUrl,
1483
+ "--json",
1484
+ ];
1485
+ if (effectiveRuntimeConfig.apiKey) {
1486
+ setupArgs.push("--api-key", effectiveRuntimeConfig.apiKey);
1487
+ }
1488
+ if (claimSlot) {
1489
+ setupArgs.push("--force-slot");
1490
+ }
1491
+ if (installYes) {
1492
+ setupArgs.push("--allow-offline");
1493
+ }
1494
+
1495
+ info(tr(
1496
+ "Delegating configuration to: openclaw openviking setup --json",
1497
+ "委托配置给: openclaw openviking setup --json",
1498
+ ));
1499
+
1500
+ const setupResult = await runCapture("openclaw", setupArgs, { env: ocEnv, shell: IS_WIN });
1501
+
1502
+ let parsed = null;
1503
+ try {
1504
+ parsed = JSON.parse(setupResult.out.trim());
1505
+ } catch {
1506
+ // If JSON parse fails, fall back to checking exit code
1507
+ }
1508
+
1509
+ if (parsed) {
1510
+ if (parsed.success) {
1511
+ info(tr("OpenClaw plugin configured via setup", "OpenClaw 插件通过 setup 配置完成"));
1512
+ if (parsed.health?.ok) {
1513
+ info(tr(
1514
+ `Server health: OK${parsed.health.version ? ` (version: ${parsed.health.version})` : ""}`,
1515
+ `服务端健康: OK${parsed.health.version ? `(版本: ${parsed.health.version})` : ""}`,
1516
+ ));
1517
+ }
1518
+ if (parsed.health?.compatibility === "server_too_old") {
1519
+ warn(tr(
1520
+ "Server version may be too old for this plugin version",
1521
+ "服务端版本可能低于此插件版本要求",
1522
+ ));
1523
+ }
1524
+ if (parsed.slot?.activated) {
1525
+ info(tr(`contextEngine slot activated`, `contextEngine slot 已激活`));
1526
+ }
1527
+ } else {
1528
+ // Setup returned success: false
1529
+ if (parsed.action === "slot_blocked") {
1530
+ warn(tr(
1531
+ `Config saved but contextEngine slot is owned by "${parsed.slot?.previousOwner}". Use --force-slot to override.`,
1532
+ `配置已保存,但 contextEngine slot 被 "${parsed.slot?.previousOwner}" 占用。使用 --force-slot 覆盖。`,
1533
+ ));
1534
+ } else {
1535
+ err(tr(
1536
+ `Setup failed: ${parsed.error || "unknown error"}`,
1537
+ `配置失败: ${parsed.error || "未知错误"}`,
1538
+ ));
1539
+ }
1540
+ }
1541
+ } else if (setupResult.code !== 0) {
1542
+ // JSON parse failed and non-zero exit
1543
+ warn(tr(
1544
+ `openclaw openviking setup exited with code ${setupResult.code}. Falling back to direct config.`,
1545
+ `openclaw openviking setup 退出码 ${setupResult.code},回退到直接配置。`,
1546
+ ));
1547
+ // Fallback: direct config set (for backward compat if setup CLI not available)
1548
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, effectiveRuntimeConfig.baseUrl || remoteBaseUrl]);
1549
+ if (effectiveRuntimeConfig.apiKey) {
1550
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, effectiveRuntimeConfig.apiKey]);
1551
+ }
1552
+ if (effectiveRuntimeConfig.agent_prefix) {
1553
+ await oc(["config", "set", `plugins.entries.${pluginId}.config.agent_prefix`, effectiveRuntimeConfig.agent_prefix]);
1554
+ }
1555
+ if (claimSlot) {
1556
+ await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
1557
+ }
1558
+ info(tr("OpenClaw plugin configured (fallback)", "OpenClaw 插件配置完成(回退模式)"));
1559
+ } else {
1560
+ info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
1561
+ }
1562
+ }
1563
+
1564
+ async function writeOpenvikingEnv() {
1565
+ const needStateDir = OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR;
1566
+ if (!needStateDir) return null;
1567
+
1568
+ await mkdir(OPENCLAW_DIR, { recursive: true });
1569
+
1570
+ if (IS_WIN) {
1571
+ const batLines = ["@echo off"];
1572
+ const psLines = [];
1573
+
1574
+ batLines.push(`set "OPENCLAW_STATE_DIR=${OPENCLAW_DIR.replace(/"/g, '""')}"`);
1575
+ psLines.push(`$env:OPENCLAW_STATE_DIR = "${OPENCLAW_DIR.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
1576
+
1577
+ const batPath = join(OPENCLAW_DIR, "openviking.env.bat");
1578
+ const ps1Path = join(OPENCLAW_DIR, "openviking.env.ps1");
1579
+ await writeFile(batPath, `${batLines.join("\r\n")}\r\n`, "utf8");
1580
+ await writeFile(ps1Path, `${psLines.join("\n")}\n`, "utf8");
1581
+
1582
+ info(tr(`Environment file generated: ${batPath}`, `已生成环境文件: ${batPath}`));
1583
+ return { shellPath: batPath, powershellPath: ps1Path };
1584
+ }
1585
+
1586
+ const envPath = join(OPENCLAW_DIR, "openviking.env");
1587
+ await writeFile(
1588
+ envPath,
1589
+ `export OPENCLAW_STATE_DIR='${OPENCLAW_DIR.replace(/'/g, "'\"'\"'")}'\n`,
1590
+ "utf8",
1591
+ );
1592
+ info(tr(`Environment file generated: ${envPath}`, `已生成环境文件: ${envPath}`));
1593
+ return { shellPath: envPath };
1594
+ }
1595
+
1596
+ function wrapCommand(command, envFiles) {
1597
+ if (!envFiles) return command;
1598
+ if (IS_WIN) return `call "${envFiles.shellPath}" && ${command}`;
1599
+ return `source '${envFiles.shellPath.replace(/'/g, "'\"'\"'")}' && ${command}`;
1600
+ }
1601
+
1602
+ function getExistingEnvFiles() {
1603
+ if (IS_WIN) {
1604
+ const batPath = join(OPENCLAW_DIR, "openviking.env.bat");
1605
+ const ps1Path = join(OPENCLAW_DIR, "openviking.env.ps1");
1606
+ if (existsSync(batPath)) {
1607
+ return { shellPath: batPath, powershellPath: existsSync(ps1Path) ? ps1Path : undefined };
1608
+ }
1609
+ if (existsSync(ps1Path)) {
1610
+ return { shellPath: ps1Path, powershellPath: ps1Path };
1611
+ }
1612
+ return null;
1613
+ }
1614
+
1615
+ const envPath = join(OPENCLAW_DIR, "openviking.env");
1616
+ return existsSync(envPath) ? { shellPath: envPath } : null;
1617
+ }
1618
+
1619
+ async function main() {
1620
+ console.log("");
1621
+ bold(tr("🦣 OpenClaw OpenViking plugin installer", "🦣 OpenClaw OpenViking 插件安装"));
1622
+ console.log("");
1623
+
1624
+ await selectWorkdir();
1625
+ if (showCurrentVersion) {
1626
+ await printCurrentVersionInfo();
1627
+ return;
1628
+ }
1629
+ if (rollbackLastUpgrade) {
1630
+ info(tr("Mode: rollback last plugin upgrade", "模式: 回滚最近一次插件升级"));
1631
+ if (pluginVersionExplicit) {
1632
+ warn("--plugin-version is ignored in --rollback mode.");
1633
+ }
1634
+ await rollbackLastUpgradeOperation();
1635
+ return;
1636
+ }
1637
+ await resolveDefaultPluginVersion();
1638
+ validateRequestedPluginVersion();
1639
+ info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
1640
+ info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
1641
+ info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
1642
+
1643
+ if (upgradePluginOnly) {
1644
+ info(tr("Mode: plugin upgrade only", "模式: 仅升级插件"));
1645
+ }
1646
+ info(tr(`Mode: ${selectedMode}`, `模式: ${selectedMode}`));
1647
+
1648
+ if (upgradePluginOnly) {
1649
+ await checkOpenClaw();
1650
+ await resolvePluginConfig();
1651
+ await checkOpenClawCompatibility();
1652
+ await prepareStrongPluginUpgrade();
1653
+ } else {
1654
+ await checkOpenClaw();
1655
+ await resolvePluginConfig();
1656
+ await checkOpenClawCompatibility();
1657
+ await collectRemoteConfig();
1658
+ }
1659
+
1660
+ await deployPluginFromRemote();
1661
+
1662
+ await configureOpenClawPlugin(
1663
+ upgradePluginOnly
1664
+ ? {
1665
+ runtimeConfig: upgradeRuntimeConfig,
1666
+ claimSlot: installedUpgradeState ? shouldClaimTargetSlot(installedUpgradeState) : true,
1667
+ }
1668
+ : { preserveExistingConfig: false },
1669
+ );
1670
+ await writeInstallStateFile({
1671
+ operation: upgradePluginOnly ? "upgrade" : "install",
1672
+ fromVersion: upgradeAudit?.fromVersion || "",
1673
+ configBackupPath: upgradeAudit?.configBackupPath || "",
1674
+ pluginBackups: upgradeAudit?.pluginBackups || [],
1675
+ });
1676
+ if (upgradeAudit) {
1677
+ upgradeAudit.completedAt = new Date().toISOString();
1678
+ await writeUpgradeAuditFile(upgradeAudit);
1679
+ }
1680
+ let envFiles = getExistingEnvFiles();
1681
+ if (!upgradePluginOnly) {
1682
+ envFiles = await writeOpenvikingEnv();
1683
+ } else if (!envFiles && OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
1684
+ envFiles = await writeOpenvikingEnv();
1685
+ }
1686
+
1687
+ console.log("");
1688
+ bold("═══════════════════════════════════════════════════════════");
1689
+ bold(` ${tr("Installation complete!", "安装完成!")}`);
1690
+ bold("═══════════════════════════════════════════════════════════");
1691
+ console.log("");
1692
+
1693
+ if (upgradeAudit) {
1694
+ info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
1695
+ info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
1696
+ for (const pluginBackup of upgradeAudit.pluginBackups || []) {
1697
+ info(tr(`Rollback plugin backup: ${pluginBackup.backupDir}`, `回滚插件备份: ${pluginBackup.backupDir}`));
1698
+ }
1699
+ info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
1700
+ console.log("");
1701
+ }
1702
+
1703
+ info(tr("Run these commands to start OpenClaw:", "请按以下命令启动 OpenClaw:"));
1704
+ console.log(` 1) ${wrapCommand("openclaw --version", envFiles)}`);
1705
+ console.log(` 2) ${wrapCommand("openclaw onboard", envFiles)}`);
1706
+ console.log(` 3) ${wrapCommand("openclaw gateway", envFiles)}`);
1707
+ console.log(` 4) ${wrapCommand("openclaw status", envFiles)}`);
1708
+ console.log("");
1709
+
1710
+ info(tr(`OpenViking server URL (plugin): ${remoteBaseUrl}`, `OpenViking 服务地址(插件): ${remoteBaseUrl}`));
1711
+ console.log("");
1712
+ }
1713
+
1714
+ main().catch((error) => {
1715
+ console.error(error);
1716
+ process.exit(1);
1717
+ });