openclaw-openviking-setup-helper 0.2.9 → 0.2.11
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 +1508 -113
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* openclaw-openviking-install
|
|
11
11
|
*
|
|
12
12
|
* Direct run:
|
|
13
|
-
* node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ]
|
|
14
|
-
* [ --openviking-version=V ] [ --repo=PATH ]
|
|
13
|
+
* node install.js [ -y | --yes ] [ --zh ] [ --workdir PATH ] [ --upgrade-plugin ]
|
|
14
|
+
* [ --plugin-version=TAG ] [ --openviking-version=V ] [ --repo=PATH ]
|
|
15
15
|
*
|
|
16
16
|
* Environment variables:
|
|
17
|
-
* REPO, BRANCH, OPENVIKING_INSTALL_YES, SKIP_OPENCLAW, SKIP_OPENVIKING
|
|
17
|
+
* REPO, PLUGIN_VERSION (or BRANCH), OPENVIKING_INSTALL_YES, SKIP_OPENCLAW, SKIP_OPENVIKING
|
|
18
18
|
* OPENVIKING_VERSION Pip install openviking==VERSION (omit for latest)
|
|
19
19
|
* OPENVIKING_REPO Repo path: source install (pip -e) + local plugin (default: off)
|
|
20
20
|
* NPM_REGISTRY, PIP_INDEX_URL
|
|
@@ -23,26 +23,28 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { spawn } from "node:child_process";
|
|
26
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
26
|
+
import { cp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
27
27
|
import { existsSync, readdirSync } from "node:fs";
|
|
28
|
-
import { dirname, join } from "node:path";
|
|
28
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
29
29
|
import { createInterface } from "node:readline";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
|
|
32
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
34
|
+
let REPO = process.env.REPO || "volcengine/OpenViking";
|
|
35
|
+
// PLUGIN_VERSION takes precedence over BRANCH (legacy). If omitted, resolve the latest tag from GitHub.
|
|
36
|
+
const pluginVersionEnv = (process.env.PLUGIN_VERSION || process.env.BRANCH || "").trim();
|
|
37
|
+
let PLUGIN_VERSION = pluginVersionEnv;
|
|
38
|
+
let pluginVersionExplicit = Boolean(pluginVersionEnv);
|
|
37
39
|
const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmmirror.com";
|
|
38
|
-
const PIP_INDEX_URL = process.env.PIP_INDEX_URL || "https://
|
|
40
|
+
const PIP_INDEX_URL = process.env.PIP_INDEX_URL || "https://mirrors.volces.com/pypi/simple/";
|
|
39
41
|
|
|
40
42
|
const IS_WIN = process.platform === "win32";
|
|
41
43
|
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
42
44
|
|
|
43
45
|
const DEFAULT_OPENCLAW_DIR = join(HOME, ".openclaw");
|
|
44
46
|
let OPENCLAW_DIR = DEFAULT_OPENCLAW_DIR;
|
|
45
|
-
let PLUGIN_DEST =
|
|
47
|
+
let PLUGIN_DEST = ""; // Will be set after resolving plugin config
|
|
46
48
|
|
|
47
49
|
const OPENVIKING_DIR = join(HOME, ".openviking");
|
|
48
50
|
|
|
@@ -51,28 +53,62 @@ const DEFAULT_AGFS_PORT = 1833;
|
|
|
51
53
|
const DEFAULT_VLM_MODEL = "doubao-seed-2-0-pro-260215";
|
|
52
54
|
const DEFAULT_EMBED_MODEL = "doubao-embedding-vision-251215";
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
56
|
+
// Fallback configs for old versions without manifest
|
|
57
|
+
const FALLBACK_LEGACY = {
|
|
58
|
+
dir: "openclaw-memory-plugin",
|
|
59
|
+
id: "memory-openviking",
|
|
60
|
+
kind: "memory",
|
|
61
|
+
slot: "memory",
|
|
62
|
+
required: ["index.ts", "config.ts", "openclaw.plugin.json", "package.json"],
|
|
63
|
+
optional: ["package-lock.json", ".gitignore"],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Must match examples/openclaw-plugin/install-manifest.json (npm only installs package deps, not these .ts files).
|
|
67
|
+
const FALLBACK_CURRENT = {
|
|
68
|
+
dir: "openclaw-plugin",
|
|
69
|
+
id: "openviking",
|
|
70
|
+
kind: "context-engine",
|
|
71
|
+
slot: "contextEngine",
|
|
72
|
+
required: ["index.ts", "config.ts", "package.json"],
|
|
73
|
+
optional: [
|
|
74
|
+
"context-engine.ts",
|
|
75
|
+
"client.ts",
|
|
76
|
+
"process-manager.ts",
|
|
77
|
+
"memory-ranking.ts",
|
|
78
|
+
"text-utils.ts",
|
|
79
|
+
"tool-call-id.ts",
|
|
80
|
+
"session-transcript-repair.ts",
|
|
81
|
+
"openclaw.plugin.json",
|
|
82
|
+
"tsconfig.json",
|
|
83
|
+
"package-lock.json",
|
|
84
|
+
".gitignore",
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const PLUGIN_VARIANTS = [
|
|
89
|
+
{ ...FALLBACK_LEGACY, generation: "legacy", slotFallback: "none" },
|
|
90
|
+
{ ...FALLBACK_CURRENT, generation: "current", slotFallback: "legacy" },
|
|
62
91
|
];
|
|
63
92
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
];
|
|
93
|
+
// Resolved plugin config (set by resolvePluginConfig)
|
|
94
|
+
let resolvedPluginDir = "";
|
|
95
|
+
let resolvedPluginId = "";
|
|
96
|
+
let resolvedPluginKind = "";
|
|
97
|
+
let resolvedPluginSlot = "";
|
|
98
|
+
let resolvedFilesRequired = [];
|
|
99
|
+
let resolvedFilesOptional = [];
|
|
100
|
+
let resolvedNpmOmitDev = true;
|
|
101
|
+
let resolvedMinOpenclawVersion = "";
|
|
102
|
+
let resolvedMinOpenvikingVersion = "";
|
|
103
|
+
let resolvedPluginReleaseId = "";
|
|
70
104
|
|
|
71
105
|
let installYes = process.env.OPENVIKING_INSTALL_YES === "1";
|
|
72
106
|
let langZh = false;
|
|
73
107
|
let openvikingVersion = process.env.OPENVIKING_VERSION || "";
|
|
74
108
|
let openvikingRepo = process.env.OPENVIKING_REPO || "";
|
|
75
109
|
let workdirExplicit = false;
|
|
110
|
+
let upgradePluginOnly = false;
|
|
111
|
+
let rollbackLastUpgrade = false;
|
|
76
112
|
|
|
77
113
|
let selectedMode = "local";
|
|
78
114
|
let selectedServerPort = DEFAULT_SERVER_PORT;
|
|
@@ -80,6 +116,9 @@ let remoteBaseUrl = "http://127.0.0.1:1933";
|
|
|
80
116
|
let remoteApiKey = "";
|
|
81
117
|
let remoteAgentId = "";
|
|
82
118
|
let openvikingPythonPath = "";
|
|
119
|
+
let upgradeRuntimeConfig = null;
|
|
120
|
+
let installedUpgradeState = null;
|
|
121
|
+
let upgradeAudit = null;
|
|
83
122
|
|
|
84
123
|
const argv = process.argv.slice(2);
|
|
85
124
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -92,6 +131,14 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
92
131
|
langZh = true;
|
|
93
132
|
continue;
|
|
94
133
|
}
|
|
134
|
+
if (arg === "--upgrade-plugin" || arg === "--update" || arg === "--upgrade") {
|
|
135
|
+
upgradePluginOnly = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (arg === "--rollback" || arg === "--rollback-last-upgrade") {
|
|
139
|
+
rollbackLastUpgrade = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
95
142
|
if (arg === "--workdir") {
|
|
96
143
|
const workdir = argv[i + 1]?.trim();
|
|
97
144
|
if (!workdir) {
|
|
@@ -103,38 +150,135 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
103
150
|
i += 1;
|
|
104
151
|
continue;
|
|
105
152
|
}
|
|
153
|
+
if (arg.startsWith("--plugin-version=")) {
|
|
154
|
+
const version = arg.slice("--plugin-version=".length).trim();
|
|
155
|
+
if (!version) {
|
|
156
|
+
console.error("--plugin-version requires a value");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
PLUGIN_VERSION = version;
|
|
160
|
+
pluginVersionExplicit = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (arg === "--plugin-version") {
|
|
164
|
+
const version = argv[i + 1]?.trim();
|
|
165
|
+
if (!version) {
|
|
166
|
+
console.error("--plugin-version requires a value");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
PLUGIN_VERSION = version;
|
|
170
|
+
pluginVersionExplicit = true;
|
|
171
|
+
i += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
106
174
|
if (arg.startsWith("--openviking-version=")) {
|
|
107
175
|
openvikingVersion = arg.slice("--openviking-version=".length).trim();
|
|
108
176
|
continue;
|
|
109
177
|
}
|
|
178
|
+
if (arg === "--openviking-version") {
|
|
179
|
+
const version = argv[i + 1]?.trim();
|
|
180
|
+
if (!version) {
|
|
181
|
+
console.error("--openviking-version requires a value");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
openvikingVersion = version;
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
110
188
|
if (arg.startsWith("--repo=")) {
|
|
111
189
|
openvikingRepo = arg.slice("--repo=".length).trim();
|
|
112
190
|
continue;
|
|
113
191
|
}
|
|
192
|
+
if (arg.startsWith("--github-repo=")) {
|
|
193
|
+
REPO = arg.slice("--github-repo=".length).trim();
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (arg === "--github-repo") {
|
|
197
|
+
const repo = argv[i + 1]?.trim();
|
|
198
|
+
if (!repo) {
|
|
199
|
+
console.error("--github-repo requires a value (e.g. owner/repo)");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
REPO = repo;
|
|
203
|
+
i += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
114
206
|
if (arg === "-h" || arg === "--help") {
|
|
115
207
|
printHelp();
|
|
116
208
|
process.exit(0);
|
|
117
209
|
}
|
|
118
210
|
}
|
|
119
211
|
|
|
120
|
-
const OPENVIKING_PIP_SPEC = openvikingVersion ? `openviking==${openvikingVersion}` : "openviking";
|
|
121
|
-
|
|
122
212
|
function setOpenClawDir(dir) {
|
|
123
213
|
OPENCLAW_DIR = dir;
|
|
124
|
-
PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", "openviking");
|
|
125
214
|
}
|
|
126
215
|
|
|
127
216
|
function printHelp() {
|
|
128
|
-
console.log("Usage: node install.js [
|
|
217
|
+
console.log("Usage: node install.js [ OPTIONS ]");
|
|
218
|
+
console.log("");
|
|
219
|
+
console.log("Options:");
|
|
220
|
+
console.log(" --github-repo=OWNER/REPO GitHub repository (default: volcengine/OpenViking)");
|
|
221
|
+
console.log(" --plugin-version=TAG Plugin version (Git tag, e.g. v0.2.9, default: latest tag)");
|
|
222
|
+
console.log(" --openviking-version=V OpenViking PyPI version (e.g. 0.2.9, default: latest)");
|
|
223
|
+
console.log(" --workdir PATH OpenClaw config directory (default: ~/.openclaw)");
|
|
224
|
+
console.log(" --repo=PATH Use local OpenViking repo at PATH (pip -e + local plugin)");
|
|
225
|
+
console.log(" --update, --upgrade-plugin");
|
|
226
|
+
console.log(" Upgrade only the plugin to the requested --plugin-version; keep ov.conf and do not change the OpenViking service");
|
|
227
|
+
console.log(" --rollback, --rollback-last-upgrade");
|
|
228
|
+
console.log(" Roll back the last plugin upgrade using the saved audit/backup files");
|
|
229
|
+
console.log(" -y, --yes Non-interactive (use defaults)");
|
|
230
|
+
console.log(" --zh Chinese prompts");
|
|
231
|
+
console.log(" -h, --help This help");
|
|
129
232
|
console.log("");
|
|
130
|
-
console.log("
|
|
131
|
-
console.log("
|
|
132
|
-
console.log("
|
|
133
|
-
console.log(" --openviking-version=VERSION Pip install openviking==VERSION (default: latest)");
|
|
134
|
-
console.log(" --repo=PATH Use OpenViking repo at PATH: pip install -e PATH, plugin from repo (default: off)");
|
|
135
|
-
console.log(" -h, --help This help");
|
|
233
|
+
console.log("Examples:");
|
|
234
|
+
console.log(" # Install latest version");
|
|
235
|
+
console.log(" node install.js");
|
|
136
236
|
console.log("");
|
|
137
|
-
console.log("
|
|
237
|
+
console.log(" # Install from a fork repository");
|
|
238
|
+
console.log(" node install.js --github-repo=yourname/OpenViking --plugin-version=dev-branch");
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(" # Install specific plugin version");
|
|
241
|
+
console.log(" node install.js --plugin-version=v0.2.8");
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log(" # Upgrade only the plugin files from main branch");
|
|
244
|
+
console.log(" node install.js --update --plugin-version=main");
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log(" # Roll back the last plugin upgrade");
|
|
247
|
+
console.log(" node install.js --rollback");
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log("Env: REPO, PLUGIN_VERSION, OPENVIKING_VERSION, SKIP_OPENCLAW, SKIP_OPENVIKING, NPM_REGISTRY, PIP_INDEX_URL");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatCliArg(value) {
|
|
253
|
+
if (!value) {
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
return /[\s"]/u.test(value) ? JSON.stringify(value) : value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getLegacyInstallCommandHint() {
|
|
260
|
+
const override = process.env.OPENVIKING_INSTALL_LEGACY_HINT?.trim();
|
|
261
|
+
if (override) {
|
|
262
|
+
return override;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const invokedScript = process.argv[1] ? basename(process.argv[1]) : "";
|
|
266
|
+
const args = ["--plugin-version", "<legacy-version>"];
|
|
267
|
+
if (workdirExplicit || OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
|
|
268
|
+
args.push("--workdir", formatCliArg(OPENCLAW_DIR));
|
|
269
|
+
}
|
|
270
|
+
if (REPO !== "volcengine/OpenViking") {
|
|
271
|
+
args.push("--github-repo", formatCliArg(REPO));
|
|
272
|
+
}
|
|
273
|
+
if (langZh) {
|
|
274
|
+
args.push("--zh");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (invokedScript === "install.js") {
|
|
278
|
+
return `node install.js ${args.join(" ")}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return `ov-install ${args.join(" ")}`;
|
|
138
282
|
}
|
|
139
283
|
|
|
140
284
|
function tr(en, zh) {
|
|
@@ -235,8 +379,19 @@ function question(prompt, defaultValue = "") {
|
|
|
235
379
|
});
|
|
236
380
|
}
|
|
237
381
|
|
|
382
|
+
async function resolveAbsoluteCommand(cmd) {
|
|
383
|
+
if (cmd.startsWith("/") || (IS_WIN && /^[A-Za-z]:[/\\]/.test(cmd))) return cmd;
|
|
384
|
+
if (IS_WIN) {
|
|
385
|
+
const r = await runCapture("where", [cmd], { shell: true });
|
|
386
|
+
return r.out.split(/\r?\n/)[0]?.trim() || cmd;
|
|
387
|
+
}
|
|
388
|
+
const r = await runCapture("which", [cmd], { shell: false });
|
|
389
|
+
return r.out.trim() || cmd;
|
|
390
|
+
}
|
|
391
|
+
|
|
238
392
|
async function checkPython() {
|
|
239
|
-
const
|
|
393
|
+
const raw = process.env.OPENVIKING_PYTHON || (IS_WIN ? "python" : "python3");
|
|
394
|
+
const py = await resolveAbsoluteCommand(raw);
|
|
240
395
|
const result = await runCapture(py, ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"]);
|
|
241
396
|
if (result.code !== 0 || !result.out) {
|
|
242
397
|
return {
|
|
@@ -390,6 +545,364 @@ async function checkOpenClaw() {
|
|
|
390
545
|
process.exit(1);
|
|
391
546
|
}
|
|
392
547
|
|
|
548
|
+
// Compare versions: returns true if v1 >= v2
|
|
549
|
+
function versionGte(v1, v2) {
|
|
550
|
+
const parseVersion = (v) => {
|
|
551
|
+
const cleaned = v.replace(/^v/, "").replace(/-.*$/, "");
|
|
552
|
+
const parts = cleaned.split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
553
|
+
while (parts.length < 3) parts.push(0);
|
|
554
|
+
return parts;
|
|
555
|
+
};
|
|
556
|
+
const [a1, a2, a3] = parseVersion(v1);
|
|
557
|
+
const [b1, b2, b3] = parseVersion(v2);
|
|
558
|
+
if (a1 !== b1) return a1 > b1;
|
|
559
|
+
if (a2 !== b2) return a2 > b2;
|
|
560
|
+
return a3 >= b3;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function isSemverLike(value) {
|
|
564
|
+
return /^v?\d+(\.\d+){1,2}$/.test(value);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function validateRequestedPluginVersion() {
|
|
568
|
+
if (!isSemverLike(PLUGIN_VERSION)) return;
|
|
569
|
+
if (versionGte(PLUGIN_VERSION, "v0.2.7") && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
|
|
570
|
+
err(tr("Plugin version v0.2.7 does not exist.", "插件版本 v0.2.7 不存在。"));
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (upgradePluginOnly && rollbackLastUpgrade) {
|
|
576
|
+
console.error("--update/--upgrade-plugin and --rollback cannot be used together");
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function ensurePluginOnlyOperationArgs() {
|
|
581
|
+
if ((upgradePluginOnly || rollbackLastUpgrade) && openvikingVersion) {
|
|
582
|
+
err(
|
|
583
|
+
tr(
|
|
584
|
+
"Plugin-only upgrade/rollback does not support --openviking-version. Use --plugin-version to choose the plugin release, and run a full install if you need to change the OpenViking service version.",
|
|
585
|
+
"仅插件升级或回滚不支持 --openviking-version。请使用 --plugin-version 指定插件版本;如果需要调整 OpenViking 服务版本,请执行完整安装流程。",
|
|
586
|
+
),
|
|
587
|
+
);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Detect OpenClaw version
|
|
593
|
+
async function detectOpenClawVersion() {
|
|
594
|
+
try {
|
|
595
|
+
const result = await runCapture("openclaw", ["--version"], { shell: IS_WIN });
|
|
596
|
+
if (result.code === 0 && result.out) {
|
|
597
|
+
const match = result.out.match(/\d+\.\d+(\.\d+)?/);
|
|
598
|
+
if (match) return match[0];
|
|
599
|
+
}
|
|
600
|
+
} catch {}
|
|
601
|
+
return "0.0.0";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Try to fetch a URL, return response text or null
|
|
605
|
+
async function tryFetch(url, timeout = 15000) {
|
|
606
|
+
try {
|
|
607
|
+
const controller = new AbortController();
|
|
608
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
609
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
610
|
+
clearTimeout(timeoutId);
|
|
611
|
+
if (response.ok) {
|
|
612
|
+
return await response.text();
|
|
613
|
+
}
|
|
614
|
+
} catch {}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Check if a remote file exists
|
|
619
|
+
async function testRemoteFile(url) {
|
|
620
|
+
try {
|
|
621
|
+
const controller = new AbortController();
|
|
622
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
623
|
+
const response = await fetch(url, { method: "HEAD", signal: controller.signal });
|
|
624
|
+
clearTimeout(timeoutId);
|
|
625
|
+
return response.ok;
|
|
626
|
+
} catch {}
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function compareSemverDesc(a, b) {
|
|
631
|
+
if (versionGte(a, b) && versionGte(b, a)) {
|
|
632
|
+
return 0;
|
|
633
|
+
}
|
|
634
|
+
return versionGte(a, b) ? -1 : 1;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function pickLatestPluginTag(tagNames) {
|
|
638
|
+
const normalized = tagNames
|
|
639
|
+
.map((tag) => String(tag ?? "").trim())
|
|
640
|
+
.filter(Boolean);
|
|
641
|
+
|
|
642
|
+
const semverTags = normalized
|
|
643
|
+
.filter((tag) => isSemverLike(tag))
|
|
644
|
+
.sort(compareSemverDesc);
|
|
645
|
+
|
|
646
|
+
if (semverTags.length > 0) {
|
|
647
|
+
return semverTags[0];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return normalized[0] || "";
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function parseGitLsRemoteTags(output) {
|
|
654
|
+
return String(output ?? "")
|
|
655
|
+
.split(/\r?\n/)
|
|
656
|
+
.map((line) => {
|
|
657
|
+
const match = line.match(/refs\/tags\/(.+)$/);
|
|
658
|
+
return match?.[1]?.trim() || "";
|
|
659
|
+
})
|
|
660
|
+
.filter(Boolean);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function resolveDefaultPluginVersion() {
|
|
664
|
+
if (PLUGIN_VERSION) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
info(tr(
|
|
669
|
+
`No plugin version specified; resolving latest tag from ${REPO}...`,
|
|
670
|
+
`未指定插件版本,正在解析 ${REPO} 的最新 tag...`,
|
|
671
|
+
));
|
|
672
|
+
|
|
673
|
+
const failures = [];
|
|
674
|
+
const apiUrl = `https://api.github.com/repos/${REPO}/tags?per_page=100`;
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const controller = new AbortController();
|
|
678
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
679
|
+
const response = await fetch(apiUrl, {
|
|
680
|
+
headers: {
|
|
681
|
+
Accept: "application/vnd.github+json",
|
|
682
|
+
"User-Agent": "openviking-setup-helper",
|
|
683
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
684
|
+
},
|
|
685
|
+
signal: controller.signal,
|
|
686
|
+
});
|
|
687
|
+
clearTimeout(timeoutId);
|
|
688
|
+
|
|
689
|
+
if (response.ok) {
|
|
690
|
+
const payload = await response.json().catch(() => null);
|
|
691
|
+
if (Array.isArray(payload)) {
|
|
692
|
+
const latestTag = pickLatestPluginTag(payload.map((item) => item?.name || ""));
|
|
693
|
+
if (latestTag) {
|
|
694
|
+
PLUGIN_VERSION = latestTag;
|
|
695
|
+
info(tr(
|
|
696
|
+
`Resolved default plugin version to latest tag: ${PLUGIN_VERSION}`,
|
|
697
|
+
`已将默认插件版本解析为最新 tag: ${PLUGIN_VERSION}`,
|
|
698
|
+
));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
failures.push("GitHub tags API returned an unexpected payload");
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
failures.push(`GitHub tags API returned HTTP ${response.status}`);
|
|
706
|
+
}
|
|
707
|
+
} catch (error) {
|
|
708
|
+
failures.push(`GitHub tags API failed: ${String(error)}`);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const gitRef = `https://github.com/${REPO}.git`;
|
|
712
|
+
const gitResult = await runCapture("git", ["ls-remote", "--tags", "--refs", gitRef], {
|
|
713
|
+
shell: IS_WIN,
|
|
714
|
+
});
|
|
715
|
+
if (gitResult.code === 0 && gitResult.out) {
|
|
716
|
+
const latestTag = pickLatestPluginTag(parseGitLsRemoteTags(gitResult.out));
|
|
717
|
+
if (latestTag) {
|
|
718
|
+
PLUGIN_VERSION = latestTag;
|
|
719
|
+
info(tr(
|
|
720
|
+
`Resolved default plugin version via git tags: ${PLUGIN_VERSION}`,
|
|
721
|
+
`已通过 git tag 解析默认插件版本: ${PLUGIN_VERSION}`,
|
|
722
|
+
));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
failures.push("git ls-remote returned no usable tags");
|
|
726
|
+
} else {
|
|
727
|
+
failures.push(`git ls-remote failed${gitResult.err ? `: ${gitResult.err}` : ""}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
err(tr(
|
|
731
|
+
`Could not resolve the latest tag for ${REPO}.`,
|
|
732
|
+
`无法解析 ${REPO} 的最新 tag。`,
|
|
733
|
+
));
|
|
734
|
+
console.log(tr(
|
|
735
|
+
"Please rerun with --plugin-version <tag>, or use --plugin-version main to track the branch head explicitly.",
|
|
736
|
+
"请使用 --plugin-version <tag> 重新执行;如果需要显式跟踪分支头,请使用 --plugin-version main。",
|
|
737
|
+
));
|
|
738
|
+
if (failures.length > 0) {
|
|
739
|
+
warn(failures.join(" | "));
|
|
740
|
+
}
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Resolve plugin configuration from manifest or fallback
|
|
745
|
+
async function resolvePluginConfig() {
|
|
746
|
+
const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
|
|
747
|
+
|
|
748
|
+
info(tr(`Resolving plugin configuration for version: ${PLUGIN_VERSION}`, `正在解析插件配置,版本: ${PLUGIN_VERSION}`));
|
|
749
|
+
|
|
750
|
+
let pluginDir = "";
|
|
751
|
+
let manifestData = null;
|
|
752
|
+
|
|
753
|
+
// Try to detect plugin directory and download manifest
|
|
754
|
+
const manifestCurrent = await tryFetch(`${ghRaw}/examples/openclaw-plugin/install-manifest.json`);
|
|
755
|
+
if (manifestCurrent) {
|
|
756
|
+
pluginDir = "openclaw-plugin";
|
|
757
|
+
try {
|
|
758
|
+
manifestData = JSON.parse(manifestCurrent);
|
|
759
|
+
} catch {}
|
|
760
|
+
info(tr("Found manifest in openclaw-plugin", "在 openclaw-plugin 中找到 manifest"));
|
|
761
|
+
} else {
|
|
762
|
+
const manifestLegacy = await tryFetch(`${ghRaw}/examples/openclaw-memory-plugin/install-manifest.json`);
|
|
763
|
+
if (manifestLegacy) {
|
|
764
|
+
pluginDir = "openclaw-memory-plugin";
|
|
765
|
+
try {
|
|
766
|
+
manifestData = JSON.parse(manifestLegacy);
|
|
767
|
+
} catch {}
|
|
768
|
+
info(tr("Found manifest in openclaw-memory-plugin", "在 openclaw-memory-plugin 中找到 manifest"));
|
|
769
|
+
} else if (await testRemoteFile(`${ghRaw}/examples/openclaw-plugin/index.ts`)) {
|
|
770
|
+
pluginDir = "openclaw-plugin";
|
|
771
|
+
info(tr("No manifest found, using fallback for openclaw-plugin", "未找到 manifest,使用 openclaw-plugin 回退配置"));
|
|
772
|
+
} else if (await testRemoteFile(`${ghRaw}/examples/openclaw-memory-plugin/index.ts`)) {
|
|
773
|
+
pluginDir = "openclaw-memory-plugin";
|
|
774
|
+
info(tr("No manifest found, using fallback for openclaw-memory-plugin", "未找到 manifest,使用 openclaw-memory-plugin 回退配置"));
|
|
775
|
+
} else {
|
|
776
|
+
err(tr(`Cannot find plugin directory for version: ${PLUGIN_VERSION}`, `无法找到版本 ${PLUGIN_VERSION} 的插件目录`));
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
resolvedPluginDir = pluginDir;
|
|
782
|
+
resolvedPluginReleaseId = "";
|
|
783
|
+
|
|
784
|
+
if (manifestData) {
|
|
785
|
+
resolvedPluginId = manifestData.plugin?.id || "";
|
|
786
|
+
resolvedPluginKind = manifestData.plugin?.kind || "";
|
|
787
|
+
resolvedPluginSlot = manifestData.plugin?.slot || "";
|
|
788
|
+
resolvedMinOpenclawVersion = manifestData.compatibility?.minOpenclawVersion || "";
|
|
789
|
+
resolvedMinOpenvikingVersion = manifestData.compatibility?.minOpenvikingVersion || "";
|
|
790
|
+
resolvedPluginReleaseId = manifestData.pluginVersion || manifestData.release?.id || "";
|
|
791
|
+
resolvedNpmOmitDev = manifestData.npm?.omitDev !== false;
|
|
792
|
+
resolvedFilesRequired = manifestData.files?.required || [];
|
|
793
|
+
resolvedFilesOptional = manifestData.files?.optional || [];
|
|
794
|
+
} else {
|
|
795
|
+
// No manifest — determine plugin identity by package.json name
|
|
796
|
+
let fallbackKey = pluginDir === "openclaw-memory-plugin" ? "legacy" : "current";
|
|
797
|
+
let compatVer = "";
|
|
798
|
+
|
|
799
|
+
const pkgJson = await tryFetch(`${ghRaw}/examples/${pluginDir}/package.json`);
|
|
800
|
+
if (pkgJson) {
|
|
801
|
+
try {
|
|
802
|
+
const pkg = JSON.parse(pkgJson);
|
|
803
|
+
const pkgName = pkg.name || "";
|
|
804
|
+
resolvedPluginReleaseId = pkg.version || "";
|
|
805
|
+
if (pkgName && pkgName !== "@openclaw/openviking") {
|
|
806
|
+
fallbackKey = "legacy";
|
|
807
|
+
info(tr(`Detected legacy plugin by package name: ${pkgName}`, `通过 package.json 名称检测到旧版插件: ${pkgName}`));
|
|
808
|
+
} else if (pkgName) {
|
|
809
|
+
fallbackKey = "current";
|
|
810
|
+
}
|
|
811
|
+
compatVer = (pkg.engines?.openclaw || "").replace(/^>=?\s*/, "").trim();
|
|
812
|
+
if (compatVer) {
|
|
813
|
+
info(tr(`Read minOpenclawVersion from package.json engines.openclaw: >=${compatVer}`, `从 package.json engines.openclaw 读取到最低版本: >=${compatVer}`));
|
|
814
|
+
}
|
|
815
|
+
} catch {}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const fallback = fallbackKey === "legacy" ? FALLBACK_LEGACY : FALLBACK_CURRENT;
|
|
819
|
+
resolvedPluginDir = pluginDir;
|
|
820
|
+
resolvedPluginId = fallback.id;
|
|
821
|
+
resolvedPluginKind = fallback.kind;
|
|
822
|
+
resolvedPluginSlot = fallback.slot;
|
|
823
|
+
resolvedFilesRequired = fallback.required;
|
|
824
|
+
resolvedFilesOptional = fallback.optional;
|
|
825
|
+
resolvedNpmOmitDev = true;
|
|
826
|
+
|
|
827
|
+
// If no compatVer from package.json, try main branch manifest
|
|
828
|
+
if (!compatVer && PLUGIN_VERSION !== "main") {
|
|
829
|
+
const mainRaw = `https://raw.githubusercontent.com/${REPO}/main`;
|
|
830
|
+
const mainManifest = await tryFetch(`${mainRaw}/examples/openclaw-plugin/install-manifest.json`);
|
|
831
|
+
if (mainManifest) {
|
|
832
|
+
try {
|
|
833
|
+
const m = JSON.parse(mainManifest);
|
|
834
|
+
compatVer = m.compatibility?.minOpenclawVersion || "";
|
|
835
|
+
if (compatVer) {
|
|
836
|
+
info(tr(`Read minOpenclawVersion from main branch manifest: >=${compatVer}`, `从 main 分支 manifest 读取到最低版本: >=${compatVer}`));
|
|
837
|
+
}
|
|
838
|
+
} catch {}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
resolvedMinOpenclawVersion = compatVer || "2026.3.7";
|
|
843
|
+
resolvedMinOpenvikingVersion = "";
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Set plugin destination
|
|
847
|
+
PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId);
|
|
848
|
+
|
|
849
|
+
info(tr(`Plugin: ${resolvedPluginId} (${resolvedPluginKind})`, `插件: ${resolvedPluginId} (${resolvedPluginKind})`));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Check OpenClaw version compatibility
|
|
853
|
+
async function checkOpenClawCompatibility() {
|
|
854
|
+
if (process.env.SKIP_OPENCLAW === "1") {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const ocVersion = await detectOpenClawVersion();
|
|
859
|
+
info(tr(`Detected OpenClaw version: ${ocVersion}`, `检测到 OpenClaw 版本: ${ocVersion}`));
|
|
860
|
+
|
|
861
|
+
// If no minimum version required, pass
|
|
862
|
+
if (!resolvedMinOpenclawVersion) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// If user explicitly requested an old version, pass
|
|
867
|
+
if (isSemverLike(PLUGIN_VERSION) && !versionGte(PLUGIN_VERSION, "v0.2.8")) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Check compatibility
|
|
872
|
+
if (!versionGte(ocVersion, resolvedMinOpenclawVersion)) {
|
|
873
|
+
err(tr(
|
|
874
|
+
`OpenClaw ${ocVersion} does not support this plugin (requires >= ${resolvedMinOpenclawVersion})`,
|
|
875
|
+
`OpenClaw ${ocVersion} 不支持此插件(需要 >= ${resolvedMinOpenclawVersion})`
|
|
876
|
+
));
|
|
877
|
+
console.log("");
|
|
878
|
+
bold(tr("Please choose one of the following options:", "请选择以下方案之一:"));
|
|
879
|
+
console.log("");
|
|
880
|
+
console.log(` ${tr("Option 1: Upgrade OpenClaw", "方案 1:升级 OpenClaw")}`);
|
|
881
|
+
console.log(` npm update -g openclaw --registry ${NPM_REGISTRY}`);
|
|
882
|
+
console.log("");
|
|
883
|
+
console.log(` ${tr("Option 2: Install a legacy plugin release compatible with your current OpenClaw version", "方案 2:安装与当前 OpenClaw 版本兼容的旧版插件")}`);
|
|
884
|
+
console.log(` ${getLegacyInstallCommandHint()}`);
|
|
885
|
+
console.log("");
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function checkRequestedOpenVikingCompatibility() {
|
|
891
|
+
if (!resolvedMinOpenvikingVersion || !openvikingVersion) return;
|
|
892
|
+
if (versionGte(openvikingVersion, resolvedMinOpenvikingVersion)) return;
|
|
893
|
+
|
|
894
|
+
err(tr(
|
|
895
|
+
`OpenViking ${openvikingVersion} does not support this plugin (requires >= ${resolvedMinOpenvikingVersion})`,
|
|
896
|
+
`OpenViking ${openvikingVersion} 不支持此插件(需要 >= ${resolvedMinOpenvikingVersion})`,
|
|
897
|
+
));
|
|
898
|
+
console.log("");
|
|
899
|
+
console.log(tr(
|
|
900
|
+
"Use a newer OpenViking version, or omit --openviking-version to install the latest release.",
|
|
901
|
+
"请使用更新版本的 OpenViking,或省略 --openviking-version 以安装最新版本。",
|
|
902
|
+
));
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
|
|
393
906
|
async function installOpenViking() {
|
|
394
907
|
if (process.env.SKIP_OPENVIKING === "1") {
|
|
395
908
|
info(tr("Skipping OpenViking install (SKIP_OPENVIKING=1)", "跳过 OpenViking 安装 (SKIP_OPENVIKING=1)"));
|
|
@@ -401,6 +914,12 @@ async function installOpenViking() {
|
|
|
401
914
|
err(tr("Python check failed.", "Python 校验失败"));
|
|
402
915
|
process.exit(1);
|
|
403
916
|
}
|
|
917
|
+
if (!python.ok) {
|
|
918
|
+
warn(tr(
|
|
919
|
+
`${python.detail}. Will attempt to find a suitable Python for pip install.`,
|
|
920
|
+
`${python.detail}。将尝试查找合适的 Python 进行 pip 安装。`,
|
|
921
|
+
));
|
|
922
|
+
}
|
|
404
923
|
|
|
405
924
|
const py = python.cmd;
|
|
406
925
|
|
|
@@ -413,14 +932,20 @@ async function installOpenViking() {
|
|
|
413
932
|
return;
|
|
414
933
|
}
|
|
415
934
|
|
|
416
|
-
|
|
935
|
+
// Determine package spec
|
|
936
|
+
const pkgSpec = openvikingVersion ? `openviking==${openvikingVersion}` : "openviking";
|
|
937
|
+
if (openvikingVersion) {
|
|
938
|
+
info(tr(`Installing OpenViking ${openvikingVersion} from PyPI...`, `正在安装 OpenViking ${openvikingVersion} (PyPI)...`));
|
|
939
|
+
} else {
|
|
940
|
+
info(tr("Installing OpenViking (latest) from PyPI...", "正在安装 OpenViking (最新版) (PyPI)..."));
|
|
941
|
+
}
|
|
417
942
|
info(tr(`Using pip index: ${PIP_INDEX_URL}`, `使用 pip 镜像源: ${PIP_INDEX_URL}`));
|
|
418
943
|
|
|
419
|
-
info(`Package: ${
|
|
944
|
+
info(`Package: ${pkgSpec}`);
|
|
420
945
|
await runCapture(py, ["-m", "pip", "install", "--upgrade", "pip", "-q", "-i", PIP_INDEX_URL], { shell: false });
|
|
421
946
|
const installResult = await runLiveCapture(
|
|
422
947
|
py,
|
|
423
|
-
["-m", "pip", "install", "--progress-bar", "on",
|
|
948
|
+
["-m", "pip", "install", "--progress-bar", "on", pkgSpec, "-i", PIP_INDEX_URL],
|
|
424
949
|
{ shell: false },
|
|
425
950
|
);
|
|
426
951
|
if (installResult.code === 0) {
|
|
@@ -440,7 +965,7 @@ async function installOpenViking() {
|
|
|
440
965
|
if (reuseCheck.code === 0) {
|
|
441
966
|
await runLiveCapture(
|
|
442
967
|
venvPy,
|
|
443
|
-
["-m", "pip", "install", "--progress-bar", "on", "-U",
|
|
968
|
+
["-m", "pip", "install", "--progress-bar", "on", "-U", pkgSpec, "-i", PIP_INDEX_URL],
|
|
444
969
|
{ shell: false },
|
|
445
970
|
);
|
|
446
971
|
openvikingPythonPath = venvPy;
|
|
@@ -476,7 +1001,7 @@ async function installOpenViking() {
|
|
|
476
1001
|
await runCapture(venvPy, ["-m", "pip", "install", "--upgrade", "pip", "-q", "-i", PIP_INDEX_URL], { shell: false });
|
|
477
1002
|
const venvInstall = await runLiveCapture(
|
|
478
1003
|
venvPy,
|
|
479
|
-
["-m", "pip", "install", "--progress-bar", "on",
|
|
1004
|
+
["-m", "pip", "install", "--progress-bar", "on", pkgSpec, "-i", PIP_INDEX_URL],
|
|
480
1005
|
{ shell: false },
|
|
481
1006
|
);
|
|
482
1007
|
if (venvInstall.code === 0) {
|
|
@@ -493,7 +1018,7 @@ async function installOpenViking() {
|
|
|
493
1018
|
if (process.env.OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES === "1") {
|
|
494
1019
|
const systemInstall = await runLiveCapture(
|
|
495
1020
|
py,
|
|
496
|
-
["-m", "pip", "install", "--progress-bar", "on", "--break-system-packages",
|
|
1021
|
+
["-m", "pip", "install", "--progress-bar", "on", "--break-system-packages", pkgSpec, "-i", PIP_INDEX_URL],
|
|
497
1022
|
{ shell: false },
|
|
498
1023
|
);
|
|
499
1024
|
if (systemInstall.code === 0) {
|
|
@@ -560,7 +1085,7 @@ async function configureOvConf() {
|
|
|
560
1085
|
},
|
|
561
1086
|
embedding: {
|
|
562
1087
|
dense: {
|
|
563
|
-
|
|
1088
|
+
provider: "volcengine",
|
|
564
1089
|
api_key: embeddingApiKey || null,
|
|
565
1090
|
model: embeddingModel,
|
|
566
1091
|
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
@@ -569,7 +1094,7 @@ async function configureOvConf() {
|
|
|
569
1094
|
},
|
|
570
1095
|
},
|
|
571
1096
|
vlm: {
|
|
572
|
-
|
|
1097
|
+
provider: "volcengine",
|
|
573
1098
|
api_key: vlmApiKey || null,
|
|
574
1099
|
model: vlmModel,
|
|
575
1100
|
api_base: "https://ark.cn-beijing.volces.com/api/v3",
|
|
@@ -583,118 +1108,886 @@ async function configureOvConf() {
|
|
|
583
1108
|
info(tr(`Config generated: ${configPath}`, `已生成配置: ${configPath}`));
|
|
584
1109
|
}
|
|
585
1110
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1111
|
+
function getOpenClawConfigPath() {
|
|
1112
|
+
return join(OPENCLAW_DIR, "openclaw.json");
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function getOpenClawEnv() {
|
|
1116
|
+
if (OPENCLAW_DIR === DEFAULT_OPENCLAW_DIR) {
|
|
1117
|
+
return { ...process.env };
|
|
1118
|
+
}
|
|
1119
|
+
return { ...process.env, OPENCLAW_STATE_DIR: OPENCLAW_DIR };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function readJsonFileIfExists(filePath) {
|
|
1123
|
+
if (!existsSync(filePath)) return null;
|
|
1124
|
+
const raw = await readFile(filePath, "utf8");
|
|
1125
|
+
return JSON.parse(raw);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function getInstallStatePathForPlugin(pluginId) {
|
|
1129
|
+
return join(OPENCLAW_DIR, "extensions", pluginId, ".ov-install-state.json");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function getUpgradeAuditDir() {
|
|
1133
|
+
return join(OPENCLAW_DIR, ".openviking-upgrade-backup");
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function getUpgradeAuditPath() {
|
|
1137
|
+
return join(getUpgradeAuditDir(), "last-upgrade.json");
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function getOpenClawConfigBackupPath() {
|
|
1141
|
+
return join(getUpgradeAuditDir(), "openclaw.json.bak");
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function normalizePluginMode(value) {
|
|
1145
|
+
return value === "remote" ? "remote" : "local";
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function getPluginVariantById(pluginId) {
|
|
1149
|
+
return PLUGIN_VARIANTS.find((variant) => variant.id === pluginId) || null;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function detectPluginPresence(config, variant) {
|
|
1153
|
+
const plugins = config?.plugins;
|
|
1154
|
+
const reasons = [];
|
|
1155
|
+
if (!plugins) {
|
|
1156
|
+
return { variant, present: false, reasons };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
|
|
1160
|
+
reasons.push("entry");
|
|
1161
|
+
}
|
|
1162
|
+
if (plugins.slots?.[variant.slot] === variant.id) {
|
|
1163
|
+
reasons.push("slot");
|
|
1164
|
+
}
|
|
1165
|
+
if (Array.isArray(plugins.allow) && plugins.allow.includes(variant.id)) {
|
|
1166
|
+
reasons.push("allow");
|
|
1167
|
+
}
|
|
1168
|
+
if (
|
|
1169
|
+
Array.isArray(plugins.load?.paths)
|
|
1170
|
+
&& plugins.load.paths.some((item) => typeof item === "string" && (item.includes(variant.id) || item.includes(variant.dir)))
|
|
1171
|
+
) {
|
|
1172
|
+
reasons.push("loadPath");
|
|
1173
|
+
}
|
|
1174
|
+
if (existsSync(join(OPENCLAW_DIR, "extensions", variant.id))) {
|
|
1175
|
+
reasons.push("dir");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return { variant, present: reasons.length > 0, reasons };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
async function detectInstalledPluginState() {
|
|
1182
|
+
const configPath = getOpenClawConfigPath();
|
|
1183
|
+
const config = await readJsonFileIfExists(configPath);
|
|
1184
|
+
const detections = [];
|
|
1185
|
+
for (const variant of PLUGIN_VARIANTS) {
|
|
1186
|
+
const detection = detectPluginPresence(config, variant);
|
|
1187
|
+
if (!detection.present) continue;
|
|
1188
|
+
detection.installState = await readJsonFileIfExists(getInstallStatePathForPlugin(variant.id));
|
|
1189
|
+
detections.push(detection);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
let generation = "none";
|
|
1193
|
+
if (detections.length === 1) {
|
|
1194
|
+
generation = detections[0].variant.generation;
|
|
1195
|
+
} else if (detections.length > 1) {
|
|
1196
|
+
generation = "mixed";
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return {
|
|
1200
|
+
config,
|
|
1201
|
+
configPath,
|
|
1202
|
+
detections,
|
|
1203
|
+
generation,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function formatInstalledDetectionLabel(detection) {
|
|
1208
|
+
const requestedRef = detection.installState?.requestedRef;
|
|
1209
|
+
const releaseId = detection.installState?.releaseId;
|
|
1210
|
+
if (requestedRef) return `${detection.variant.id}@${requestedRef}`;
|
|
1211
|
+
if (releaseId) return `${detection.variant.id}#${releaseId}`;
|
|
1212
|
+
return `${detection.variant.id} (${detection.variant.generation}, exact version unknown)`;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function formatInstalledStateLabel(installedState) {
|
|
1216
|
+
if (!installedState?.detections?.length) {
|
|
1217
|
+
return "not-installed";
|
|
1218
|
+
}
|
|
1219
|
+
return installedState.detections.map(formatInstalledDetectionLabel).join(" + ");
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function formatTargetVersionLabel() {
|
|
1223
|
+
const base = `${resolvedPluginId || "openviking"}@${PLUGIN_VERSION}`;
|
|
1224
|
+
if (resolvedPluginReleaseId && resolvedPluginReleaseId !== PLUGIN_VERSION) {
|
|
1225
|
+
return `${base} (${resolvedPluginReleaseId})`;
|
|
1226
|
+
}
|
|
1227
|
+
return base;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function extractRuntimeConfigFromPluginEntry(entryConfig) {
|
|
1231
|
+
if (!entryConfig || typeof entryConfig !== "object") return null;
|
|
1232
|
+
|
|
1233
|
+
const mode = normalizePluginMode(entryConfig.mode);
|
|
1234
|
+
const runtime = { mode };
|
|
1235
|
+
|
|
1236
|
+
if (mode === "remote") {
|
|
1237
|
+
if (typeof entryConfig.baseUrl === "string" && entryConfig.baseUrl.trim()) {
|
|
1238
|
+
runtime.baseUrl = entryConfig.baseUrl.trim();
|
|
1239
|
+
}
|
|
1240
|
+
if (typeof entryConfig.apiKey === "string" && entryConfig.apiKey.trim()) {
|
|
1241
|
+
runtime.apiKey = entryConfig.apiKey;
|
|
1242
|
+
}
|
|
1243
|
+
if (typeof entryConfig.agentId === "string" && entryConfig.agentId.trim()) {
|
|
1244
|
+
runtime.agentId = entryConfig.agentId.trim();
|
|
1245
|
+
}
|
|
1246
|
+
return runtime;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (typeof entryConfig.configPath === "string" && entryConfig.configPath.trim()) {
|
|
1250
|
+
runtime.configPath = entryConfig.configPath.trim();
|
|
1251
|
+
}
|
|
1252
|
+
if (entryConfig.port !== undefined && entryConfig.port !== null && `${entryConfig.port}`.trim()) {
|
|
1253
|
+
const parsedPort = Number.parseInt(String(entryConfig.port), 10);
|
|
1254
|
+
if (Number.isFinite(parsedPort) && parsedPort > 0) {
|
|
1255
|
+
runtime.port = parsedPort;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return runtime;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
async function readPortFromOvConf(configPath) {
|
|
1262
|
+
const filePath = configPath || join(OPENVIKING_DIR, "ov.conf");
|
|
1263
|
+
if (!existsSync(filePath)) return null;
|
|
1264
|
+
try {
|
|
1265
|
+
const ovConf = await readJsonFileIfExists(filePath);
|
|
1266
|
+
const parsedPort = Number.parseInt(String(ovConf?.server?.port ?? ""), 10);
|
|
1267
|
+
return Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : null;
|
|
1268
|
+
} catch {
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
async function backupOpenClawConfig(configPath) {
|
|
1274
|
+
await mkdir(getUpgradeAuditDir(), { recursive: true });
|
|
1275
|
+
const backupPath = getOpenClawConfigBackupPath();
|
|
1276
|
+
const configText = await readFile(configPath, "utf8");
|
|
1277
|
+
await writeFile(backupPath, configText, "utf8");
|
|
1278
|
+
return backupPath;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
async function writeUpgradeAuditFile(data) {
|
|
1282
|
+
await mkdir(getUpgradeAuditDir(), { recursive: true });
|
|
1283
|
+
await writeFile(getUpgradeAuditPath(), `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function writeInstallStateFile({ operation, fromVersion, configBackupPath, pluginBackups }) {
|
|
1287
|
+
const installStatePath = getInstallStatePathForPlugin(resolvedPluginId || "openviking");
|
|
1288
|
+
const state = {
|
|
1289
|
+
pluginId: resolvedPluginId || "openviking",
|
|
1290
|
+
generation: getPluginVariantById(resolvedPluginId || "openviking")?.generation || "unknown",
|
|
1291
|
+
requestedRef: PLUGIN_VERSION,
|
|
1292
|
+
releaseId: resolvedPluginReleaseId || "",
|
|
1293
|
+
operation,
|
|
1294
|
+
fromVersion: fromVersion || "",
|
|
1295
|
+
configBackupPath: configBackupPath || "",
|
|
1296
|
+
pluginBackups: pluginBackups || [],
|
|
1297
|
+
installedAt: new Date().toISOString(),
|
|
1298
|
+
repo: REPO,
|
|
1299
|
+
};
|
|
1300
|
+
await writeFile(installStatePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
async function moveDirWithFallback(sourceDir, destDir) {
|
|
1304
|
+
try {
|
|
1305
|
+
await rename(sourceDir, destDir);
|
|
1306
|
+
} catch {
|
|
1307
|
+
await cp(sourceDir, destDir, { recursive: true, force: true });
|
|
1308
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
async function rollbackLastUpgradeOperation() {
|
|
1313
|
+
const auditPath = getUpgradeAuditPath();
|
|
1314
|
+
const audit = await readJsonFileIfExists(auditPath);
|
|
1315
|
+
if (!audit) {
|
|
1316
|
+
err(
|
|
1317
|
+
tr(
|
|
1318
|
+
`No rollback audit file found at ${auditPath}.`,
|
|
1319
|
+
`未找到回滚审计文件: ${auditPath}`,
|
|
1320
|
+
),
|
|
1321
|
+
);
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (audit.rolledBackAt) {
|
|
1326
|
+
warn(
|
|
1327
|
+
tr(
|
|
1328
|
+
`The last recorded upgrade was already rolled back at ${audit.rolledBackAt}.`,
|
|
1329
|
+
`最近一次升级已在 ${audit.rolledBackAt} 回滚。`,
|
|
1330
|
+
),
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const configBackupPath = audit.configBackupPath || getOpenClawConfigBackupPath();
|
|
1335
|
+
if (!existsSync(configBackupPath)) {
|
|
1336
|
+
err(
|
|
1337
|
+
tr(
|
|
1338
|
+
`Rollback config backup is missing: ${configBackupPath}`,
|
|
1339
|
+
`回滚配置备份缺失: ${configBackupPath}`,
|
|
1340
|
+
),
|
|
1341
|
+
);
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const pluginBackups = Array.isArray(audit.pluginBackups) ? audit.pluginBackups : [];
|
|
1346
|
+
if (pluginBackups.length === 0) {
|
|
1347
|
+
err(tr("Rollback audit file contains no plugin backups.", "回滚审计文件中没有插件备份信息。"));
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
for (const pluginBackup of pluginBackups) {
|
|
1351
|
+
if (!pluginBackup?.pluginId || !pluginBackup?.backupDir || !existsSync(pluginBackup.backupDir)) {
|
|
1352
|
+
err(
|
|
1353
|
+
tr(
|
|
1354
|
+
`Rollback plugin backup is missing: ${pluginBackup?.backupDir || "<unknown>"}`,
|
|
1355
|
+
`回滚插件备份缺失: ${pluginBackup?.backupDir || "<unknown>"}`,
|
|
1356
|
+
),
|
|
1357
|
+
);
|
|
1358
|
+
process.exit(1);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
info(tr(`Rolling back last upgrade: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`, `开始回滚最近一次升级: ${audit.fromVersion || "unknown"} <- ${audit.toVersion || "unknown"}`));
|
|
1363
|
+
await stopOpenClawGatewayForUpgrade();
|
|
1364
|
+
|
|
1365
|
+
const configText = await readFile(configBackupPath, "utf8");
|
|
1366
|
+
await writeFile(getOpenClawConfigPath(), configText, "utf8");
|
|
1367
|
+
info(tr(`Restored openclaw.json from backup: ${configBackupPath}`, `已从备份恢复 openclaw.json: ${configBackupPath}`));
|
|
1368
|
+
|
|
1369
|
+
const extensionsDir = join(OPENCLAW_DIR, "extensions");
|
|
1370
|
+
await mkdir(extensionsDir, { recursive: true });
|
|
1371
|
+
for (const variant of PLUGIN_VARIANTS) {
|
|
1372
|
+
const liveDir = join(extensionsDir, variant.id);
|
|
1373
|
+
if (existsSync(liveDir)) {
|
|
1374
|
+
await rm(liveDir, { recursive: true, force: true });
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
for (const pluginBackup of pluginBackups) {
|
|
1379
|
+
if (!pluginBackup?.pluginId || !pluginBackup?.backupDir) continue;
|
|
1380
|
+
if (!existsSync(pluginBackup.backupDir)) {
|
|
1381
|
+
err(
|
|
1382
|
+
tr(
|
|
1383
|
+
`Rollback plugin backup is missing: ${pluginBackup.backupDir}`,
|
|
1384
|
+
`回滚插件备份缺失: ${pluginBackup.backupDir}`,
|
|
1385
|
+
),
|
|
1386
|
+
);
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
const destDir = join(extensionsDir, pluginBackup.pluginId);
|
|
1390
|
+
await moveDirWithFallback(pluginBackup.backupDir, destDir);
|
|
1391
|
+
info(tr(`Restored plugin directory: ${destDir}`, `已恢复插件目录: ${destDir}`));
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
audit.rolledBackAt = new Date().toISOString();
|
|
1395
|
+
audit.rollbackConfigPath = configBackupPath;
|
|
1396
|
+
await writeUpgradeAuditFile(audit);
|
|
1397
|
+
|
|
1398
|
+
console.log("");
|
|
1399
|
+
bold(tr("Rollback complete!", "回滚完成!"));
|
|
1400
|
+
console.log("");
|
|
1401
|
+
info(tr(`Rollback audit file: ${auditPath}`, `回滚审计文件: ${auditPath}`));
|
|
1402
|
+
info(tr("Run `openclaw gateway` and `openclaw status` to verify the restored plugin state.", "请运行 `openclaw gateway` 和 `openclaw status` 验证恢复后的插件状态。"));
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
async function prepareUpgradeRuntimeConfig(installedState) {
|
|
1406
|
+
const plugins = installedState.config?.plugins ?? {};
|
|
1407
|
+
const candidateOrder = installedState.detections
|
|
1408
|
+
.map((item) => item.variant)
|
|
1409
|
+
.sort((left, right) => (right.generation === "current" ? 1 : 0) - (left.generation === "current" ? 1 : 0));
|
|
1410
|
+
|
|
1411
|
+
let runtime = null;
|
|
1412
|
+
for (const variant of candidateOrder) {
|
|
1413
|
+
const entryConfig = extractRuntimeConfigFromPluginEntry(plugins.entries?.[variant.id]?.config);
|
|
1414
|
+
if (entryConfig) {
|
|
1415
|
+
runtime = entryConfig;
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (!runtime) {
|
|
1421
|
+
runtime = { mode: "local" };
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (runtime.mode === "remote") {
|
|
1425
|
+
runtime.baseUrl = runtime.baseUrl || remoteBaseUrl;
|
|
1426
|
+
return runtime;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
runtime.configPath = runtime.configPath || join(OPENVIKING_DIR, "ov.conf");
|
|
1430
|
+
runtime.port = runtime.port || await readPortFromOvConf(runtime.configPath) || DEFAULT_SERVER_PORT;
|
|
1431
|
+
return runtime;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function removePluginConfig(config, variant) {
|
|
1435
|
+
const plugins = config?.plugins;
|
|
1436
|
+
if (!plugins) return false;
|
|
1437
|
+
|
|
1438
|
+
let changed = false;
|
|
1439
|
+
|
|
1440
|
+
if (Array.isArray(plugins.allow)) {
|
|
1441
|
+
const nextAllow = plugins.allow.filter((item) => item !== variant.id);
|
|
1442
|
+
changed = changed || nextAllow.length !== plugins.allow.length;
|
|
1443
|
+
plugins.allow = nextAllow;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (Array.isArray(plugins.load?.paths)) {
|
|
1447
|
+
const nextPaths = plugins.load.paths.filter(
|
|
1448
|
+
(item) => typeof item !== "string" || (!item.includes(variant.id) && !item.includes(variant.dir)),
|
|
1449
|
+
);
|
|
1450
|
+
changed = changed || nextPaths.length !== plugins.load.paths.length;
|
|
1451
|
+
plugins.load.paths = nextPaths;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (plugins.entries && Object.prototype.hasOwnProperty.call(plugins.entries, variant.id)) {
|
|
1455
|
+
delete plugins.entries[variant.id];
|
|
1456
|
+
changed = true;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (plugins.slots?.[variant.slot] === variant.id) {
|
|
1460
|
+
plugins.slots[variant.slot] = variant.slotFallback;
|
|
1461
|
+
changed = true;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return changed;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async function prunePreviousUpgradeBackups(disabledDir, variant, keepDir) {
|
|
1468
|
+
if (!existsSync(disabledDir)) return;
|
|
1469
|
+
|
|
1470
|
+
const prefix = `${variant.id}-upgrade-backup-`;
|
|
1471
|
+
const keepName = keepDir ? keepDir.split(/[\\/]/).pop() : "";
|
|
1472
|
+
const entries = readdirSync(disabledDir, { withFileTypes: true });
|
|
1473
|
+
for (const entry of entries) {
|
|
1474
|
+
if (!entry.isDirectory()) continue;
|
|
1475
|
+
if (!entry.name.startsWith(prefix)) continue;
|
|
1476
|
+
if (keepName && entry.name === keepName) continue;
|
|
1477
|
+
await rm(join(disabledDir, entry.name), { recursive: true, force: true });
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async function backupPluginDirectory(variant) {
|
|
1482
|
+
const pluginDir = join(OPENCLAW_DIR, "extensions", variant.id);
|
|
1483
|
+
if (!existsSync(pluginDir)) return null;
|
|
1484
|
+
|
|
1485
|
+
const disabledDir = join(OPENCLAW_DIR, "disabled-extensions");
|
|
1486
|
+
const backupDir = join(disabledDir, `${variant.id}-upgrade-backup-${Date.now()}`);
|
|
1487
|
+
await mkdir(disabledDir, { recursive: true });
|
|
1488
|
+
try {
|
|
1489
|
+
await rename(pluginDir, backupDir);
|
|
1490
|
+
} catch {
|
|
1491
|
+
await cp(pluginDir, backupDir, { recursive: true, force: true });
|
|
1492
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
1493
|
+
}
|
|
1494
|
+
info(tr(`Backed up plugin directory: ${backupDir}`, `已备份插件目录: ${backupDir}`));
|
|
1495
|
+
await prunePreviousUpgradeBackups(disabledDir, variant, backupDir);
|
|
1496
|
+
return backupDir;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async function stopOpenClawGatewayForUpgrade() {
|
|
1500
|
+
const result = await runCapture("openclaw", ["gateway", "stop"], {
|
|
1501
|
+
env: getOpenClawEnv(),
|
|
1502
|
+
shell: IS_WIN,
|
|
1503
|
+
});
|
|
1504
|
+
if (result.code === 0) {
|
|
1505
|
+
info(tr("Stopped OpenClaw gateway before plugin upgrade", "升级插件前已停止 OpenClaw gateway"));
|
|
1506
|
+
} else {
|
|
1507
|
+
warn(tr("OpenClaw gateway may not be running; continuing", "OpenClaw gateway 可能未在运行,继续执行"));
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function shouldClaimTargetSlot(installedState) {
|
|
1512
|
+
const currentOwner = installedState.config?.plugins?.slots?.[resolvedPluginSlot];
|
|
1513
|
+
if (!currentOwner || currentOwner === "none" || currentOwner === "legacy" || currentOwner === resolvedPluginId) {
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
const currentOwnerVariant = getPluginVariantById(currentOwner);
|
|
1517
|
+
if (currentOwnerVariant && installedState.detections.some((item) => item.variant.id === currentOwnerVariant.id)) {
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
async function cleanupInstalledPluginConfig(installedState) {
|
|
1524
|
+
if (!installedState.config || !installedState.config.plugins) {
|
|
1525
|
+
warn(tr("openclaw.json has no plugins section; skipped targeted plugin cleanup", "openclaw.json 中没有 plugins 配置,已跳过定向插件清理"));
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const nextConfig = structuredClone(installedState.config);
|
|
1530
|
+
let changed = false;
|
|
1531
|
+
for (const detection of installedState.detections) {
|
|
1532
|
+
changed = removePluginConfig(nextConfig, detection.variant) || changed;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (!changed) {
|
|
1536
|
+
info(tr("No OpenViking plugin config changes were required", "无需修改 OpenViking 插件配置"));
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
await writeFile(installedState.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
1541
|
+
info(tr("Cleaned existing OpenViking plugin config only", "已仅清理 OpenViking 自身插件配置"));
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async function prepareStrongPluginUpgrade() {
|
|
1545
|
+
const installedState = await detectInstalledPluginState();
|
|
1546
|
+
if (installedState.generation === "none") {
|
|
1547
|
+
err(
|
|
1548
|
+
tr(
|
|
1549
|
+
"Plugin upgrade mode requires an existing OpenViking plugin entry in openclaw.json.",
|
|
1550
|
+
"插件升级模式要求 openclaw.json 中已经存在 OpenViking 插件记录。",
|
|
1551
|
+
),
|
|
1552
|
+
);
|
|
1553
|
+
process.exit(1);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
installedUpgradeState = installedState;
|
|
1557
|
+
upgradeRuntimeConfig = await prepareUpgradeRuntimeConfig(installedState);
|
|
1558
|
+
const fromVersion = formatInstalledStateLabel(installedState);
|
|
1559
|
+
const toVersion = formatTargetVersionLabel();
|
|
1560
|
+
selectedMode = upgradeRuntimeConfig.mode;
|
|
1561
|
+
info(
|
|
1562
|
+
tr(
|
|
1563
|
+
`Detected installed OpenViking plugin state: ${installedState.generation}`,
|
|
1564
|
+
`检测到已安装 OpenViking 插件状态: ${installedState.generation}`,
|
|
1565
|
+
),
|
|
1566
|
+
);
|
|
1567
|
+
if (upgradeRuntimeConfig.mode === "remote") {
|
|
1568
|
+
remoteBaseUrl = upgradeRuntimeConfig.baseUrl || remoteBaseUrl;
|
|
1569
|
+
remoteApiKey = upgradeRuntimeConfig.apiKey || "";
|
|
1570
|
+
remoteAgentId = upgradeRuntimeConfig.agentId || "";
|
|
1571
|
+
} else {
|
|
1572
|
+
selectedServerPort = upgradeRuntimeConfig.port || DEFAULT_SERVER_PORT;
|
|
1573
|
+
}
|
|
1574
|
+
info(tr(`Upgrade runtime mode: ${selectedMode}`, `升级运行模式: ${selectedMode}`));
|
|
1575
|
+
|
|
1576
|
+
info(tr(`Upgrade path: ${fromVersion} -> ${toVersion}`, `升级路径: ${fromVersion} -> ${toVersion}`));
|
|
1577
|
+
|
|
1578
|
+
await stopOpenClawGatewayForUpgrade();
|
|
1579
|
+
const configBackupPath = await backupOpenClawConfig(installedState.configPath);
|
|
1580
|
+
info(tr(`Backed up openclaw.json: ${configBackupPath}`, `已备份 openclaw.json: ${configBackupPath}`));
|
|
1581
|
+
const pluginBackups = [];
|
|
1582
|
+
for (const detection of installedState.detections) {
|
|
1583
|
+
const backupDir = await backupPluginDirectory(detection.variant);
|
|
1584
|
+
if (backupDir) {
|
|
1585
|
+
pluginBackups.push({ pluginId: detection.variant.id, backupDir });
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
upgradeAudit = {
|
|
1589
|
+
operation: "upgrade",
|
|
1590
|
+
createdAt: new Date().toISOString(),
|
|
1591
|
+
fromVersion,
|
|
1592
|
+
toVersion,
|
|
1593
|
+
configBackupPath,
|
|
1594
|
+
pluginBackups,
|
|
1595
|
+
runtimeMode: selectedMode,
|
|
1596
|
+
};
|
|
1597
|
+
await writeUpgradeAuditFile(upgradeAudit);
|
|
1598
|
+
await cleanupInstalledPluginConfig(installedState);
|
|
1599
|
+
|
|
1600
|
+
info(
|
|
1601
|
+
tr(
|
|
1602
|
+
"Upgrade will keep the existing OpenViking runtime file and re-apply only the minimum plugin runtime settings.",
|
|
1603
|
+
"升级将保留现有 OpenViking 运行时文件,并只回填最小插件运行配置。",
|
|
1604
|
+
),
|
|
1605
|
+
);
|
|
1606
|
+
info(tr(`Upgrade audit file: ${getUpgradeAuditPath()}`, `升级审计文件: ${getUpgradeAuditPath()}`));
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
async function downloadPluginFile(destDir, fileName, url, required, index, total) {
|
|
589
1610
|
const maxRetries = 3;
|
|
1611
|
+
const destPath = join(destDir, fileName);
|
|
590
1612
|
|
|
591
1613
|
process.stdout.write(` [${index}/${total}] ${fileName} `);
|
|
592
1614
|
|
|
1615
|
+
let lastStatus = 0;
|
|
1616
|
+
let saw404 = false;
|
|
1617
|
+
|
|
593
1618
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
594
1619
|
try {
|
|
595
1620
|
const response = await fetch(url);
|
|
1621
|
+
lastStatus = response.status;
|
|
596
1622
|
if (response.ok) {
|
|
597
1623
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1624
|
+
if (buffer.length === 0) {
|
|
1625
|
+
lastStatus = 0;
|
|
1626
|
+
} else {
|
|
1627
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
1628
|
+
await writeFile(destPath, buffer);
|
|
1629
|
+
console.log(" OK");
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
} else if (!required && response.status === 404) {
|
|
1633
|
+
saw404 = true;
|
|
1634
|
+
break;
|
|
601
1635
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
} catch {}
|
|
1636
|
+
} catch {
|
|
1637
|
+
lastStatus = 0;
|
|
1638
|
+
}
|
|
607
1639
|
|
|
608
1640
|
if (attempt < maxRetries) {
|
|
609
1641
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
610
1642
|
}
|
|
611
1643
|
}
|
|
612
1644
|
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
1645
|
+
if (saw404 || lastStatus === 404) {
|
|
1646
|
+
if (fileName === ".gitignore") {
|
|
1647
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
1648
|
+
await writeFile(destPath, "node_modules/\n", "utf8");
|
|
1649
|
+
console.log(" OK");
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
console.log(tr(" skip", " 跳过"));
|
|
616
1653
|
return;
|
|
617
1654
|
}
|
|
618
1655
|
|
|
1656
|
+
if (!required) {
|
|
1657
|
+
console.log("");
|
|
1658
|
+
err(
|
|
1659
|
+
tr(
|
|
1660
|
+
`Optional file failed after ${maxRetries} retries (HTTP ${lastStatus || "network"}): ${url}`,
|
|
1661
|
+
`可选文件已重试 ${maxRetries} 次仍失败(HTTP ${lastStatus || "网络错误"}): ${url}`,
|
|
1662
|
+
),
|
|
1663
|
+
);
|
|
1664
|
+
process.exit(1);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
619
1667
|
console.log("");
|
|
620
|
-
err(tr(`Download failed: ${url}`,
|
|
1668
|
+
err(tr(`Download failed after ${maxRetries} retries: ${url}`, `下载失败(已重试 ${maxRetries} 次): ${url}`));
|
|
621
1669
|
process.exit(1);
|
|
622
1670
|
}
|
|
623
1671
|
|
|
624
|
-
async function downloadPlugin() {
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1672
|
+
async function downloadPlugin(destDir) {
|
|
1673
|
+
const ghRaw = `https://raw.githubusercontent.com/${REPO}/${PLUGIN_VERSION}`;
|
|
1674
|
+
const pluginDir = resolvedPluginDir;
|
|
1675
|
+
const total = resolvedFilesRequired.length + resolvedFilesOptional.length;
|
|
1676
|
+
|
|
1677
|
+
await mkdir(destDir, { recursive: true });
|
|
1678
|
+
|
|
1679
|
+
info(tr(`Downloading plugin from ${REPO}@${PLUGIN_VERSION} (${total} files)...`, `正在从 ${REPO}@${PLUGIN_VERSION} 下载插件(共 ${total} 个文件)...`));
|
|
630
1680
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1681
|
+
let i = 0;
|
|
1682
|
+
// Download required files
|
|
1683
|
+
for (const name of resolvedFilesRequired) {
|
|
1684
|
+
if (!name) continue;
|
|
1685
|
+
i++;
|
|
1686
|
+
const url = `${ghRaw}/examples/${pluginDir}/${name}`;
|
|
1687
|
+
await downloadPluginFile(destDir, name, url, true, i, total);
|
|
635
1688
|
}
|
|
636
1689
|
|
|
1690
|
+
// Download optional files
|
|
1691
|
+
for (const name of resolvedFilesOptional) {
|
|
1692
|
+
if (!name) continue;
|
|
1693
|
+
i++;
|
|
1694
|
+
const url = `${ghRaw}/examples/${pluginDir}/${name}`;
|
|
1695
|
+
await downloadPluginFile(destDir, name, url, false, i, total);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// npm install
|
|
637
1699
|
info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
|
|
638
|
-
|
|
1700
|
+
const npmArgs = resolvedNpmOmitDev
|
|
1701
|
+
? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
|
|
1702
|
+
: ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
|
|
1703
|
+
await run("npm", npmArgs, { cwd: destDir, silent: false });
|
|
639
1704
|
info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
|
|
640
1705
|
}
|
|
641
1706
|
|
|
642
|
-
async function
|
|
643
|
-
|
|
1707
|
+
async function deployLocalPlugin(localPluginDir, destDir) {
|
|
1708
|
+
await rm(destDir, { recursive: true, force: true });
|
|
1709
|
+
await mkdir(destDir, { recursive: true });
|
|
1710
|
+
await cp(localPluginDir, destDir, {
|
|
1711
|
+
recursive: true,
|
|
1712
|
+
force: true,
|
|
1713
|
+
filter: (sourcePath) => {
|
|
1714
|
+
const rel = relative(localPluginDir, sourcePath);
|
|
1715
|
+
if (!rel) return true;
|
|
1716
|
+
const firstSegment = rel.split(/[\\/]/)[0];
|
|
1717
|
+
return firstSegment !== "node_modules" && firstSegment !== ".git";
|
|
1718
|
+
},
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
async function installPluginDependencies(destDir) {
|
|
1723
|
+
info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
|
|
1724
|
+
const npmArgs = resolvedNpmOmitDev
|
|
1725
|
+
? ["install", "--omit=dev", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY]
|
|
1726
|
+
: ["install", "--no-audit", "--no-fund", "--registry", NPM_REGISTRY];
|
|
1727
|
+
await run("npm", npmArgs, { cwd: destDir, silent: false });
|
|
1728
|
+
return info(tr(`Plugin prepared: ${destDir}`, `插件已准备: ${destDir}`));
|
|
1729
|
+
}
|
|
644
1730
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1731
|
+
async function createPluginStagingDir() {
|
|
1732
|
+
const pluginId = resolvedPluginId || "openviking";
|
|
1733
|
+
const extensionsDir = join(OPENCLAW_DIR, "extensions");
|
|
1734
|
+
const stagingDir = join(extensionsDir, `.${pluginId}.staging-${process.pid}-${Date.now()}`);
|
|
1735
|
+
await mkdir(extensionsDir, { recursive: true });
|
|
1736
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1737
|
+
await mkdir(stagingDir, { recursive: true });
|
|
1738
|
+
return stagingDir;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
async function finalizePluginDeployment(stagingDir) {
|
|
1742
|
+
await rm(PLUGIN_DEST, { recursive: true, force: true });
|
|
1743
|
+
try {
|
|
1744
|
+
await rename(stagingDir, PLUGIN_DEST);
|
|
1745
|
+
} catch {
|
|
1746
|
+
await cp(stagingDir, PLUGIN_DEST, { recursive: true, force: true });
|
|
1747
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1748
|
+
}
|
|
1749
|
+
return info(tr(`Plugin deployed: ${PLUGIN_DEST}`, `插件部署完成: ${PLUGIN_DEST}`));
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
async function deployPluginFromRemote() {
|
|
1753
|
+
const stagingDir = await createPluginStagingDir();
|
|
1754
|
+
try {
|
|
1755
|
+
await downloadPlugin(stagingDir);
|
|
1756
|
+
await finalizePluginDeployment(stagingDir);
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1759
|
+
throw error;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/** Same as INSTALL*.md manual cleanup: stale entries block `plugins.slots.*` validation after reinstall. */
|
|
1764
|
+
function resolvedPluginSlotFallback() {
|
|
1765
|
+
if (resolvedPluginId === "memory-openviking") return "none";
|
|
1766
|
+
if (resolvedPluginId === "openviking") return "legacy";
|
|
1767
|
+
return "none";
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
async function scrubStaleOpenClawPluginRegistration() {
|
|
1771
|
+
const configPath = getOpenClawConfigPath();
|
|
1772
|
+
if (!existsSync(configPath)) return;
|
|
1773
|
+
const pluginId = resolvedPluginId;
|
|
1774
|
+
const slot = resolvedPluginSlot;
|
|
1775
|
+
const slotFallback = resolvedPluginSlotFallback();
|
|
1776
|
+
let raw;
|
|
1777
|
+
try {
|
|
1778
|
+
raw = await readFile(configPath, "utf8");
|
|
1779
|
+
} catch {
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
let cfg;
|
|
1783
|
+
try {
|
|
1784
|
+
cfg = JSON.parse(raw);
|
|
1785
|
+
} catch {
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (!cfg.plugins) return;
|
|
1789
|
+
const p = cfg.plugins;
|
|
1790
|
+
let changed = false;
|
|
1791
|
+
if (p.entries && Object.prototype.hasOwnProperty.call(p.entries, pluginId)) {
|
|
1792
|
+
delete p.entries[pluginId];
|
|
1793
|
+
changed = true;
|
|
1794
|
+
}
|
|
1795
|
+
if (Array.isArray(p.allow)) {
|
|
1796
|
+
const next = p.allow.filter((id) => id !== pluginId);
|
|
1797
|
+
if (next.length !== p.allow.length) {
|
|
1798
|
+
p.allow = next;
|
|
1799
|
+
changed = true;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (p.load && Array.isArray(p.load.paths)) {
|
|
1803
|
+
const norm = (s) => String(s).replace(/\\/g, "/");
|
|
1804
|
+
const extNeedle = `/extensions/${pluginId}`;
|
|
1805
|
+
const next = p.load.paths.filter((path) => {
|
|
1806
|
+
if (typeof path !== "string") return true;
|
|
1807
|
+
return !norm(path).includes(extNeedle);
|
|
1808
|
+
});
|
|
1809
|
+
if (next.length !== p.load.paths.length) {
|
|
1810
|
+
p.load.paths = next;
|
|
1811
|
+
changed = true;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (p.slots && p.slots[slot] === pluginId) {
|
|
1815
|
+
p.slots[slot] = slotFallback;
|
|
1816
|
+
changed = true;
|
|
1817
|
+
}
|
|
1818
|
+
if (!changed) return;
|
|
1819
|
+
const out = JSON.stringify(cfg, null, 2) + "\n";
|
|
1820
|
+
const tmp = `${configPath}.ov-install-tmp.${process.pid}`;
|
|
1821
|
+
await writeFile(tmp, out, "utf8");
|
|
1822
|
+
await rename(tmp, configPath);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
async function deployPluginFromLocal(localPluginDir) {
|
|
1826
|
+
const stagingDir = await createPluginStagingDir();
|
|
1827
|
+
try {
|
|
1828
|
+
await deployLocalPlugin(localPluginDir, stagingDir);
|
|
1829
|
+
await installPluginDependencies(stagingDir);
|
|
1830
|
+
await finalizePluginDeployment(stagingDir);
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
1833
|
+
throw error;
|
|
648
1834
|
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async function configureOpenClawPlugin({
|
|
1838
|
+
preserveExistingConfig = false,
|
|
1839
|
+
runtimeConfig = null,
|
|
1840
|
+
skipGatewayMode = false,
|
|
1841
|
+
claimSlot = true,
|
|
1842
|
+
} = {}) {
|
|
1843
|
+
info(tr("Configuring OpenClaw plugin...", "正在配置 OpenClaw 插件..."));
|
|
1844
|
+
|
|
1845
|
+
const pluginId = resolvedPluginId;
|
|
1846
|
+
const pluginSlot = resolvedPluginSlot;
|
|
649
1847
|
|
|
650
|
-
const
|
|
1848
|
+
const ocEnv = getOpenClawEnv();
|
|
1849
|
+
|
|
1850
|
+
const oc = async (args) => {
|
|
1851
|
+
const result = await runCapture("openclaw", args, { env: ocEnv, shell: IS_WIN });
|
|
1852
|
+
if (result.code !== 0) {
|
|
1853
|
+
const detail = result.err || result.out;
|
|
1854
|
+
throw new Error(`openclaw ${args.join(" ")} failed (exit code ${result.code})${detail ? `: ${detail}` : ""}`);
|
|
1855
|
+
}
|
|
1856
|
+
return result;
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
if (!preserveExistingConfig) {
|
|
1860
|
+
await scrubStaleOpenClawPluginRegistration();
|
|
1861
|
+
}
|
|
651
1862
|
|
|
652
1863
|
// Enable plugin (files already deployed to extensions dir by deployPlugin)
|
|
653
|
-
|
|
654
|
-
if (
|
|
655
|
-
|
|
1864
|
+
await oc(["plugins", "enable", pluginId]);
|
|
1865
|
+
if (claimSlot) {
|
|
1866
|
+
await oc(["config", "set", `plugins.slots.${pluginSlot}`, pluginId]);
|
|
1867
|
+
} else {
|
|
1868
|
+
warn(
|
|
1869
|
+
tr(
|
|
1870
|
+
`Skipped claiming plugins.slots.${pluginSlot}; it is currently owned by another plugin.`,
|
|
1871
|
+
`已跳过设置 plugins.slots.${pluginSlot},当前该 slot 由其他插件占用。`,
|
|
1872
|
+
),
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
if (preserveExistingConfig) {
|
|
1877
|
+
info(
|
|
1878
|
+
tr(
|
|
1879
|
+
`Preserved existing plugin runtime config for ${pluginId}`,
|
|
1880
|
+
`已保留 ${pluginId} 的现有插件运行时配置`,
|
|
1881
|
+
),
|
|
1882
|
+
);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
656
1885
|
|
|
657
|
-
|
|
658
|
-
|
|
1886
|
+
const effectiveRuntimeConfig = runtimeConfig || (
|
|
1887
|
+
selectedMode === "remote"
|
|
1888
|
+
? { mode: "remote", baseUrl: remoteBaseUrl, apiKey: remoteApiKey, agentId: remoteAgentId }
|
|
1889
|
+
: { mode: "local", configPath: join(OPENVIKING_DIR, "ov.conf"), port: selectedServerPort }
|
|
1890
|
+
);
|
|
1891
|
+
|
|
1892
|
+
if (!skipGatewayMode) {
|
|
1893
|
+
await oc(["config", "set", "gateway.mode", effectiveRuntimeConfig.mode === "remote" ? "remote" : "local"]);
|
|
1894
|
+
}
|
|
659
1895
|
|
|
660
1896
|
// Set plugin config for the selected mode
|
|
661
|
-
if (
|
|
662
|
-
const ovConfPath = join(OPENVIKING_DIR, "ov.conf");
|
|
663
|
-
await oc(["config", "set",
|
|
664
|
-
await oc(["config", "set",
|
|
665
|
-
await oc(["config", "set",
|
|
1897
|
+
if (effectiveRuntimeConfig.mode === "local") {
|
|
1898
|
+
const ovConfPath = effectiveRuntimeConfig.configPath || join(OPENVIKING_DIR, "ov.conf");
|
|
1899
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "local"]);
|
|
1900
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.configPath`, ovConfPath]);
|
|
1901
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.port`, String(effectiveRuntimeConfig.port || DEFAULT_SERVER_PORT)]);
|
|
666
1902
|
} else {
|
|
667
|
-
await oc(["config", "set",
|
|
668
|
-
await oc(["config", "set",
|
|
669
|
-
if (
|
|
670
|
-
await oc(["config", "set",
|
|
1903
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.mode`, "remote"]);
|
|
1904
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.baseUrl`, effectiveRuntimeConfig.baseUrl || remoteBaseUrl]);
|
|
1905
|
+
if (effectiveRuntimeConfig.apiKey) {
|
|
1906
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.apiKey`, effectiveRuntimeConfig.apiKey]);
|
|
671
1907
|
}
|
|
672
|
-
if (
|
|
673
|
-
await oc(["config", "set",
|
|
1908
|
+
if (effectiveRuntimeConfig.agentId) {
|
|
1909
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.agentId`, effectiveRuntimeConfig.agentId]);
|
|
674
1910
|
}
|
|
675
1911
|
}
|
|
676
1912
|
|
|
1913
|
+
// Legacy (memory) plugins need explicit targetUri/autoRecall/autoCapture (new version has defaults in config.ts)
|
|
1914
|
+
if (resolvedPluginKind === "memory") {
|
|
1915
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.targetUri`, "viking://user/memories"]);
|
|
1916
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.autoRecall`, "true", "--json"]);
|
|
1917
|
+
await oc(["config", "set", `plugins.entries.${pluginId}.config.autoCapture`, "true", "--json"]);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
677
1920
|
info(tr("OpenClaw plugin configured", "OpenClaw 插件配置完成"));
|
|
678
1921
|
}
|
|
679
1922
|
|
|
1923
|
+
async function discoverOpenvikingPython(failedPy) {
|
|
1924
|
+
const candidates = IS_WIN
|
|
1925
|
+
? ["python3", "python", "py -3"]
|
|
1926
|
+
: ["python3.13", "python3.12", "python3.11", "python3.10", "python3", "python"];
|
|
1927
|
+
for (const candidate of candidates) {
|
|
1928
|
+
if (candidate === failedPy) continue;
|
|
1929
|
+
const resolved = await resolveAbsoluteCommand(candidate);
|
|
1930
|
+
if (!resolved || resolved === candidate || resolved === failedPy) continue;
|
|
1931
|
+
const check = await runCapture(resolved, ["-c", "import openviking"], { shell: false });
|
|
1932
|
+
if (check.code === 0) return resolved;
|
|
1933
|
+
}
|
|
1934
|
+
return "";
|
|
1935
|
+
}
|
|
1936
|
+
|
|
680
1937
|
async function resolvePythonPath() {
|
|
681
1938
|
if (openvikingPythonPath) return openvikingPythonPath;
|
|
682
1939
|
const python = await checkPython();
|
|
683
|
-
|
|
684
|
-
if (!py) return "";
|
|
685
|
-
|
|
686
|
-
if (IS_WIN) {
|
|
687
|
-
const result = await runCapture("where", [py], { shell: true });
|
|
688
|
-
return result.out.split(/\r?\n/)[0]?.trim() || py;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const result = await runCapture("which", [py], { shell: false });
|
|
692
|
-
return result.out.trim() || py;
|
|
1940
|
+
return python.cmd || "";
|
|
693
1941
|
}
|
|
694
1942
|
|
|
695
1943
|
async function writeOpenvikingEnv({ includePython }) {
|
|
696
1944
|
const needStateDir = OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR;
|
|
697
|
-
|
|
1945
|
+
let pythonPath = "";
|
|
1946
|
+
if (includePython) {
|
|
1947
|
+
pythonPath = await resolvePythonPath();
|
|
1948
|
+
if (!pythonPath) {
|
|
1949
|
+
pythonPath = (process.env.OPENVIKING_PYTHON || "").trim() || (IS_WIN ? "python" : "python3");
|
|
1950
|
+
warn(
|
|
1951
|
+
tr(
|
|
1952
|
+
"Could not resolve absolute Python path; wrote fallback OPENVIKING_PYTHON to openviking.env. Edit that file if OpenViking fails to start.",
|
|
1953
|
+
"未能解析 Python 绝对路径,已在 openviking.env 中写入后备值。若启动失败请手动修改为虚拟环境中的 python 可执行文件路径。",
|
|
1954
|
+
),
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// Verify the resolved Python can actually import openviking
|
|
1959
|
+
if (pythonPath) {
|
|
1960
|
+
const verify = await runCapture(pythonPath, ["-c", "import openviking"], { shell: false });
|
|
1961
|
+
if (verify.code !== 0) {
|
|
1962
|
+
warn(
|
|
1963
|
+
tr(
|
|
1964
|
+
`Resolved Python (${pythonPath}) cannot import openviking. The pip install target may differ from the runtime python3.`,
|
|
1965
|
+
`解析到的 Python(${pythonPath})无法 import openviking。pip 安装目标可能与运行时的 python3 不一致。`,
|
|
1966
|
+
),
|
|
1967
|
+
);
|
|
1968
|
+
// Try to discover the correct Python via pip show
|
|
1969
|
+
const corrected = await discoverOpenvikingPython(pythonPath);
|
|
1970
|
+
if (corrected) {
|
|
1971
|
+
info(
|
|
1972
|
+
tr(
|
|
1973
|
+
`Auto-corrected OPENVIKING_PYTHON to ${corrected}`,
|
|
1974
|
+
`已自动修正 OPENVIKING_PYTHON 为 ${corrected}`,
|
|
1975
|
+
),
|
|
1976
|
+
);
|
|
1977
|
+
pythonPath = corrected;
|
|
1978
|
+
} else {
|
|
1979
|
+
warn(
|
|
1980
|
+
tr(
|
|
1981
|
+
`Could not auto-detect the correct Python. Edit OPENVIKING_PYTHON in the env file manually.`,
|
|
1982
|
+
`无法自动检测正确的 Python。请手动修改 env 文件中的 OPENVIKING_PYTHON。`,
|
|
1983
|
+
),
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// Remote mode + default state dir + no python line → nothing to persist
|
|
698
1991
|
if (!needStateDir && !pythonPath) return null;
|
|
699
1992
|
|
|
700
1993
|
await mkdir(OPENCLAW_DIR, { recursive: true });
|
|
@@ -741,45 +2034,129 @@ function wrapCommand(command, envFiles) {
|
|
|
741
2034
|
return `source '${envFiles.shellPath.replace(/'/g, "'\"'\"'")}' && ${command}`;
|
|
742
2035
|
}
|
|
743
2036
|
|
|
2037
|
+
function getExistingEnvFiles() {
|
|
2038
|
+
if (IS_WIN) {
|
|
2039
|
+
const batPath = join(OPENCLAW_DIR, "openviking.env.bat");
|
|
2040
|
+
const ps1Path = join(OPENCLAW_DIR, "openviking.env.ps1");
|
|
2041
|
+
if (existsSync(batPath)) {
|
|
2042
|
+
return { shellPath: batPath, powershellPath: existsSync(ps1Path) ? ps1Path : undefined };
|
|
2043
|
+
}
|
|
2044
|
+
if (existsSync(ps1Path)) {
|
|
2045
|
+
return { shellPath: ps1Path, powershellPath: ps1Path };
|
|
2046
|
+
}
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
const envPath = join(OPENCLAW_DIR, "openviking.env");
|
|
2051
|
+
return existsSync(envPath) ? { shellPath: envPath } : null;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function ensureExistingPluginForUpgrade() {
|
|
2055
|
+
if (!existsSync(PLUGIN_DEST)) {
|
|
2056
|
+
err(
|
|
2057
|
+
tr(
|
|
2058
|
+
`Plugin upgrade mode expects an existing plugin at ${PLUGIN_DEST}. Run the full installer first if this is a fresh install.`,
|
|
2059
|
+
`插件升级模式要求 ${PLUGIN_DEST} 处已存在插件安装。若是首次安装,请先运行完整安装流程。`,
|
|
2060
|
+
),
|
|
2061
|
+
);
|
|
2062
|
+
process.exit(1);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
744
2066
|
async function main() {
|
|
745
2067
|
console.log("");
|
|
746
2068
|
bold(tr("🦣 OpenClaw + OpenViking Installer", "🦣 OpenClaw + OpenViking 一键安装"));
|
|
747
2069
|
console.log("");
|
|
748
2070
|
|
|
2071
|
+
ensurePluginOnlyOperationArgs();
|
|
749
2072
|
await selectWorkdir();
|
|
2073
|
+
if (rollbackLastUpgrade) {
|
|
2074
|
+
info(tr("Mode: rollback last plugin upgrade", "模式: 回滚最近一次插件升级"));
|
|
2075
|
+
if (pluginVersionExplicit) {
|
|
2076
|
+
warn("--plugin-version is ignored in --rollback mode.");
|
|
2077
|
+
}
|
|
2078
|
+
await rollbackLastUpgradeOperation();
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
await resolveDefaultPluginVersion();
|
|
2082
|
+
validateRequestedPluginVersion();
|
|
750
2083
|
info(tr(`Target: ${OPENCLAW_DIR}`, `目标实例: ${OPENCLAW_DIR}`));
|
|
2084
|
+
info(tr(`Repository: ${REPO}`, `仓库: ${REPO}`));
|
|
2085
|
+
info(tr(`Plugin version: ${PLUGIN_VERSION}`, `插件版本: ${PLUGIN_VERSION}`));
|
|
2086
|
+
if (openvikingVersion) {
|
|
2087
|
+
info(tr(`OpenViking version: ${openvikingVersion}`, `OpenViking 版本: ${openvikingVersion}`));
|
|
2088
|
+
}
|
|
751
2089
|
|
|
752
|
-
|
|
2090
|
+
if (upgradePluginOnly) {
|
|
2091
|
+
selectedMode = "local";
|
|
2092
|
+
info("Mode: plugin upgrade only (backup old plugin, clean only OpenViking plugin config, keep ov.conf)");
|
|
2093
|
+
} else {
|
|
2094
|
+
await selectMode();
|
|
2095
|
+
}
|
|
753
2096
|
info(tr(`Mode: ${selectedMode}`, `模式: ${selectedMode}`));
|
|
754
2097
|
|
|
755
|
-
if (
|
|
2098
|
+
if (upgradePluginOnly) {
|
|
2099
|
+
await checkOpenClaw();
|
|
2100
|
+
await resolvePluginConfig();
|
|
2101
|
+
await checkOpenClawCompatibility();
|
|
2102
|
+
await prepareStrongPluginUpgrade();
|
|
2103
|
+
} else if (selectedMode === "local") {
|
|
756
2104
|
await validateEnvironment();
|
|
757
2105
|
await checkOpenClaw();
|
|
2106
|
+
// Resolve plugin config after OpenClaw is available (for version detection)
|
|
2107
|
+
await resolvePluginConfig();
|
|
2108
|
+
await checkOpenClawCompatibility();
|
|
2109
|
+
checkRequestedOpenVikingCompatibility();
|
|
758
2110
|
await installOpenViking();
|
|
759
2111
|
await configureOvConf();
|
|
760
2112
|
} else {
|
|
761
2113
|
await checkOpenClaw();
|
|
2114
|
+
await resolvePluginConfig();
|
|
2115
|
+
await checkOpenClawCompatibility();
|
|
762
2116
|
await collectRemoteConfig();
|
|
763
2117
|
}
|
|
764
2118
|
|
|
765
2119
|
let pluginPath;
|
|
766
|
-
const localPluginDir = openvikingRepo ? join(openvikingRepo, "examples", "openclaw-plugin") : "";
|
|
2120
|
+
const localPluginDir = openvikingRepo ? join(openvikingRepo, "examples", resolvedPluginDir || "openclaw-plugin") : "";
|
|
767
2121
|
if (openvikingRepo && existsSync(join(localPluginDir, "index.ts"))) {
|
|
768
2122
|
pluginPath = localPluginDir;
|
|
2123
|
+
PLUGIN_DEST = join(OPENCLAW_DIR, "extensions", resolvedPluginId || "openviking");
|
|
769
2124
|
info(tr(`Using local plugin from repo: ${pluginPath}`, `使用仓库内插件: ${pluginPath}`));
|
|
770
|
-
|
|
2125
|
+
await deployPluginFromLocal(pluginPath);
|
|
771
2126
|
info(tr("Installing plugin npm dependencies...", "正在安装插件 npm 依赖..."));
|
|
772
|
-
|
|
773
|
-
}
|
|
2127
|
+
pluginPath = PLUGIN_DEST;
|
|
774
2128
|
} else {
|
|
775
|
-
await
|
|
2129
|
+
await deployPluginFromRemote();
|
|
776
2130
|
pluginPath = PLUGIN_DEST;
|
|
777
2131
|
}
|
|
778
2132
|
|
|
779
|
-
await configureOpenClawPlugin(
|
|
780
|
-
|
|
781
|
-
|
|
2133
|
+
await configureOpenClawPlugin(
|
|
2134
|
+
upgradePluginOnly
|
|
2135
|
+
? {
|
|
2136
|
+
runtimeConfig: upgradeRuntimeConfig,
|
|
2137
|
+
skipGatewayMode: true,
|
|
2138
|
+
claimSlot: installedUpgradeState ? shouldClaimTargetSlot(installedUpgradeState) : true,
|
|
2139
|
+
}
|
|
2140
|
+
: { preserveExistingConfig: false },
|
|
2141
|
+
);
|
|
2142
|
+
await writeInstallStateFile({
|
|
2143
|
+
operation: upgradePluginOnly ? "upgrade" : "install",
|
|
2144
|
+
fromVersion: upgradeAudit?.fromVersion || "",
|
|
2145
|
+
configBackupPath: upgradeAudit?.configBackupPath || "",
|
|
2146
|
+
pluginBackups: upgradeAudit?.pluginBackups || [],
|
|
782
2147
|
});
|
|
2148
|
+
if (upgradeAudit) {
|
|
2149
|
+
upgradeAudit.completedAt = new Date().toISOString();
|
|
2150
|
+
await writeUpgradeAuditFile(upgradeAudit);
|
|
2151
|
+
}
|
|
2152
|
+
let envFiles = getExistingEnvFiles();
|
|
2153
|
+
if (!upgradePluginOnly) {
|
|
2154
|
+
envFiles = await writeOpenvikingEnv({
|
|
2155
|
+
includePython: selectedMode === "local",
|
|
2156
|
+
});
|
|
2157
|
+
} else if (!envFiles && OPENCLAW_DIR !== DEFAULT_OPENCLAW_DIR) {
|
|
2158
|
+
envFiles = await writeOpenvikingEnv({ includePython: false });
|
|
2159
|
+
}
|
|
783
2160
|
|
|
784
2161
|
console.log("");
|
|
785
2162
|
bold("═══════════════════════════════════════════════════════════");
|
|
@@ -787,6 +2164,16 @@ async function main() {
|
|
|
787
2164
|
bold("═══════════════════════════════════════════════════════════");
|
|
788
2165
|
console.log("");
|
|
789
2166
|
|
|
2167
|
+
if (upgradeAudit) {
|
|
2168
|
+
info(tr(`Upgrade path recorded: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`, `已记录升级路径: ${upgradeAudit.fromVersion} -> ${upgradeAudit.toVersion}`));
|
|
2169
|
+
info(tr(`Rollback config backup: ${upgradeAudit.configBackupPath}`, `回滚配置备份: ${upgradeAudit.configBackupPath}`));
|
|
2170
|
+
for (const pluginBackup of upgradeAudit.pluginBackups || []) {
|
|
2171
|
+
info(tr(`Rollback plugin backup: ${pluginBackup.backupDir}`, `回滚插件备份: ${pluginBackup.backupDir}`));
|
|
2172
|
+
}
|
|
2173
|
+
info(tr(`Rollback audit file: ${getUpgradeAuditPath()}`, `回滚审计文件: ${getUpgradeAuditPath()}`));
|
|
2174
|
+
console.log("");
|
|
2175
|
+
}
|
|
2176
|
+
|
|
790
2177
|
if (selectedMode === "local") {
|
|
791
2178
|
info(tr("Run these commands to start OpenClaw + OpenViking:", "请按以下命令启动 OpenClaw + OpenViking:"));
|
|
792
2179
|
} else {
|
|
@@ -799,6 +2186,14 @@ async function main() {
|
|
|
799
2186
|
console.log("");
|
|
800
2187
|
|
|
801
2188
|
if (selectedMode === "local") {
|
|
2189
|
+
if (envFiles?.shellPath && !IS_WIN) {
|
|
2190
|
+
info(
|
|
2191
|
+
tr(
|
|
2192
|
+
'If source fails, set: export OPENVIKING_PYTHON="$(command -v python3)"',
|
|
2193
|
+
'若 source 失败,可执行: export OPENVIKING_PYTHON="$(command -v python3)"',
|
|
2194
|
+
),
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
802
2197
|
info(tr(`You can edit the config freely: ${OPENVIKING_DIR}/ov.conf`, `你可以按需自由修改配置文件: ${OPENVIKING_DIR}/ov.conf`));
|
|
803
2198
|
} else {
|
|
804
2199
|
info(tr(`Remote server: ${remoteBaseUrl}`, `远程服务器: ${remoteBaseUrl}`));
|