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