openclaw-openviking-setup-helper 0.2.9-dev.0 → 0.2.9-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/install.js +986 -183
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* openclaw-openviking-install
|
|
11
11
|
*
|
|
12
12
|
* Direct run:
|
|
13
|
-
* node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ]
|
|
13
|
+
* node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ] [ --upgrade-plugin ]
|
|
14
14
|
* [ --plugin-version=TAG ] [ --openviking-version=V ] [ --repo=PATH ]
|
|
15
15
|
*
|
|
16
16
|
* Environment variables:
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { spawn } from "node:child_process";
|
|
26
|
-
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
27
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
28
|
-
import { dirname, join, relative } from "node:path";
|
|
26
|
+
import { cp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
27
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
28
|
+
import { dirname, join, relative } from "node:path";
|
|
29
29
|
import { createInterface } from "node:readline";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ let REPO = process.env.REPO || "volcengine/OpenViking";
|
|
|
35
35
|
// PLUGIN_VERSION takes precedence over BRANCH (legacy)
|
|
36
36
|
let PLUGIN_VERSION = process.env.PLUGIN_VERSION || process.env.BRANCH || "main";
|
|
37
37
|
const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmmirror.com";
|
|
38
|
-
const PIP_INDEX_URL = process.env.PIP_INDEX_URL || "https://mirrors.volces.com/pypi/simple/";
|
|
38
|
+
const PIP_INDEX_URL = process.env.PIP_INDEX_URL || "https://mirrors.volces.com/pypi/simple/";
|
|
39
39
|
|
|
40
40
|
const IS_WIN = process.platform === "win32";
|
|
41
41
|
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
@@ -61,38 +61,49 @@ const FALLBACK_LEGACY = {
|
|
|
61
61
|
optional: ["package-lock.json", ".gitignore"],
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
const FALLBACK_CURRENT = {
|
|
65
|
-
dir: "openclaw-plugin",
|
|
66
|
-
id: "openviking",
|
|
67
|
-
kind: "context-engine",
|
|
68
|
-
slot: "contextEngine",
|
|
69
|
-
required: ["index.ts", "
|
|
70
|
-
optional: ["package-lock.json", ".gitignore"],
|
|
71
|
-
};
|
|
64
|
+
const FALLBACK_CURRENT = {
|
|
65
|
+
dir: "openclaw-plugin",
|
|
66
|
+
id: "openviking",
|
|
67
|
+
kind: "context-engine",
|
|
68
|
+
slot: "contextEngine",
|
|
69
|
+
required: ["index.ts", "config.ts", "openclaw.plugin.json", "package.json"],
|
|
70
|
+
optional: ["context-engine.ts", "client.ts", "process-manager.ts", "memory-ranking.ts", "text-utils.ts", "session-transcript-repair.ts", "tool-call-id.ts", "tsconfig.json", "package-lock.json", ".gitignore"],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const PLUGIN_VARIANTS = [
|
|
74
|
+
{ ...FALLBACK_LEGACY, generation: "legacy", slotFallback: "none" },
|
|
75
|
+
{ ...FALLBACK_CURRENT, generation: "current", slotFallback: "legacy" },
|
|
76
|
+
];
|
|
72
77
|
|
|
73
78
|
// Resolved plugin config (set by resolvePluginConfig)
|
|
74
79
|
let resolvedPluginDir = "";
|
|
75
80
|
let resolvedPluginId = "";
|
|
76
81
|
let resolvedPluginKind = "";
|
|
77
82
|
let resolvedPluginSlot = "";
|
|
78
|
-
let resolvedFilesRequired = [];
|
|
83
|
+
let resolvedFilesRequired = [];
|
|
79
84
|
let resolvedFilesOptional = [];
|
|
80
85
|
let resolvedNpmOmitDev = true;
|
|
81
86
|
let resolvedMinOpenclawVersion = "";
|
|
82
87
|
let resolvedMinOpenvikingVersion = "";
|
|
88
|
+
let resolvedPluginReleaseId = "";
|
|
83
89
|
|
|
84
|
-
let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
|
|
85
|
-
let langZh = false;
|
|
86
|
-
let openvikingVersion = process.env.OPENVIKING_VERSION || "";
|
|
87
|
-
let openvikingRepo = process.env.OPENVIKING_REPO || "";
|
|
88
|
-
let workdirExplicit = false;
|
|
90
|
+
let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
|
|
91
|
+
let langZh = false;
|
|
92
|
+
let openvikingVersion = process.env.OPENVIKING_VERSION || "";
|
|
93
|
+
let openvikingRepo = process.env.OPENVIKING_REPO || "";
|
|
94
|
+
let workdirExplicit = false;
|
|
95
|
+
let upgradePluginOnly = false;
|
|
96
|
+
let rollbackLastUpgrade = false;
|
|
89
97
|
|
|
90
98
|
let selectedMode = "local";
|
|
91
99
|
let selectedServerPort = DEFAULT_SERVER_PORT;
|
|
92
|
-
let remoteBaseUrl = "http://127.0.0.1:1933";
|
|
93
|
-
let remoteApiKey = "";
|
|
94
|
-
let remoteAgentId = "";
|
|
95
|
-
let openvikingPythonPath = "";
|
|
100
|
+
let remoteBaseUrl = "http://127.0.0.1:1933";
|
|
101
|
+
let remoteApiKey = "";
|
|
102
|
+
let remoteAgentId = "";
|
|
103
|
+
let openvikingPythonPath = "";
|
|
104
|
+
let upgradeRuntimeConfig = null;
|
|
105
|
+
let installedUpgradeState = null;
|
|
106
|
+
let upgradeAudit = null;
|
|
96
107
|
|
|
97
108
|
const argv = process.argv.slice(2);
|
|
98
109
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -101,11 +112,19 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
101
112
|
installYes = true;
|
|
102
113
|
continue;
|
|
103
114
|
}
|
|
104
|
-
if (arg === "--zh") {
|
|
105
|
-
langZh = true;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (arg === "--
|
|
115
|
+
if (arg === "--zh") {
|
|
116
|
+
langZh = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (arg === "--upgrade-plugin" || arg === "--update" || arg === "--upgrade") {
|
|
120
|
+
upgradePluginOnly = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (arg === "--rollback" || arg === "--rollback-last-upgrade") {
|
|
124
|
+
rollbackLastUpgrade = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (arg === "--workdir") {
|
|
109
128
|
const workdir = argv[i + 1]?.trim();
|
|
110
129
|
if (!workdir) {
|
|
111
130
|
console.error("--workdir requires a path");
|
|
@@ -178,12 +197,16 @@ function printHelp() {
|
|
|
178
197
|
console.log("Options:");
|
|
179
198
|
console.log(" --github-repo=OWNER/REPO GitHub repository (default: volcengine/OpenViking)");
|
|
180
199
|
console.log(" --plugin-version=TAG Plugin version (Git tag, e.g. v0.2.9, default: main)");
|
|
181
|
-
console.log(" --openviking-version=V OpenViking PyPI version (e.g. 0.2.9, default: latest)");
|
|
182
|
-
console.log(" --workdir PATH OpenClaw config directory (default: ~/.openclaw)");
|
|
183
|
-
console.log(" --repo=PATH Use local OpenViking repo at PATH (pip -e + local plugin)");
|
|
184
|
-
console.log("
|
|
185
|
-
console.log("
|
|
186
|
-
console.log("
|
|
200
|
+
console.log(" --openviking-version=V OpenViking PyPI version (e.g. 0.2.9, default: latest)");
|
|
201
|
+
console.log(" --workdir PATH OpenClaw config directory (default: ~/.openclaw)");
|
|
202
|
+
console.log(" --repo=PATH Use local OpenViking repo at PATH (pip -e + local plugin)");
|
|
203
|
+
console.log(" --update, --upgrade-plugin");
|
|
204
|
+
console.log(" Backup old plugin, clean only OpenViking plugin config, keep ov.conf");
|
|
205
|
+
console.log(" --rollback, --rollback-last-upgrade");
|
|
206
|
+
console.log(" Roll back the last plugin upgrade using the saved audit/backup files");
|
|
207
|
+
console.log(" -y, --yes Non-interactive (use defaults)");
|
|
208
|
+
console.log(" --zh Chinese prompts");
|
|
209
|
+
console.log(" -h, --help This help");
|
|
187
210
|
console.log("");
|
|
188
211
|
console.log("Examples:");
|
|
189
212
|
console.log(" # Install latest version");
|
|
@@ -191,12 +214,18 @@ function printHelp() {
|
|
|
191
214
|
console.log("");
|
|
192
215
|
console.log(" # Install from a fork repository");
|
|
193
216
|
console.log(" node install.js --github-repo=yourname/OpenViking --plugin-version=dev-branch");
|
|
194
|
-
console.log("");
|
|
195
|
-
console.log(" # Install specific plugin version");
|
|
196
|
-
console.log(" node install.js --plugin-version=v0.2.8");
|
|
197
|
-
console.log("");
|
|
198
|
-
console.log("
|
|
199
|
-
|
|
217
|
+
console.log("");
|
|
218
|
+
console.log(" # Install specific plugin version");
|
|
219
|
+
console.log(" node install.js --plugin-version=v0.2.8");
|
|
220
|
+
console.log("");
|
|
221
|
+
console.log(" # Upgrade only the plugin files");
|
|
222
|
+
console.log(" node install.js --update --plugin-version=main");
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log(" # Roll back the last plugin upgrade");
|
|
225
|
+
console.log(" node install.js --rollback");
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log("Env: REPO, PLUGIN_VERSION, OPENVIKING_VERSION, SKIP_OPENCLAW, SKIP_OPENVIKING, NPM_REGISTRY, PIP_INDEX_URL");
|
|
228
|
+
}
|
|
200
229
|
|
|
201
230
|
function tr(en, zh) {
|
|
202
231
|
return langZh ? zh : en;
|
|
@@ -296,8 +325,19 @@ function question(prompt, defaultValue = "") {
|
|
|
296
325
|
});
|
|
297
326
|
}
|
|
298
327
|
|
|
328
|
+
async function resolveAbsoluteCommand(cmd) {
|
|
329
|
+
if (cmd.startsWith("/") || (IS_WIN && /^[A-Za-z]:[/\\]/.test(cmd))) return cmd;
|
|
330
|
+
if (IS_WIN) {
|
|
331
|
+
const r = await runCapture("where", [cmd], { shell: true });
|
|
332
|
+
return r.out.split(/\r?\n/)[0]?.trim() || cmd;
|
|
333
|
+
}
|
|
334
|
+
const r = await runCapture("which", [cmd], { shell: false });
|
|
335
|
+
return r.out.trim() || cmd;
|
|
336
|
+
}
|
|
337
|
+
|
|
299
338
|
async function checkPython() {
|
|
300
|
-
const
|
|
339
|
+
const raw = process.env.OPENVIKING_PYTHON || (IS_WIN ? "python" : "python3");
|
|
340
|
+
const py = await resolveAbsoluteCommand(raw);
|
|
301
341
|
const result = await runCapture(py, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"]);
|
|
302
342
|
if (result.code !== 0 || !result.out) {
|
|
303
343
|
return {
|
|
@@ -452,7 +492,7 @@ async function checkOpenClaw() {
|
|
|
452
492
|
}
|
|
453
493
|
|
|
454
494
|
// Compare versions: returns true if v1 >= v2
|
|
455
|
-
function versionGte(v1, v2) {
|
|
495
|
+
function versionGte(v1, v2) {
|
|
456
496
|
const parseVersion = (v) => {
|
|
457
497
|
const cleaned = v.replace(/^v/, "").replace(/-.*$/, "");
|
|
458
498
|
const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
@@ -463,15 +503,28 @@ function versionGte(v1, v2) {
|
|
|
463
503
|
const [b1, b2, b3] = parseVersion(v2);
|
|
464
504
|
if (a1 !== b1) return a1 > b1;
|
|
465
505
|
if (a2 !== b2) return a2 > b2;
|
|
466
|
-
return a3 >= b3;
|
|
467
|
-
}
|
|
468
|
-
|
|
506
|
+
return a3 >= b3;
|
|
507
|
+
}
|
|
508
|
+
|
|
469
509
|
function isSemverLike(value) {
|
|
470
510
|
return /^v?\d+(\.\d+){1,2}$/.test(value);
|
|
471
511
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
512
|
+
|
|
513
|
+
function validateRequestedPluginVersion() {
|
|
514
|
+
if (!isSemverLike(PLUGIN_VERSION)) return;
|
|
515
|
+
if (versionGte(PLUGIN_VERSION, "v0.2.7") && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
|
|
516
|
+
err(tr("Plugin version v0.2.7 does not exist.", "插件版本 v0.2.7 不存在。"));
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (upgradePluginOnly && rollbackLastUpgrade) {
|
|
522
|
+
console.error("--update/--upgrade-plugin and --rollback cannot be used together");
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Detect OpenClaw version
|
|
527
|
+
async function detectOpenClawVersion() {
|
|
475
528
|
try {
|
|
476
529
|
const result = await runCapture("openclaw", ["--version"], { shell: IS_WIN });
|
|
477
530
|
if (result.code === 0 && result.out) {
|
|
@@ -545,21 +598,45 @@ async function resolvePluginConfig() {
|
|
|
545
598
|
}
|
|
546
599
|
}
|
|
547
600
|
|
|
548
|
-
resolvedPluginDir = pluginDir;
|
|
549
|
-
|
|
601
|
+
resolvedPluginDir = pluginDir;
|
|
602
|
+
resolvedPluginReleaseId = "";
|
|
603
|
+
|
|
550
604
|
if (manifestData) {
|
|
551
605
|
resolvedPluginId = manifestData.plugin?.id || "";
|
|
552
606
|
resolvedPluginKind = manifestData.plugin?.kind || "";
|
|
553
607
|
resolvedPluginSlot = manifestData.plugin?.slot || "";
|
|
554
608
|
resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
|
|
555
609
|
resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
|
|
610
|
+
resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
|
|
556
611
|
resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
|
|
557
612
|
resolvedFilesRequired = manifestData.files?.required || [];
|
|
558
613
|
resolvedFilesOptional = manifestData.files?.optional || [];
|
|
559
614
|
} else {
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
615
|
+
// No manifest — determine plugin identity by package.json name
|
|
616
|
+
let fallbackKey = pluginDir === "openclaw-memory-plugin" ? "legacy" : "current";
|
|
617
|
+
let compatVer = "";
|
|
618
|
+
|
|
619
|
+
const pkgJson = await tryFetch(`${ghRaw}/examples/${pluginDir}/package.json`);
|
|
620
|
+
if (pkgJson) {
|
|
621
|
+
try {
|
|
622
|
+
const pkg = JSON.parse(pkgJson);
|
|
623
|
+
const pkgName = pkg.name || "";
|
|
624
|
+
resolvedPluginReleaseId = pkg.version || "";
|
|
625
|
+
if (pkgName && pkgName !== "@openclaw/openviking") {
|
|
626
|
+
fallbackKey = "legacy";
|
|
627
|
+
info(tr(`Detected legacy plugin by package name: ${pkgName}`, `通过 package.json 名称检测到旧版插件: ${pkgName}`));
|
|
628
|
+
} else if (pkgName) {
|
|
629
|
+
fallbackKey = "current";
|
|
630
|
+
}
|
|
631
|
+
compatVer = (pkg.engines?.openclaw || "").replace(/^>=?\s*/, "").trim();
|
|
632
|
+
if (compatVer) {
|
|
633
|
+
info(tr(`Read minOpenclawVersion from package.json engines.openclaw: >=${compatVer}`, `从 package.json engines.openclaw 读取到最低版本: >=${compatVer}`));
|
|
634
|
+
}
|
|
635
|
+
} catch {}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const fallback = fallbackKey === "legacy" ? FALLBACK_LEGACY : FALLBACK_CURRENT;
|
|
639
|
+
resolvedPluginDir = pluginDir;
|
|
563
640
|
resolvedPluginId = fallback.id;
|
|
564
641
|
resolvedPluginKind = fallback.kind;
|
|
565
642
|
resolvedPluginSlot = fallback.slot;
|
|
@@ -567,13 +644,24 @@ async function resolvePluginConfig() {
|
|
|
567
644
|
resolvedFilesOptional = fallback.optional;
|
|
568
645
|
resolvedNpmOmitDev = true;
|
|
569
646
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
647
|
+
// If no compatVer from package.json, try main branch manifest
|
|
648
|
+
if (!compatVer && PLUGIN_VERSION !== "main") {
|
|
649
|
+
const mainRaw = `https://raw.githubusercontent.com/${REPO}/main`;
|
|
650
|
+
const mainManifest = await tryFetch(`${mainRaw}/examples/openclaw-plugin/install-manifest.json`);
|
|
651
|
+
if (mainManifest) {
|
|
652
|
+
try {
|
|
653
|
+
const m = JSON.parse(mainManifest);
|
|
654
|
+
compatVer = m.compatibility?.minOpenclawVersion || "";
|
|
655
|
+
if (compatVer) {
|
|
656
|
+
info(tr(`Read minOpenclawVersion from main branch manifest: >=${compatVer}`, `从 main 分支 manifest 读取到最低版本: >=${compatVer}`));
|
|
657
|
+
}
|
|
658
|
+
} catch {}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
resolvedMinOpenclawVersion = compatVer || "2026.3.7";
|
|
663
|
+
resolvedMinOpenvikingVersion = "";
|
|
664
|
+
}
|
|
577
665
|
|
|
578
666
|
// Set plugin destination
|
|
579
667
|
PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId);
|
|
@@ -596,7 +684,7 @@ async function checkOpenClawCompatibility() {
|
|
|
596
684
|
}
|
|
597
685
|
|
|
598
686
|
// If user explicitly requested an old version, pass
|
|
599
|
-
if (PLUGIN_VERSION !== "main" && isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.
|
|
687
|
+
if (PLUGIN_VERSION !== "main" && isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
|
|
600
688
|
return;
|
|
601
689
|
}
|
|
602
690
|
|
|
@@ -610,32 +698,32 @@ async function checkOpenClawCompatibility() {
|
|
|
610
698
|
bold(tr("Please choose one of the following options:", "请选择以下方案之一:"));
|
|
611
699
|
console.log("");
|
|
612
700
|
console.log(` ${tr("Option 1: Upgrade OpenClaw", "方案 1:升级 OpenClaw")}`);
|
|
613
|
-
console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
|
|
701
|
+
console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
|
|
614
702
|
console.log("");
|
|
615
703
|
console.log(` ${tr("Option 2: Install legacy plugin (v0.2.8)", "方案 2:安装旧版插件 (v0.2.8)")}`);
|
|
616
|
-
console.log(` node install.js --plugin-version=v0.2.
|
|
704
|
+
console.log(` node install.js --plugin-version=v0.2.6${langZh ? " --zh" : ""}`);
|
|
617
705
|
console.log("");
|
|
618
706
|
process.exit(1);
|
|
619
707
|
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function checkRequestedOpenVikingCompatibility() {
|
|
623
|
-
if (!resolvedMinOpenvikingVersion || !openvikingVersion) return;
|
|
624
|
-
if (versionGte(openvikingVersion, resolvedMinOpenvikingVersion)) return;
|
|
625
|
-
|
|
626
|
-
err(tr(
|
|
627
|
-
`OpenViking ${openvikingVersion} does not support this plugin (requires >= ${resolvedMinOpenvikingVersion})`,
|
|
628
|
-
`OpenViking ${openvikingVersion} 不支持此插件(需要 >= ${resolvedMinOpenvikingVersion})`,
|
|
629
|
-
));
|
|
630
|
-
console.log("");
|
|
631
|
-
console.log(tr(
|
|
632
|
-
"Use a newer OpenViking version, or omit --openviking-version to install the latest release.",
|
|
633
|
-
"请使用更新版本的 OpenViking,或省略 --openviking-version 以安装最新版本。",
|
|
634
|
-
));
|
|
635
|
-
process.exit(1);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async function installOpenViking() {
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function checkRequestedOpenVikingCompatibility() {
|
|
711
|
+
if (!resolvedMinOpenvikingVersion || !openvikingVersion) return;
|
|
712
|
+
if (versionGte(openvikingVersion, resolvedMinOpenvikingVersion)) return;
|
|
713
|
+
|
|
714
|
+
err(tr(
|
|
715
|
+
`OpenViking ${openvikingVersion} does not support this plugin (requires >= ${resolvedMinOpenvikingVersion})`,
|
|
716
|
+
`OpenViking ${openvikingVersion} 不支持此插件(需要 >= ${resolvedMinOpenvikingVersion})`,
|
|
717
|
+
));
|
|
718
|
+
console.log("");
|
|
719
|
+
console.log(tr(
|
|
720
|
+
"Use a newer OpenViking version, or omit --openviking-version to install the latest release.",
|
|
721
|
+
"请使用更新版本的 OpenViking,或省略 --openviking-version 以安装最新版本。",
|
|
722
|
+
));
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function installOpenViking() {
|
|
639
727
|
if (process.env.SKIP_OPENVIKING === "1") {
|
|
640
728
|
info(tr("Skipping OpenViking install (SKIP_OPENVIKING=1)", "跳过 OpenViking 安装 (SKIP_OPENVIKING=1)"));
|
|
641
729
|
return;
|
|
@@ -646,6 +734,12 @@ async function installOpenViking() {
|
|
|
646
734
|
err(tr("Python check failed.", "Python 校验失败"));
|
|
647
735
|
process.exit(1);
|
|
648
736
|
}
|
|
737
|
+
if (!python.ok) {
|
|
738
|
+
warn(tr(
|
|
739
|
+
`${python.detail}. Will attempt to find a suitable Python for pip install.`,
|
|
740
|
+
`${python.detail}。将尝试查找合适的 Python 进行 pip 安装。`,
|
|
741
|
+
));
|
|
742
|
+
}
|
|
649
743
|
|
|
650
744
|
const py = python.cmd;
|
|
651
745
|
|
|
@@ -809,20 +903,20 @@ async function configureOvConf() {
|
|
|
809
903
|
rotation_days: 3,
|
|
810
904
|
rotation_interval: "midnight",
|
|
811
905
|
},
|
|
812
|
-
embedding: {
|
|
813
|
-
dense: {
|
|
814
|
-
provider: "volcengine",
|
|
815
|
-
api_key: embeddingApiKey || null,
|
|
816
|
-
model: embeddingModel,
|
|
817
|
-
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
906
|
+
embedding: {
|
|
907
|
+
dense: {
|
|
908
|
+
provider: "volcengine",
|
|
909
|
+
api_key: embeddingApiKey || null,
|
|
910
|
+
model: embeddingModel,
|
|
911
|
+
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
818
912
|
dimension: 1024,
|
|
819
913
|
input: "multimodal",
|
|
820
914
|
},
|
|
821
|
-
},
|
|
822
|
-
vlm: {
|
|
823
|
-
provider: "volcengine",
|
|
824
|
-
api_key: vlmApiKey || null,
|
|
825
|
-
model: vlmModel,
|
|
915
|
+
},
|
|
916
|
+
vlm: {
|
|
917
|
+
provider: "volcengine",
|
|
918
|
+
api_key: vlmApiKey || null,
|
|
919
|
+
model: vlmModel,
|
|
826
920
|
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
827
921
|
temperature: 0.1,
|
|
828
922
|
max_retries: 3,
|
|
@@ -834,8 +928,507 @@ async function configureOvConf() {
|
|
|
834
928
|
info(tr(`Config generated: ${configPath}`, `已生成配置: ${configPath}`));
|
|
835
929
|
}
|
|
836
930
|
|
|
837
|
-
|
|
838
|
-
|
|
931
|
+
function getOpenClawConfigPath() {
|
|
932
|
+
return join(OPENCLAW_DIR, "openclaw.json");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function getOpenClawEnv() {
|
|
936
|
+
if (OPENCLAW_DIR === DEFAULT_OPENCLAW_DIR) {
|
|
937
|
+
return { ...process.env };
|
|
938
|
+
}
|
|
939
|
+
return { ...process.env, OPENCLAW_STATE_DIR: OPENCLAW_DIR };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function readJsonFileIfExists(filePath) {
|
|
943
|
+
if (!existsSync(filePath)) return null;
|
|
944
|
+
const raw = await readFile(filePath, "utf8");
|
|
945
|
+
return JSON.parse(raw);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function getInstallStatePathForPlugin(pluginId) {
|
|
949
|
+
return join(OPENCLAW_DIR, "extensions", pluginId, ".ov-install-state.json");
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function getUpgradeAuditDir() {
|
|
953
|
+
return join(OPENCLAW_DIR, ".openviking-upgrade-backup");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function getUpgradeAuditPath() {
|
|
957
|
+
return join(getUpgradeAuditDir(), "last-upgrade.json");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function getOpenClawConfigBackupPath() {
|
|
961
|
+
return join(getUpgradeAuditDir(), "openclaw.json.bak");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function normalizePluginMode(value) {
|
|
965
|
+
return value === "remote" ? "remote" : "local";
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function getPluginVariantById(pluginId) {
|
|
969
|
+
return PLUGIN_VARIANTS.find((variant) => variant.id === pluginId) || null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function detectPluginPresence(config, variant) {
|
|
973
|
+
const plugins = config?.plugins;
|
|
974
|
+
const reasons = [];
|
|
975
|
+
if (!plugins) {
|
|
976
|
+
return { variant, present: false, reasons };
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
|
|
980
|
+
reasons.push("entry");
|
|
981
|
+
}
|
|
982
|
+
if (plugins.slots?.[variant.slot] === variant.id) {
|
|
983
|
+
reasons.push("slot");
|
|
984
|
+
}
|
|
985
|
+
if (Array.isArray(plugins.allow) && plugins.allow.includes(variant.id)) {
|
|
986
|
+
reasons.push("allow");
|
|
987
|
+
}
|
|
988
|
+
if (
|
|
989
|
+
Array.isArray(plugins.load?.paths)
|
|
990
|
+
&& plugins.load.paths.some((item) => typeof item === "string" && (item.includes(variant.id) || item.includes(variant.dir)))
|
|
991
|
+
) {
|
|
992
|
+
reasons.push("loadPath");
|
|
993
|
+
}
|
|
994
|
+
if (existsSync(join(OPENCLAW_DIR, "extensions", variant.id))) {
|
|
995
|
+
reasons.push("dir");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return { variant, present: reasons.length > 0, reasons };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function detectInstalledPluginState() {
|
|
1002
|
+
const configPath = getOpenClawConfigPath();
|
|
1003
|
+
const config = await readJsonFileIfExists(configPath);
|
|
1004
|
+
const detections = [];
|
|
1005
|
+
for (const variant of PLUGIN_VARIANTS) {
|
|
1006
|
+
const detection = detectPluginPresence(config, variant);
|
|
1007
|
+
if (!detection.present) continue;
|
|
1008
|
+
detection.installState = await readJsonFileIfExists(getInstallStatePathForPlugin(variant.id));
|
|
1009
|
+
detections.push(detection);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
let generation = "none";
|
|
1013
|
+
if (detections.length === 1) {
|
|
1014
|
+
generation = detections[0].variant.generation;
|
|
1015
|
+
} else if (detections.length > 1) {
|
|
1016
|
+
generation = "mixed";
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return {
|
|
1020
|
+
config,
|
|
1021
|
+
configPath,
|
|
1022
|
+
detections,
|
|
1023
|
+
generation,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function formatInstalledDetectionLabel(detection) {
|
|
1028
|
+
const requestedRef = detection.installState?.requestedRef;
|
|
1029
|
+
const releaseId = detection.installState?.releaseId;
|
|
1030
|
+
if (requestedRef) return `${detection.variant.id}@${requestedRef}`;
|
|
1031
|
+
if (releaseId) return `${detection.variant.id}#${releaseId}`;
|
|
1032
|
+
return `${detection.variant.id} (${detection.variant.generation}, exact version unknown)`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function formatInstalledStateLabel(installedState) {
|
|
1036
|
+
if (!installedState?.detections?.length) {
|
|
1037
|
+
return "not-installed";
|
|
1038
|
+
}
|
|
1039
|
+
return installedState.detections.map(formatInstalledDetectionLabel).join(" + ");
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function formatTargetVersionLabel() {
|
|
1043
|
+
const base = `${resolvedPluginId || "openviking"}@${PLUGIN_VERSION}`;
|
|
1044
|
+
if (resolvedPluginReleaseId && resolvedPluginReleaseId !== PLUGIN_VERSION) {
|
|
1045
|
+
return `${base} (${resolvedPluginReleaseId})`;
|
|
1046
|
+
}
|
|
1047
|
+
return base;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function extractRuntimeConfigFromPluginEntry(entryConfig) {
|
|
1051
|
+
if (!entryConfig || typeof entryConfig !== "object") return null;
|
|
1052
|
+
|
|
1053
|
+
const mode = normalizePluginMode(entryConfig.mode);
|
|
1054
|
+
const runtime = { mode };
|
|
1055
|
+
|
|
1056
|
+
if (mode === "remote") {
|
|
1057
|
+
if (typeof entryConfig.baseUrl === "string" && entryConfig.baseUrl.trim()) {
|
|
1058
|
+
runtime.baseUrl = entryConfig.baseUrl.trim();
|
|
1059
|
+
}
|
|
1060
|
+
if (typeof entryConfig.apiKey === "string" && entryConfig.apiKey.trim()) {
|
|
1061
|
+
runtime.apiKey = entryConfig.apiKey;
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof entryConfig.agentId === "string" && entryConfig.agentId.trim()) {
|
|
1064
|
+
runtime.agentId = entryConfig.agentId.trim();
|
|
1065
|
+
}
|
|
1066
|
+
return runtime;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (typeof entryConfig.configPath === "string" && entryConfig.configPath.trim()) {
|
|
1070
|
+
runtime.configPath = entryConfig.configPath.trim();
|
|
1071
|
+
}
|
|
1072
|
+
if (entryConfig.port !== undefined && entryConfig.port !== null && `${entryConfig.port}`.trim()) {
|
|
1073
|
+
const parsedPort = Number.parseInt(String(entryConfig.port), 10);
|
|
1074
|
+
if (Number.isFinite(parsedPort) && parsedPort > 0) {
|
|
1075
|
+
runtime.port = parsedPort;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return runtime;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function readPortFromOvConf(configPath) {
|
|
1082
|
+
const filePath = configPath || join(OPENVIKING_DIR, "ov.conf");
|
|
1083
|
+
if (!existsSync(filePath)) return null;
|
|
1084
|
+
try {
|
|
1085
|
+
const ovConf = await readJsonFileIfExists(filePath);
|
|
1086
|
+
const parsedPort = Number.parseInt(String(ovConf?.server?.port ?? ""), 10);
|
|
1087
|
+
return Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : null;
|
|
1088
|
+
} catch {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async function backupOpenClawConfig(configPath) {
|
|
1094
|
+
await mkdir(getUpgradeAuditDir(), { recursive: true });
|
|
1095
|
+
const backupPath = getOpenClawConfigBackupPath();
|
|
1096
|
+
const configText = await readFile(configPath, "utf8");
|
|
1097
|
+
await writeFile(backupPath, configText, "utf8");
|
|
1098
|
+
return backupPath;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function writeUpgradeAuditFile(data) {
|
|
1102
|
+
await mkdir(getUpgradeAuditDir(), { recursive: true });
|
|
1103
|
+
await writeFile(getUpgradeAuditPath(), `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
async function writeInstallStateFile({ operation, fromVersion, configBackupPath, pluginBackups }) {
|
|
1107
|
+
const installStatePath = getInstallStatePathForPlugin(resolvedPluginId || "openviking");
|
|
1108
|
+
const state = {
|
|
1109
|
+
pluginId: resolvedPluginId || "openviking",
|
|
1110
|
+
generation: getPluginVariantById(resolvedPluginId || "openviking")?.generation || "unknown",
|
|
1111
|
+
requestedRef: PLUGIN_VERSION,
|
|
1112
|
+
releaseId: resolvedPluginReleaseId || "",
|
|
1113
|
+
operation,
|
|
1114
|
+
fromVersion: fromVersion || "",
|
|
1115
|
+
configBackupPath: configBackupPath || "",
|
|
1116
|
+
pluginBackups: pluginBackups || [],
|
|
1117
|
+
installedAt: new Date().toISOString(),
|
|
1118
|
+
repo: REPO,
|
|
1119
|
+
};
|
|
1120
|
+
await writeFile(installStatePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function moveDirWithFallback(sourceDir, destDir) {
|
|
1124
|
+
try {
|
|
1125
|
+
await rename(sourceDir, destDir);
|
|
1126
|
+
} catch {
|
|
1127
|
+
await cp(sourceDir, destDir, { recursive: true, force: true });
|
|
1128
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async function rollbackLastUpgradeOperation() {
|
|
1133
|
+
const auditPath = getUpgradeAuditPath();
|
|
1134
|
+
const audit = await readJsonFileIfExists(auditPath);
|
|
1135
|
+
if (!audit) {
|
|
1136
|
+
err(
|
|
1137
|
+
tr(
|
|
1138
|
+
`No rollback audit file found at ${auditPath}.`,
|
|
1139
|
+
`未找到回滚审计文件: ${auditPath}`,
|
|
1140
|
+
),
|
|
1141
|
+
);
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (audit.rolledBackAt) {
|
|
1146
|
+
warn(
|
|
1147
|
+
tr(
|
|
1148
|
+
`The last recorded upgrade was already rolled back at ${audit.rolledBackAt}.`,
|
|
1149
|
+
`最近一次升级已在 ${audit.rolledBackAt} 回滚。`,
|
|
1150
|
+
),
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const configBackupPath = audit.configBackupPath || getOpenClawConfigBackupPath();
|
|
1155
|
+
if (!existsSync(configBackupPath)) {
|
|
1156
|
+
err(
|
|
1157
|
+
tr(
|
|
1158
|
+
`Rollback config backup is missing: ${configBackupPath}`,
|
|
1159
|
+
`回滚配置备份缺失: ${configBackupPath}`,
|
|
1160
|
+
),
|
|
1161
|
+
);
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const pluginBackups = Array.isArray(audit.pluginBackups) ? audit.pluginBackups : [];
|
|
1166
|
+
if (pluginBackups.length === 0) {
|
|
1167
|
+
err(tr("Rollback audit file contains no plugin backups.", "回滚审计文件中没有插件备份信息。"));
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
for (const pluginBackup of pluginBackups) {
|
|
1171
|
+
if (!pluginBackup?.pluginId || !pluginBackup?.backupDir || !existsSync(pluginBackup.backupDir)) {
|
|
1172
|
+
err(
|
|
1173
|
+
tr(
|
|
1174
|
+
`Rollback plugin backup is missing: ${pluginBackup?.backupDir || "<unknown>"}`,
|
|
1175
|
+
`回滚插件备份缺失: ${pluginBackup?.backupDir || "<unknown>"}`,
|
|
1176
|
+
),
|
|
1177
|
+
);
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
info(tr(`Rolling back last upgrade: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`, `开始回滚最近一次升级: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`));
|
|
1183
|
+
await stopOpenClawGatewayForUpgrade();
|
|
1184
|
+
|
|
1185
|
+
const configText = await readFile(configBackupPath, "utf8");
|
|
1186
|
+
await writeFile(getOpenClawConfigPath(), configText, "utf8");
|
|
1187
|
+
info(tr(`Restored openclaw.json from backup: ${configBackupPath}`, `已从备份恢复 openclaw.json: ${configBackupPath}`));
|
|
1188
|
+
|
|
1189
|
+
const extensionsDir = join(OPENCLAW_DIR, "extensions");
|
|
1190
|
+
await mkdir(extensionsDir, { recursive: true });
|
|
1191
|
+
for (const variant of PLUGIN_VARIANTS) {
|
|
1192
|
+
const liveDir = join(extensionsDir, variant.id);
|
|
1193
|
+
if (existsSync(liveDir)) {
|
|
1194
|
+
await rm(liveDir, { recursive: true, force: true });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
for (const pluginBackup of pluginBackups) {
|
|
1199
|
+
if (!pluginBackup?.pluginId || !pluginBackup?.backupDir) continue;
|
|
1200
|
+
if (!existsSync(pluginBackup.backupDir)) {
|
|
1201
|
+
err(
|
|
1202
|
+
tr(
|
|
1203
|
+
`Rollback plugin backup is missing: ${pluginBackup.backupDir}`,
|
|
1204
|
+
`回滚插件备份缺失: ${pluginBackup.backupDir}`,
|
|
1205
|
+
),
|
|
1206
|
+
);
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
const destDir = join(extensionsDir, pluginBackup.pluginId);
|
|
1210
|
+
await moveDirWithFallback(pluginBackup.backupDir, destDir);
|
|
1211
|
+
info(tr(`Restored plugin directory: ${destDir}`, `已恢复插件目录: ${destDir}`));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
audit.rolledBackAt = new Date().toISOString();
|
|
1215
|
+
audit.rollbackConfigPath = configBackupPath;
|
|
1216
|
+
await writeUpgradeAuditFile(audit);
|
|
1217
|
+
|
|
1218
|
+
console.log("");
|
|
1219
|
+
bold(tr("Rollback complete!", "回滚完成!"));
|
|
1220
|
+
console.log("");
|
|
1221
|
+
info(tr(`Rollback audit file: ${auditPath}`, `回滚审计文件: ${auditPath}`));
|
|
1222
|
+
info(tr("Run `openclaw gateway` and `openclaw status` to verify the restored plugin state.", "请运行 `openclaw gateway` 和 `openclaw status` 验证恢复后的插件状态。"));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async function prepareUpgradeRuntimeConfig(installedState) {
|
|
1226
|
+
const plugins = installedState.config?.plugins ?? {};
|
|
1227
|
+
const candidateOrder = installedState.detections
|
|
1228
|
+
.map((item) => item.variant)
|
|
1229
|
+
.sort((left, right) => (right.generation === "current" ? 1 : 0) - (left.generation === "current" ? 1 : 0));
|
|
1230
|
+
|
|
1231
|
+
let runtime = null;
|
|
1232
|
+
for (const variant of candidateOrder) {
|
|
1233
|
+
const entryConfig = extractRuntimeConfigFromPluginEntry(plugins.entries?.[variant.id]?.config);
|
|
1234
|
+
if (entryConfig) {
|
|
1235
|
+
runtime = entryConfig;
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (!runtime) {
|
|
1241
|
+
runtime = { mode: "local" };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (runtime.mode === "remote") {
|
|
1245
|
+
runtime.baseUrl = runtime.baseUrl || remoteBaseUrl;
|
|
1246
|
+
return runtime;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
runtime.configPath = runtime.configPath || join(OPENVIKING_DIR, "ov.conf");
|
|
1250
|
+
runtime.port = runtime.port || await readPortFromOvConf(runtime.configPath) || DEFAULT_SERVER_PORT;
|
|
1251
|
+
return runtime;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function removePluginConfig(config, variant) {
|
|
1255
|
+
const plugins = config?.plugins;
|
|
1256
|
+
if (!plugins) return false;
|
|
1257
|
+
|
|
1258
|
+
let changed = false;
|
|
1259
|
+
|
|
1260
|
+
if (Array.isArray(plugins.allow)) {
|
|
1261
|
+
const nextAllow = plugins.allow.filter((item) => item !== variant.id);
|
|
1262
|
+
changed = changed || nextAllow.length !== plugins.allow.length;
|
|
1263
|
+
plugins.allow = nextAllow;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (Array.isArray(plugins.load?.paths)) {
|
|
1267
|
+
const nextPaths = plugins.load.paths.filter(
|
|
1268
|
+
(item) => typeof item !== "string" || (!item.includes(variant.id) && !item.includes(variant.dir)),
|
|
1269
|
+
);
|
|
1270
|
+
changed = changed || nextPaths.length !== plugins.load.paths.length;
|
|
1271
|
+
plugins.load.paths = nextPaths;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
|
|
1275
|
+
delete plugins.entries[variant.id];
|
|
1276
|
+
changed = true;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (plugins.slots?.[variant.slot] === variant.id) {
|
|
1280
|
+
plugins.slots[variant.slot] = variant.slotFallback;
|
|
1281
|
+
changed = true;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
return changed;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async function prunePreviousUpgradeBackups(disabledDir, variant, keepDir) {
|
|
1288
|
+
if (!existsSync(disabledDir)) return;
|
|
1289
|
+
|
|
1290
|
+
const prefix = `${variant.id}-upgrade-backup-`;
|
|
1291
|
+
const keepName = keepDir ? keepDir.split(/[\\/]/).pop() : "";
|
|
1292
|
+
const entries = readdirSync(disabledDir, { withFileTypes: true });
|
|
1293
|
+
for (const entry of entries) {
|
|
1294
|
+
if (!entry.isDirectory()) continue;
|
|
1295
|
+
if (!entry.name.startsWith(prefix)) continue;
|
|
1296
|
+
if (keepName && entry.name === keepName) continue;
|
|
1297
|
+
await rm(join(disabledDir, entry.name), { recursive: true, force: true });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
async function backupPluginDirectory(variant) {
|
|
1302
|
+
const pluginDir = join(OPENCLAW_DIR, "extensions", variant.id);
|
|
1303
|
+
if (!existsSync(pluginDir)) return null;
|
|
1304
|
+
|
|
1305
|
+
const disabledDir = join(OPENCLAW_DIR, "disabled-extensions");
|
|
1306
|
+
const backupDir = join(disabledDir, `${variant.id}-upgrade-backup-${Date.now()}`);
|
|
1307
|
+
await mkdir(disabledDir, { recursive: true });
|
|
1308
|
+
try {
|
|
1309
|
+
await rename(pluginDir, backupDir);
|
|
1310
|
+
} catch {
|
|
1311
|
+
await cp(pluginDir, backupDir, { recursive: true, force: true });
|
|
1312
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
1313
|
+
}
|
|
1314
|
+
info(tr(`Backed up plugin directory: ${backupDir}`, `已备份插件目录: ${backupDir}`));
|
|
1315
|
+
await prunePreviousUpgradeBackups(disabledDir, variant, backupDir);
|
|
1316
|
+
return backupDir;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
async function stopOpenClawGatewayForUpgrade() {
|
|
1320
|
+
const result = await runCapture("openclaw", ["gateway", "stop"], {
|
|
1321
|
+
env: getOpenClawEnv(),
|
|
1322
|
+
shell: IS_WIN,
|
|
1323
|
+
});
|
|
1324
|
+
if (result.code === 0) {
|
|
1325
|
+
info(tr("Stopped OpenClaw gateway before plugin upgrade", "升级插件前已停止 OpenClaw gateway"));
|
|
1326
|
+
} else {
|
|
1327
|
+
warn(tr("OpenClaw gateway may not be running; continuing", "OpenClaw gateway 可能未在运行,继续执行"));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function shouldClaimTargetSlot(installedState) {
|
|
1332
|
+
const currentOwner = installedState.config?.plugins?.slots?.[resolvedPluginSlot];
|
|
1333
|
+
if (!currentOwner || currentOwner === "none" || currentOwner === "legacy" || currentOwner === resolvedPluginId) {
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
const currentOwnerVariant = getPluginVariantById(currentOwner);
|
|
1337
|
+
if (currentOwnerVariant && installedState.detections.some((item) => item.variant.id === currentOwnerVariant.id)) {
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
async function cleanupInstalledPluginConfig(installedState) {
|
|
1344
|
+
if (!installedState.config || !installedState.config.plugins) {
|
|
1345
|
+
warn(tr("openclaw.json has no plugins section; skipped targeted plugin cleanup", "openclaw.json 中没有 plugins 配置,已跳过定向插件清理"));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const nextConfig = structuredClone(installedState.config);
|
|
1350
|
+
let changed = false;
|
|
1351
|
+
for (const detection of installedState.detections) {
|
|
1352
|
+
changed = removePluginConfig(nextConfig, detection.variant) || changed;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (!changed) {
|
|
1356
|
+
info(tr("No OpenViking plugin config changes were required", "无需修改 OpenViking 插件配置"));
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
await writeFile(installedState.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
1361
|
+
info(tr("Cleaned existing OpenViking plugin config only", "已仅清理 OpenViking 自身插件配置"));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
async function prepareStrongPluginUpgrade() {
|
|
1365
|
+
const installedState = await detectInstalledPluginState();
|
|
1366
|
+
if (installedState.generation === "none") {
|
|
1367
|
+
err(
|
|
1368
|
+
tr(
|
|
1369
|
+
"Plugin upgrade mode requires an existing OpenViking plugin entry in openclaw.json.",
|
|
1370
|
+
"插件升级模式要求 openclaw.json 中已经存在 OpenViking 插件记录。",
|
|
1371
|
+
),
|
|
1372
|
+
);
|
|
1373
|
+
process.exit(1);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
installedUpgradeState = installedState;
|
|
1377
|
+
upgradeRuntimeConfig = await prepareUpgradeRuntimeConfig(installedState);
|
|
1378
|
+
const fromVersion = formatInstalledStateLabel(installedState);
|
|
1379
|
+
const toVersion = formatTargetVersionLabel();
|
|
1380
|
+
selectedMode = upgradeRuntimeConfig.mode;
|
|
1381
|
+
info(
|
|
1382
|
+
tr(
|
|
1383
|
+
`Detected installed OpenViking plugin state: ${installedState.generation}`,
|
|
1384
|
+
`检测到已安装 OpenViking 插件状态: ${installedState.generation}`,
|
|
1385
|
+
),
|
|
1386
|
+
);
|
|
1387
|
+
if (upgradeRuntimeConfig.mode === "remote") {
|
|
1388
|
+
remoteBaseUrl = upgradeRuntimeConfig.baseUrl || remoteBaseUrl;
|
|
1389
|
+
remoteApiKey = upgradeRuntimeConfig.apiKey || "";
|
|
1390
|
+
remoteAgentId = upgradeRuntimeConfig.agentId || "";
|
|
1391
|
+
} else {
|
|
1392
|
+
selectedServerPort = upgradeRuntimeConfig.port || DEFAULT_SERVER_PORT;
|
|
1393
|
+
}
|
|
1394
|
+
info(tr(`Upgrade runtime mode: ${selectedMode}`, `升级运行模式: ${selectedMode}`));
|
|
1395
|
+
|
|
1396
|
+
info(tr(`Upgrade path: ${fromVersion} -> ${toVersion}`, `升级路径: ${fromVersion} -> ${toVersion}`));
|
|
1397
|
+
|
|
1398
|
+
await stopOpenClawGatewayForUpgrade();
|
|
1399
|
+
const configBackupPath = await backupOpenClawConfig(installedState.configPath);
|
|
1400
|
+
info(tr(`Backed up openclaw.json: ${configBackupPath}`, `已备份 openclaw.json: ${configBackupPath}`));
|
|
1401
|
+
const pluginBackups = [];
|
|
1402
|
+
for (const detection of installedState.detections) {
|
|
1403
|
+
const backupDir = await backupPluginDirectory(detection.variant);
|
|
1404
|
+
if (backupDir) {
|
|
1405
|
+
pluginBackups.push({ pluginId: detection.variant.id, backupDir });
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
upgradeAudit = {
|
|
1409
|
+
operation: "upgrade",
|
|
1410
|
+
createdAt: new Date().toISOString(),
|
|
1411
|
+
fromVersion,
|
|
1412
|
+
toVersion,
|
|
1413
|
+
configBackupPath,
|
|
1414
|
+
pluginBackups,
|
|
1415
|
+
runtimeMode: selectedMode,
|
|
1416
|
+
};
|
|
1417
|
+
await writeUpgradeAuditFile(upgradeAudit);
|
|
1418
|
+
await cleanupInstalledPluginConfig(installedState);
|
|
1419
|
+
|
|
1420
|
+
info(
|
|
1421
|
+
tr(
|
|
1422
|
+
"Upgrade will keep the existing OpenViking runtime file and re-apply only the minimum plugin runtime settings.",
|
|
1423
|
+
"升级将保留现有 OpenViking 运行时文件,并只回填最小插件运行配置。",
|
|
1424
|
+
),
|
|
1425
|
+
);
|
|
1426
|
+
info(tr(`Upgrade audit file: ${getUpgradeAuditPath()}`, `升级审计文件: ${getUpgradeAuditPath()}`));
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async function downloadPluginFile(destDir, fileName, url, required, index, total) {
|
|
1430
|
+
const maxRetries = 3;
|
|
1431
|
+
const destPath = join(destDir, fileName);
|
|
839
1432
|
|
|
840
1433
|
process.stdout.write(` [${index}/${total}] ${fileName} `);
|
|
841
1434
|
|
|
@@ -844,7 +1437,8 @@ async function downloadPluginFile(fileName, url, required, index, total) {
|
|
|
844
1437
|
const response = await fetch(url);
|
|
845
1438
|
if (response.ok) {
|
|
846
1439
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
847
|
-
await
|
|
1440
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
1441
|
+
await writeFile(destPath, buffer);
|
|
848
1442
|
console.log("✓");
|
|
849
1443
|
return;
|
|
850
1444
|
}
|
|
@@ -861,7 +1455,8 @@ async function downloadPluginFile(fileName, url, required, index, total) {
|
|
|
861
1455
|
|
|
862
1456
|
if (fileName === ".gitignore") {
|
|
863
1457
|
console.log(tr("(retries failed, using minimal .gitignore)", "(重试失败,使用最小 .gitignore)"));
|
|
864
|
-
await
|
|
1458
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
1459
|
+
await writeFile(destPath, "node_modules/\n", "utf8");
|
|
865
1460
|
return;
|
|
866
1461
|
}
|
|
867
1462
|
|
|
@@ -875,12 +1470,12 @@ async function downloadPluginFile(fileName, url, required, index, total) {
|
|
|
875
1470
|
process.exit(1);
|
|
876
1471
|
}
|
|
877
1472
|
|
|
878
|
-
async function downloadPlugin() {
|
|
1473
|
+
async function downloadPlugin(destDir) {
|
|
879
1474
|
const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
|
|
880
1475
|
const pluginDir = resolvedPluginDir;
|
|
881
1476
|
const total = resolvedFilesRequired.length + resolvedFilesOptional.length;
|
|
882
1477
|
|
|
883
|
-
await mkdir(
|
|
1478
|
+
await mkdir(destDir, { recursive: true });
|
|
884
1479
|
|
|
885
1480
|
info(tr(`Downloading plugin from ${REPO}@${PLUGIN_VERSION} (${total} files)...`, `正在从 ${REPO}@${PLUGIN_VERSION} 下载插件(共 ${total} 个文件)...`));
|
|
886
1481
|
|
|
@@ -890,7 +1485,7 @@ async function downloadPlugin() {
|
|
|
890
1485
|
if (!name) continue;
|
|
891
1486
|
i++;
|
|
892
1487
|
const url = `${ghRaw}/examples/${pluginDir}/${name}`;
|
|
893
|
-
await downloadPluginFile(name, url, true, i, total);
|
|
1488
|
+
await downloadPluginFile(destDir, name, url, true, i, total);
|
|
894
1489
|
}
|
|
895
1490
|
|
|
896
1491
|
// Download optional files
|
|
@@ -898,93 +1493,187 @@ async function downloadPlugin() {
|
|
|
898
1493
|
if (!name) continue;
|
|
899
1494
|
i++;
|
|
900
1495
|
const url = `${ghRaw}/examples/${pluginDir}/${name}`;
|
|
901
|
-
await downloadPluginFile(name, url, false, i, total);
|
|
1496
|
+
await downloadPluginFile(destDir, name, url, false, i, total);
|
|
902
1497
|
}
|
|
903
1498
|
|
|
904
1499
|
// npm install
|
|
905
1500
|
info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
|
|
906
|
-
const npmArgs = resolvedNpmOmitDev
|
|
907
|
-
? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
|
|
908
|
-
: ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
|
|
909
|
-
await run("npm", npmArgs, { cwd:
|
|
1501
|
+
const npmArgs = resolvedNpmOmitDev
|
|
1502
|
+
? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
|
|
1503
|
+
: ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
|
|
1504
|
+
await run("npm", npmArgs, { cwd: destDir, silent: false });
|
|
910
1505
|
info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
|
|
911
1506
|
}
|
|
912
1507
|
|
|
913
|
-
async function deployLocalPlugin(localPluginDir) {
|
|
914
|
-
await rm(
|
|
915
|
-
await mkdir(
|
|
916
|
-
await cp(localPluginDir,
|
|
917
|
-
recursive: true,
|
|
918
|
-
force: true,
|
|
919
|
-
filter: (sourcePath) => {
|
|
920
|
-
const rel = relative(localPluginDir, sourcePath);
|
|
921
|
-
if (!rel) return true;
|
|
922
|
-
const firstSegment = rel.split(/[\\/]/)[0];
|
|
1508
|
+
async function deployLocalPlugin(localPluginDir, destDir) {
|
|
1509
|
+
await rm(destDir, { recursive: true, force: true });
|
|
1510
|
+
await mkdir(destDir, { recursive: true });
|
|
1511
|
+
await cp(localPluginDir, destDir, {
|
|
1512
|
+
recursive: true,
|
|
1513
|
+
force: true,
|
|
1514
|
+
filter: (sourcePath) => {
|
|
1515
|
+
const rel = relative(localPluginDir, sourcePath);
|
|
1516
|
+
if (!rel) return true;
|
|
1517
|
+
const firstSegment = rel.split(/[\\/]/)[0];
|
|
923
1518
|
return firstSegment !== "node_modules" && firstSegment !== ".git";
|
|
924
1519
|
},
|
|
925
1520
|
});
|
|
926
1521
|
}
|
|
927
1522
|
|
|
928
|
-
async function
|
|
1523
|
+
async function installPluginDependencies(destDir) {
|
|
1524
|
+
info(tr("Installing plugin npm dependencies...", "姝e湪瀹夎鎻掍欢 npm 渚濊禆..."));
|
|
1525
|
+
const npmArgs = resolvedNpmOmitDev
|
|
1526
|
+
? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
|
|
1527
|
+
: ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
|
|
1528
|
+
await run("npm", npmArgs, { cwd: destDir, silent: false });
|
|
1529
|
+
return info(`Plugin prepared: ${destDir}`);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
async function createPluginStagingDir() {
|
|
1533
|
+
const pluginId = resolvedPluginId || "openviking";
|
|
1534
|
+
const extensionsDir = join(OPENCLAW_DIR, "extensions");
|
|
1535
|
+
const stagingDir = join(extensionsDir, `.${pluginId}.staging-${process.pid}-${Date.now()}`);
|
|
1536
|
+
await mkdir(extensionsDir, { recursive: true });
|
|
1537
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1538
|
+
await mkdir(stagingDir, { recursive: true });
|
|
1539
|
+
return stagingDir;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
async function finalizePluginDeployment(stagingDir) {
|
|
1543
|
+
await rm(PLUGIN_DEST, { recursive: true, force: true });
|
|
1544
|
+
try {
|
|
1545
|
+
await rename(stagingDir, PLUGIN_DEST);
|
|
1546
|
+
} catch {
|
|
1547
|
+
await cp(stagingDir, PLUGIN_DEST, { recursive: true, force: true });
|
|
1548
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1549
|
+
}
|
|
1550
|
+
return info(`Plugin deployed: ${PLUGIN_DEST}`);
|
|
1551
|
+
info(tr(`Plugin prepared: ${destDir}`, `鎻掍欢宸插噯澶? ${destDir}`));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async function deployPluginFromRemote() {
|
|
1555
|
+
const stagingDir = await createPluginStagingDir();
|
|
1556
|
+
try {
|
|
1557
|
+
await downloadPlugin(stagingDir);
|
|
1558
|
+
await finalizePluginDeployment(stagingDir);
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1561
|
+
throw error;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
async function deployPluginFromLocal(localPluginDir) {
|
|
1566
|
+
const stagingDir = await createPluginStagingDir();
|
|
1567
|
+
try {
|
|
1568
|
+
await deployLocalPlugin(localPluginDir, stagingDir);
|
|
1569
|
+
await installPluginDependencies(stagingDir);
|
|
1570
|
+
await finalizePluginDeployment(stagingDir);
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1573
|
+
throw error;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
async function configureOpenClawPlugin({
|
|
1578
|
+
preserveExistingConfig = false,
|
|
1579
|
+
runtimeConfig = null,
|
|
1580
|
+
skipGatewayMode = false,
|
|
1581
|
+
claimSlot = true,
|
|
1582
|
+
} = {}) {
|
|
929
1583
|
info(tr("Configuring OpenClaw plugin...", "正在配置 OpenClaw 插件..."));
|
|
930
1584
|
|
|
931
1585
|
const pluginId = resolvedPluginId;
|
|
932
1586
|
const pluginSlot = resolvedPluginSlot;
|
|
933
1587
|
|
|
934
|
-
const ocEnv =
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1588
|
+
const ocEnv = getOpenClawEnv();
|
|
1589
|
+
|
|
1590
|
+
const oc = async (args) => {
|
|
1591
|
+
const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
|
|
1592
|
+
if (result.code !== 0) {
|
|
1593
|
+
const detail = result.err || result.out;
|
|
1594
|
+
throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
|
|
1595
|
+
}
|
|
1596
|
+
return result;
|
|
1597
|
+
};
|
|
938
1598
|
|
|
939
|
-
const oc = async (args) => {
|
|
940
|
-
const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
|
|
941
|
-
if (result.code !== 0) {
|
|
942
|
-
const detail = result.err || result.out;
|
|
943
|
-
throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
|
|
944
|
-
}
|
|
945
|
-
return result;
|
|
946
|
-
};
|
|
947
|
-
|
|
948
1599
|
// Enable plugin (files already deployed to extensions dir by deployPlugin)
|
|
949
1600
|
await oc(["plugins", "enable", pluginId]);
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1601
|
+
if (claimSlot) {
|
|
1602
|
+
await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
|
|
1603
|
+
} else {
|
|
1604
|
+
warn(
|
|
1605
|
+
tr(
|
|
1606
|
+
`Skipped claiming plugins.slots.${pluginSlot}; it is currently owned by another plugin.`,
|
|
1607
|
+
`已跳过设置 plugins.slots.${pluginSlot},当前该 slot 由其他插件占用。`,
|
|
1608
|
+
),
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
if (preserveExistingConfig) {
|
|
1613
|
+
info(
|
|
1614
|
+
tr(
|
|
1615
|
+
`Preserved existing plugin runtime config for ${pluginId}`,
|
|
1616
|
+
`宸蹭繚鐣?${pluginId} 鐨勭幇鏈夋彃浠惰繍琛岄厤缃?`,
|
|
1617
|
+
),
|
|
1618
|
+
);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const effectiveRuntimeConfig = runtimeConfig || (
|
|
1623
|
+
selectedMode === "remote"
|
|
1624
|
+
? { mode: "remote", baseUrl: remoteBaseUrl, apiKey: remoteApiKey, agentId: remoteAgentId }
|
|
1625
|
+
: { mode: "local", configPath: join(OPENVIKING_DIR, "ov.conf"), port: selectedServerPort }
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1628
|
+
if (!skipGatewayMode) {
|
|
1629
|
+
await oc(["config", "set", "gateway.mode", effectiveRuntimeConfig.mode === "remote" ? "remote" : "local"]);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Set plugin config for the selected mode
|
|
1633
|
+
if (effectiveRuntimeConfig.mode === "local") {
|
|
1634
|
+
const ovConfPath = effectiveRuntimeConfig.configPath || join(OPENVIKING_DIR, "ov.conf");
|
|
1635
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "local"]);
|
|
1636
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.configPath`, ovConfPath]);
|
|
1637
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.port`, String(effectiveRuntimeConfig.port || DEFAULT_SERVER_PORT)]);
|
|
1638
|
+
} else {
|
|
1639
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "remote"]);
|
|
1640
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, effectiveRuntimeConfig.baseUrl || remoteBaseUrl]);
|
|
1641
|
+
if (effectiveRuntimeConfig.apiKey) {
|
|
1642
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, effectiveRuntimeConfig.apiKey]);
|
|
1643
|
+
}
|
|
1644
|
+
if (effectiveRuntimeConfig.agentId) {
|
|
1645
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.agentId`, effectiveRuntimeConfig.agentId]);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
954
1648
|
|
|
955
|
-
//
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
await oc(["config", "set", `plugins.entries.${pluginId}.config.
|
|
959
|
-
await oc(["config", "set", `plugins.entries.${pluginId}.config.
|
|
960
|
-
await oc(["config", "set", `plugins.entries.${pluginId}.config.port`, String(selectedServerPort)]);
|
|
961
|
-
} else {
|
|
962
|
-
await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "remote"]);
|
|
963
|
-
await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, remoteBaseUrl]);
|
|
964
|
-
if (remoteApiKey) {
|
|
965
|
-
await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, remoteApiKey]);
|
|
966
|
-
}
|
|
967
|
-
if (remoteAgentId) {
|
|
968
|
-
await oc(["config", "set", `plugins.entries.${pluginId}.config.agentId`, remoteAgentId]);
|
|
969
|
-
}
|
|
1649
|
+
// Legacy (memory) plugins need explicit targetUri/autoRecall/autoCapture (new version has defaults in config.ts)
|
|
1650
|
+
if (resolvedPluginKind === "memory") {
|
|
1651
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.targetUri`, "viking://user/memories"]);
|
|
1652
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.autoRecall`, "true", "--json"]);
|
|
1653
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.autoCapture`, "true", "--json"]);
|
|
970
1654
|
}
|
|
971
1655
|
|
|
972
1656
|
info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
|
|
973
1657
|
}
|
|
974
1658
|
|
|
1659
|
+
async function discoverOpenvikingPython(failedPy) {
|
|
1660
|
+
const candidates = IS_WIN
|
|
1661
|
+
? ["python3", "python", "py -3"]
|
|
1662
|
+
: ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
|
|
1663
|
+
for (const candidate of candidates) {
|
|
1664
|
+
if (candidate === failedPy) continue;
|
|
1665
|
+
const resolved = await resolveAbsoluteCommand(candidate);
|
|
1666
|
+
if (!resolved || resolved === candidate || resolved === failedPy) continue;
|
|
1667
|
+
const check = await runCapture(resolved, ["-c", "import openviking"], { shell: false });
|
|
1668
|
+
if (check.code === 0) return resolved;
|
|
1669
|
+
}
|
|
1670
|
+
return "";
|
|
1671
|
+
}
|
|
1672
|
+
|
|
975
1673
|
async function resolvePythonPath() {
|
|
976
1674
|
if (openvikingPythonPath) return openvikingPythonPath;
|
|
977
1675
|
const python = await checkPython();
|
|
978
|
-
|
|
979
|
-
if (!py) return "";
|
|
980
|
-
|
|
981
|
-
if (IS_WIN) {
|
|
982
|
-
const result = await runCapture("where", [py], { shell: true });
|
|
983
|
-
return result.out.split(/\r?\n/)[0]?.trim() || py;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
const result = await runCapture("which", [py], { shell: false });
|
|
987
|
-
return result.out.trim() || py;
|
|
1676
|
+
return python.cmd || "";
|
|
988
1677
|
}
|
|
989
1678
|
|
|
990
1679
|
async function writeOpenvikingEnv({ includePython }) {
|
|
@@ -1001,6 +1690,37 @@ async function writeOpenvikingEnv({ includePython }) {
|
|
|
1001
1690
|
),
|
|
1002
1691
|
);
|
|
1003
1692
|
}
|
|
1693
|
+
|
|
1694
|
+
// Verify the resolved Python can actually import openviking
|
|
1695
|
+
if (pythonPath) {
|
|
1696
|
+
const verify = await runCapture(pythonPath, ["-c", "import openviking"], { shell: false });
|
|
1697
|
+
if (verify.code !== 0) {
|
|
1698
|
+
warn(
|
|
1699
|
+
tr(
|
|
1700
|
+
`Resolved Python (${pythonPath}) cannot import openviking. The pip install target may differ from the runtime python3.`,
|
|
1701
|
+
`解析到的 Python(${pythonPath})无法 import openviking。pip 安装目标可能与运行时的 python3 不一致。`,
|
|
1702
|
+
),
|
|
1703
|
+
);
|
|
1704
|
+
// Try to discover the correct Python via pip show
|
|
1705
|
+
const corrected = await discoverOpenvikingPython(pythonPath);
|
|
1706
|
+
if (corrected) {
|
|
1707
|
+
info(
|
|
1708
|
+
tr(
|
|
1709
|
+
`Auto-corrected OPENVIKING_PYTHON to ${corrected}`,
|
|
1710
|
+
`已自动修正 OPENVIKING_PYTHON 为 ${corrected}`,
|
|
1711
|
+
),
|
|
1712
|
+
);
|
|
1713
|
+
pythonPath = corrected;
|
|
1714
|
+
} else {
|
|
1715
|
+
warn(
|
|
1716
|
+
tr(
|
|
1717
|
+
`Could not auto-detect the correct Python. Edit OPENVIKING_PYTHON in the env file manually.`,
|
|
1718
|
+
`无法自动检测正确的 Python。请手动修改 env 文件中的 OPENVIKING_PYTHON。`,
|
|
1719
|
+
),
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1004
1724
|
}
|
|
1005
1725
|
|
|
1006
1726
|
// Remote mode + default state dir + no python line → nothing to persist
|
|
@@ -1044,18 +1764,59 @@ async function writeOpenvikingEnv({ includePython }) {
|
|
|
1044
1764
|
return { shellPath: envPath };
|
|
1045
1765
|
}
|
|
1046
1766
|
|
|
1047
|
-
function wrapCommand(command, envFiles) {
|
|
1048
|
-
if (!envFiles) return command;
|
|
1049
|
-
if (IS_WIN) return `call "${envFiles.shellPath}" && ${command}`;
|
|
1050
|
-
return `source '${envFiles.shellPath.replace(/'/g, "'\"'\"'")}' && ${command}`;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1767
|
+
function wrapCommand(command, envFiles) {
|
|
1768
|
+
if (!envFiles) return command;
|
|
1769
|
+
if (IS_WIN) return `call "${envFiles.shellPath}" && ${command}`;
|
|
1770
|
+
return `source '${envFiles.shellPath.replace(/'/g, "'\"'\"'")}' && ${command}`;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function getExistingEnvFiles() {
|
|
1774
|
+
if (IS_WIN) {
|
|
1775
|
+
const batPath = join(OPENCLAW_DIR, "openviking.env.bat");
|
|
1776
|
+
const ps1Path = join(OPENCLAW_DIR, "openviking.env.ps1");
|
|
1777
|
+
if (existsSync(batPath)) {
|
|
1778
|
+
return { shellPath: batPath, powershellPath: existsSync(ps1Path) ? ps1Path : undefined };
|
|
1779
|
+
}
|
|
1780
|
+
if (existsSync(ps1Path)) {
|
|
1781
|
+
return { shellPath: ps1Path, powershellPath: ps1Path };
|
|
1782
|
+
}
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const envPath = join(OPENCLAW_DIR, "openviking.env");
|
|
1787
|
+
return existsSync(envPath) ? { shellPath: envPath } : null;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function ensureExistingPluginForUpgrade() {
|
|
1791
|
+
if (!existsSync(PLUGIN_DEST)) {
|
|
1792
|
+
err(
|
|
1793
|
+
tr(
|
|
1794
|
+
`Plugin upgrade mode expects an existing plugin at ${PLUGIN_DEST}. Run the full installer first if this is a fresh install.`,
|
|
1795
|
+
`鎻掍欢鍗囩骇妯″紡瑕佹眰鐜版湁鎻掍欢宸插畨瑁呭湪 ${PLUGIN_DEST}銆傚鏄娆″畨瑁咃紝璇疯繍琛屽叏閲忓畨瑁呮祦绋嬨€?`,
|
|
1796
|
+
),
|
|
1797
|
+
);
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
async function main() {
|
|
1054
1803
|
console.log("");
|
|
1055
1804
|
bold(tr("🦣 OpenClaw + OpenViking Installer", "🦣 OpenClaw + OpenViking 一键安装"));
|
|
1056
1805
|
console.log("");
|
|
1057
1806
|
|
|
1058
|
-
await selectWorkdir();
|
|
1807
|
+
await selectWorkdir();
|
|
1808
|
+
if (rollbackLastUpgrade) {
|
|
1809
|
+
info(tr("Mode: rollback last plugin upgrade", "模式: 回滚最近一次插件升级"));
|
|
1810
|
+
if (PLUGIN_VERSION !== "main") {
|
|
1811
|
+
warn("--plugin-version is ignored in --rollback mode.");
|
|
1812
|
+
}
|
|
1813
|
+
if (openvikingVersion) {
|
|
1814
|
+
warn("--openviking-version is ignored in --rollback mode.");
|
|
1815
|
+
}
|
|
1816
|
+
await rollbackLastUpgradeOperation();
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
validateRequestedPluginVersion();
|
|
1059
1820
|
info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
|
|
1060
1821
|
info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
|
|
1061
1822
|
info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
|
|
@@ -1063,18 +1824,31 @@ async function main() {
|
|
|
1063
1824
|
info(tr(`OpenViking version: ${openvikingVersion}`, `OpenViking 版本: ${openvikingVersion}`));
|
|
1064
1825
|
}
|
|
1065
1826
|
|
|
1066
|
-
|
|
1827
|
+
if (upgradePluginOnly) {
|
|
1828
|
+
selectedMode = "local";
|
|
1829
|
+
info("Mode: plugin upgrade only (backup old plugin, clean only OpenViking plugin config, keep ov.conf)");
|
|
1830
|
+
if (openvikingVersion) {
|
|
1831
|
+
warn("--openviking-version is ignored in --upgrade-plugin mode.");
|
|
1832
|
+
}
|
|
1833
|
+
} else {
|
|
1834
|
+
await selectMode();
|
|
1835
|
+
}
|
|
1067
1836
|
info(tr(`Mode: ${selectedMode}`, `模式: ${selectedMode}`));
|
|
1068
1837
|
|
|
1069
|
-
if (
|
|
1070
|
-
await
|
|
1071
|
-
await
|
|
1838
|
+
if (upgradePluginOnly) {
|
|
1839
|
+
await checkOpenClaw();
|
|
1840
|
+
await resolvePluginConfig();
|
|
1841
|
+
await checkOpenClawCompatibility();
|
|
1842
|
+
await prepareStrongPluginUpgrade();
|
|
1843
|
+
} else if (selectedMode === "local") {
|
|
1844
|
+
await validateEnvironment();
|
|
1845
|
+
await checkOpenClaw();
|
|
1072
1846
|
// Resolve plugin config after OpenClaw is available (for version detection)
|
|
1073
1847
|
await resolvePluginConfig();
|
|
1074
1848
|
await checkOpenClawCompatibility();
|
|
1075
1849
|
checkRequestedOpenVikingCompatibility();
|
|
1076
1850
|
await installOpenViking();
|
|
1077
|
-
await configureOvConf();
|
|
1851
|
+
await configureOvConf();
|
|
1078
1852
|
} else {
|
|
1079
1853
|
await checkOpenClaw();
|
|
1080
1854
|
await resolvePluginConfig();
|
|
@@ -1088,22 +1862,41 @@ async function main() {
|
|
|
1088
1862
|
pluginPath = localPluginDir;
|
|
1089
1863
|
PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId || "openviking");
|
|
1090
1864
|
info(tr(`Using local plugin from repo: ${pluginPath}`, `使用仓库内插件: ${pluginPath}`));
|
|
1091
|
-
await
|
|
1865
|
+
await deployPluginFromLocal(pluginPath);
|
|
1092
1866
|
info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
|
|
1093
|
-
|
|
1094
|
-
? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
|
|
1095
|
-
: ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
|
|
1096
|
-
await run("npm", npmArgs, { cwd: PLUGIN_DEST, silent: false });
|
|
1097
|
-
pluginPath = PLUGIN_DEST;
|
|
1867
|
+
pluginPath = PLUGIN_DEST;
|
|
1098
1868
|
} else {
|
|
1099
|
-
await
|
|
1869
|
+
await deployPluginFromRemote();
|
|
1100
1870
|
pluginPath = PLUGIN_DEST;
|
|
1101
1871
|
}
|
|
1102
1872
|
|
|
1103
|
-
await configureOpenClawPlugin(
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1873
|
+
await configureOpenClawPlugin(
|
|
1874
|
+
upgradePluginOnly
|
|
1875
|
+
? {
|
|
1876
|
+
runtimeConfig: upgradeRuntimeConfig,
|
|
1877
|
+
skipGatewayMode: true,
|
|
1878
|
+
claimSlot: installedUpgradeState ? shouldClaimTargetSlot(installedUpgradeState) : true,
|
|
1879
|
+
}
|
|
1880
|
+
: { preserveExistingConfig: false },
|
|
1881
|
+
);
|
|
1882
|
+
await writeInstallStateFile({
|
|
1883
|
+
operation: upgradePluginOnly ? "upgrade" : "install",
|
|
1884
|
+
fromVersion: upgradeAudit?.fromVersion || "",
|
|
1885
|
+
configBackupPath: upgradeAudit?.configBackupPath || "",
|
|
1886
|
+
pluginBackups: upgradeAudit?.pluginBackups || [],
|
|
1887
|
+
});
|
|
1888
|
+
if (upgradeAudit) {
|
|
1889
|
+
upgradeAudit.completedAt = new Date().toISOString();
|
|
1890
|
+
await writeUpgradeAuditFile(upgradeAudit);
|
|
1891
|
+
}
|
|
1892
|
+
let envFiles = getExistingEnvFiles();
|
|
1893
|
+
if (!upgradePluginOnly) {
|
|
1894
|
+
envFiles = await writeOpenvikingEnv({
|
|
1895
|
+
includePython: selectedMode === "local",
|
|
1896
|
+
});
|
|
1897
|
+
} else if (!envFiles && OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
|
|
1898
|
+
envFiles = await writeOpenvikingEnv({ includePython: false });
|
|
1899
|
+
}
|
|
1107
1900
|
|
|
1108
1901
|
console.log("");
|
|
1109
1902
|
bold("═══════════════════════════════════════════════════════════");
|
|
@@ -1111,7 +1904,17 @@ async function main() {
|
|
|
1111
1904
|
bold("═══════════════════════════════════════════════════════════");
|
|
1112
1905
|
console.log("");
|
|
1113
1906
|
|
|
1114
|
-
if (
|
|
1907
|
+
if (upgradeAudit) {
|
|
1908
|
+
info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
|
|
1909
|
+
info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
|
|
1910
|
+
for (const pluginBackup of upgradeAudit.pluginBackups || []) {
|
|
1911
|
+
info(tr(`Rollback plugin backup: ${pluginBackup.backupDir}`, `回滚插件备份: ${pluginBackup.backupDir}`));
|
|
1912
|
+
}
|
|
1913
|
+
info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
|
|
1914
|
+
console.log("");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (selectedMode === "local") {
|
|
1115
1918
|
info(tr("Run these commands to start OpenClaw + OpenViking:", "请按以下命令启动 OpenClaw + OpenViking:"));
|
|
1116
1919
|
} else {
|
|
1117
1920
|
info(tr("Run these commands to start OpenClaw:", "请按以下命令启动 OpenClaw:"));
|