tencent-claw-shield 0.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1663 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import readline from "node:readline";
7
+ import { spawnSync } from "node:child_process";
8
+ import JSON5 from "json5";
9
+
10
+ const DEFAULT_PLUGIN_ID = "claw-shield";
11
+ const DEFAULT_PLUGIN_SPEC = "tencent-claw-shield";
12
+ const DEFAULT_AUTH_FILE_NAME = "remote-guard-auth.json";
13
+ const INSTALL_REPORT_PATH = "/api/guardrails/plugin/install_report";
14
+ const API_KEY_ENV_NAME = "RUNTIME_GUARDRAIL_API_KEY";
15
+ const DEFAULT_OPENCLAW_STATE_DIR = path.join(os.homedir(), ".openclaw");
16
+
17
+ function printHelp() {
18
+ console.log(`claw-shieldctl
19
+
20
+ Usage:
21
+ claw-shieldctl install [options]
22
+ claw-shieldctl update [options]
23
+ claw-shieldctl uninstall [options]
24
+ claw-shieldctl bypass [options]
25
+ claw-shieldctl resume [options]
26
+ claw-shieldctl set-api-key [options]
27
+ claw-shieldctl set-server [options]
28
+ claw-shieldctl set-websocket [options]
29
+ claw-shieldctl set-config [options]
30
+ claw-shieldctl reload [options]
31
+ claw-shieldctl sync [options] # reload 的兼容别名
32
+
33
+ Options:
34
+ --plugin-spec <spec> 插件 npm spec,默认 ${DEFAULT_PLUGIN_SPEC}
35
+ --plugin-id <id> 插件 id,默认 ${DEFAULT_PLUGIN_ID}
36
+ --config-path <path> 自定义 remote-guard-config.json 路径(高级覆盖项)
37
+ --api-key <key> 直接设置 AI Guardrails 平台 API Key(注意避免 shell 历史泄露)
38
+ --server-address <addr> 直接设置远端 Server 地址(格式:[http|https://]ip:port,如 203.0.113.1:80 或 https://203.0.113.1:443)
39
+ --ws-address <addr> 直接设置 WebSocket 地址(格式:[ws|wss://]ip:port,如 203.0.113.1:8081 或 wss://203.0.113.1:8081)
40
+ --openclaw-bin <bin> OpenClaw 可执行文件名/路径,默认 openclaw
41
+ --global 忽略 OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH,强制写入 ~/.openclaw
42
+ --state-dir <dir> 显式指定 OpenClaw state dir(优先级高于环境变量)
43
+ --no-restart 完成后不自动执行 gateway restart
44
+ --purge uninstall 时同时删除认证文件(API Key)
45
+ -h, --help 查看帮助
46
+ `);
47
+ }
48
+
49
+ function fail(message, exitCode = 1) {
50
+ console.error(`[claw-shieldctl] ${message}`);
51
+ process.exit(exitCode);
52
+ }
53
+
54
+ function resolveUserPath(input) {
55
+ const trimmed = input.trim();
56
+ if (!trimmed) {
57
+ return trimmed;
58
+ }
59
+ if (trimmed === "~") {
60
+ return os.homedir();
61
+ }
62
+ if (trimmed.startsWith("~/")) {
63
+ return path.join(os.homedir(), trimmed.slice(2));
64
+ }
65
+ return path.resolve(trimmed);
66
+ }
67
+
68
+ function resolveOpenClawStateDir(options = {}) {
69
+ if (options.global === true) {
70
+ return DEFAULT_OPENCLAW_STATE_DIR;
71
+ }
72
+
73
+ if (options.stateDir?.trim()) {
74
+ return resolveUserPath(options.stateDir);
75
+ }
76
+
77
+ const override = process.env.OPENCLAW_STATE_DIR?.trim();
78
+ return resolveUserPath(override || DEFAULT_OPENCLAW_STATE_DIR);
79
+ }
80
+
81
+ function resolveOpenClawConfigPath(options = {}, resolvedStateDir = resolveOpenClawStateDir(options)) {
82
+ if (options.global === true || options.stateDir?.trim()) {
83
+ return path.join(resolvedStateDir, "openclaw.json");
84
+ }
85
+
86
+ const override = process.env.OPENCLAW_CONFIG_PATH?.trim();
87
+ if (override) {
88
+ return resolveUserPath(override);
89
+ }
90
+ return path.join(resolvedStateDir, "openclaw.json");
91
+ }
92
+
93
+ function resolveManagedConfigPath(explicitPath) {
94
+ if (!explicitPath?.trim()) {
95
+ return undefined;
96
+ }
97
+ return resolveUserPath(explicitPath);
98
+ }
99
+
100
+ function resolveAuthConfigPath(pluginId, resolvedStateDir) {
101
+ return path.join(resolvedStateDir, pluginId, DEFAULT_AUTH_FILE_NAME);
102
+ }
103
+
104
+ function buildOpenClawCommandEnv(options, resolvedStateDir, openclawConfigPath) {
105
+ const nextEnv = { ...process.env };
106
+
107
+ if (options.global === true || options.stateDir?.trim()) {
108
+ nextEnv.OPENCLAW_STATE_DIR = resolvedStateDir;
109
+ nextEnv.OPENCLAW_CONFIG_PATH = openclawConfigPath;
110
+ }
111
+
112
+ // 如果 openclawBin 是绝对路径(如通过 nvm 自动定位),将其所在目录加入 PATH 最前面,
113
+ // 确保 openclaw 执行时能找到对应版本的 node
114
+ const openclawBin = options.openclawBin;
115
+ if (openclawBin && path.isAbsolute(openclawBin)) {
116
+ const binDir = path.dirname(openclawBin);
117
+ const currentPath = nextEnv.PATH || "";
118
+ if (!currentPath.split(path.delimiter).includes(binDir)) {
119
+ nextEnv.PATH = `${binDir}${path.delimiter}${currentPath}`;
120
+ }
121
+ }
122
+
123
+ return nextEnv;
124
+ }
125
+
126
+ function readConfigFile(configPath) {
127
+ if (!fs.existsSync(configPath)) {
128
+ return {};
129
+ }
130
+ const raw = fs.readFileSync(configPath, "utf-8");
131
+ if (!raw.trim()) {
132
+ return {};
133
+ }
134
+ return JSON5.parse(raw);
135
+ }
136
+
137
+ function writeJsonFile(targetPath, payload, options = {}) {
138
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
139
+ fs.writeFileSync(targetPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
140
+ if (typeof options.mode === "number") {
141
+ try {
142
+ fs.chmodSync(targetPath, options.mode);
143
+ } catch {
144
+ // best effort on cross-platform environments
145
+ }
146
+ }
147
+ }
148
+
149
+ function parseArgs(argv) {
150
+ const args = [...argv];
151
+ const command = args.shift();
152
+ if (!command || command === "-h" || command === "--help" || command === "help") {
153
+ return { command: "help", options: {} };
154
+ }
155
+
156
+ const options = {
157
+ pluginSpec: DEFAULT_PLUGIN_SPEC,
158
+ pluginSpecExplicit: false,
159
+ pluginId: DEFAULT_PLUGIN_ID,
160
+ openclawBin: "openclaw",
161
+ restart: true,
162
+ global: false,
163
+ };
164
+
165
+ while (args.length > 0) {
166
+ const token = args.shift();
167
+ switch (token) {
168
+ case "--plugin-spec":
169
+ options.pluginSpec = args.shift() || fail("--plugin-spec 需要值");
170
+ options.pluginSpecExplicit = true;
171
+ break;
172
+ case "--plugin-id":
173
+ options.pluginId = args.shift() || fail("--plugin-id 需要值");
174
+ break;
175
+ case "--config-path":
176
+ options.configPath = args.shift() || fail("--config-path 需要值");
177
+ break;
178
+ case "--api-key":
179
+ options.apiKey = args.shift() || fail("--api-key 需要值");
180
+ break;
181
+ case "--server-address":
182
+ options.serverAddress = args.shift() || fail("--server-address 需要值");
183
+ break;
184
+ case "--ws-address":
185
+ options.wsAddress = args.shift() || fail("--ws-address 需要值");
186
+ break;
187
+ case "--openclaw-bin":
188
+ options.openclawBin = args.shift() || fail("--openclaw-bin 需要值");
189
+ break;
190
+ case "--global":
191
+ options.global = true;
192
+ break;
193
+ case "--state-dir":
194
+ options.stateDir = args.shift() || fail("--state-dir 需要值");
195
+ break;
196
+ case "--no-restart":
197
+ options.restart = false;
198
+ break;
199
+ case "--purge":
200
+ options.purge = true;
201
+ break;
202
+ case "-h":
203
+ case "--help":
204
+ return { command: "help", options };
205
+ default:
206
+ fail(`未知参数: ${token}`);
207
+ }
208
+ }
209
+
210
+ if (options.global && options.stateDir?.trim()) {
211
+ fail("--global 和 --state-dir 不能同时使用");
212
+ }
213
+
214
+ return { command, options };
215
+ }
216
+
217
+ function ensurePluginAllowed(config, pluginId) {
218
+ const next = typeof config === "object" && config !== null ? { ...config } : {};
219
+ next.plugins = typeof next.plugins === "object" && next.plugins !== null ? { ...next.plugins } : {};
220
+ next.plugins.allow = Array.isArray(next.plugins.allow) ? [...next.plugins.allow] : [];
221
+ if (!next.plugins.allow.includes(pluginId)) {
222
+ next.plugins.allow.push(pluginId);
223
+ }
224
+ return next;
225
+ }
226
+
227
+ function ensurePluginEntry(config, params) {
228
+ const next = ensurePluginAllowed(config, params.pluginId);
229
+ next.plugins.entries =
230
+ typeof next.plugins.entries === "object" && next.plugins.entries !== null
231
+ ? { ...next.plugins.entries }
232
+ : {};
233
+
234
+ const entry =
235
+ typeof next.plugins.entries[params.pluginId] === "object" && next.plugins.entries[params.pluginId] !== null
236
+ ? { ...next.plugins.entries[params.pluginId] }
237
+ : {};
238
+
239
+ entry.enabled = true;
240
+ const currentConfig = typeof entry.config === "object" && entry.config !== null ? { ...entry.config } : {};
241
+ if (params.configPath) {
242
+ currentConfig.remoteGuard = {
243
+ ...(typeof currentConfig.remoteGuard === "object" && currentConfig.remoteGuard !== null
244
+ ? currentConfig.remoteGuard
245
+ : {}),
246
+ configPath: params.configPath,
247
+ };
248
+ } else {
249
+ delete currentConfig.remoteGuard;
250
+ }
251
+ delete currentConfig.configSync;
252
+ entry.config = currentConfig;
253
+
254
+ if (typeof entry.options === "object" && entry.options !== null) {
255
+ const nextOptions = { ...entry.options };
256
+ delete nextOptions.remoteGuard;
257
+ delete nextOptions.configSync;
258
+ entry.options = nextOptions;
259
+ }
260
+
261
+ next.plugins.entries[params.pluginId] = entry;
262
+ return next;
263
+ }
264
+
265
+ function resolveManagedPluginInstallPath(pluginId, resolvedStateDir) {
266
+ return path.join(resolvedStateDir, "extensions", pluginId);
267
+ }
268
+
269
+ function readJsonFileIfExists(filePath) {
270
+ if (!fs.existsSync(filePath)) {
271
+ return undefined;
272
+ }
273
+
274
+ try {
275
+ const raw = fs.readFileSync(filePath, "utf-8");
276
+ if (!raw.trim()) {
277
+ return undefined;
278
+ }
279
+ return JSON.parse(raw);
280
+ } catch {
281
+ return undefined;
282
+ }
283
+ }
284
+
285
+ function readInstalledPluginVersion(pluginId, resolvedStateDir) {
286
+ const installPath = resolveManagedPluginInstallPath(pluginId, resolvedStateDir);
287
+ const packageJson = readJsonFileIfExists(path.join(installPath, "package.json"));
288
+ if (typeof packageJson?.version === "string" && packageJson.version.trim()) {
289
+ return packageJson.version.trim();
290
+ }
291
+
292
+ const manifest = readJsonFileIfExists(path.join(installPath, "openclaw.plugin.json"));
293
+ if (typeof manifest?.version === "string" && manifest.version.trim()) {
294
+ return manifest.version.trim();
295
+ }
296
+
297
+ return undefined;
298
+ }
299
+
300
+ function ensurePluginTrackedInstallRecord(config, params) {
301
+ const next = ensurePluginEntry(config, params);
302
+ next.plugins.installs =
303
+ typeof next.plugins.installs === "object" && next.plugins.installs !== null
304
+ ? { ...next.plugins.installs }
305
+ : {};
306
+
307
+ const installedVersion = readInstalledPluginVersion(params.pluginId, params.resolvedStateDir);
308
+
309
+ next.plugins.installs[params.pluginId] = {
310
+ ...(typeof installedVersion === "string" && installedVersion ? { version: installedVersion } : {}),
311
+ installedAt: new Date().toISOString(),
312
+ source: "npm",
313
+ spec: params.pluginSpec,
314
+ installPath: resolveManagedPluginInstallPath(params.pluginId, params.resolvedStateDir),
315
+ };
316
+
317
+ return next;
318
+ }
319
+
320
+ function removeManagedPluginInstallDir(pluginId, resolvedStateDir) {
321
+ const installPath = resolveManagedPluginInstallPath(pluginId, resolvedStateDir);
322
+ if (!fs.existsSync(installPath)) {
323
+ return;
324
+ }
325
+
326
+ fs.rmSync(installPath, { recursive: true, force: true });
327
+ console.log(`[claw-shieldctl] 已移除插件目录 ${installPath}`);
328
+ }
329
+
330
+ /**
331
+ * 从 openclaw.json 中移除插件的所有配置记录
332
+ */
333
+ function removePluginFromConfig(openclawConfigPath, pluginId) {
334
+ const config = readJsonFileIfExists(openclawConfigPath);
335
+ if (!config) {
336
+ return;
337
+ }
338
+
339
+ let changed = false;
340
+
341
+ // 移除 plugins.entries 中的记录
342
+ if (config.plugins?.entries?.[pluginId]) {
343
+ delete config.plugins.entries[pluginId];
344
+ changed = true;
345
+ }
346
+
347
+ // 移除 plugins.installs 中的记录
348
+ if (config.plugins?.installs?.[pluginId]) {
349
+ delete config.plugins.installs[pluginId];
350
+ changed = true;
351
+ }
352
+
353
+ // 移除 plugins.allowed 中的记录
354
+ if (Array.isArray(config.plugins?.allowed)) {
355
+ const idx = config.plugins.allowed.indexOf(pluginId);
356
+ if (idx >= 0) {
357
+ config.plugins.allowed.splice(idx, 1);
358
+ changed = true;
359
+ }
360
+ }
361
+
362
+ if (changed) {
363
+ writeJsonFile(openclawConfigPath, config);
364
+ console.log(`[claw-shieldctl] 已从 ${openclawConfigPath} 中移除 ${pluginId} 相关配置`);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * 移除认证文件(API Key)
370
+ */
371
+ function removeAuthConfig(authConfigPath) {
372
+ if (!fs.existsSync(authConfigPath)) {
373
+ return;
374
+ }
375
+ fs.unlinkSync(authConfigPath);
376
+ console.log(`[claw-shieldctl] 已移除认证文件 ${authConfigPath}`);
377
+
378
+ // 尝试清理空目录
379
+ const dir = path.dirname(authConfigPath);
380
+ try {
381
+ const entries = fs.readdirSync(dir);
382
+ if (entries.length === 0) {
383
+ fs.rmdirSync(dir);
384
+ console.log(`[claw-shieldctl] 已移除空目录 ${dir}`);
385
+ }
386
+ } catch {
387
+ // 忽略目录清理失败
388
+ }
389
+ }
390
+
391
+ function uninstallPluginForReinstall(params) {
392
+ const uninstallResult = runCommand(
393
+ params.openclawBin,
394
+ ["plugins", "uninstall", params.pluginId, "--force"],
395
+ { captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
396
+ );
397
+ if (uninstallResult.error) {
398
+ fail(`执行 ${params.openclawBin} plugins uninstall ${params.pluginId} 失败:${uninstallResult.error.message}`);
399
+ }
400
+
401
+ if ((uninstallResult.status ?? 1) === 0) {
402
+ echoCapturedOutput(uninstallResult);
403
+ return;
404
+ }
405
+
406
+ console.warn(
407
+ `[claw-shieldctl] 卸载旧版本返回非 0,将继续按目录重装方式更新 ${params.pluginId}。`,
408
+ );
409
+ echoCapturedOutput(uninstallResult);
410
+ }
411
+
412
+ function readPluginEntry(config, pluginId) {
413
+ return config?.plugins?.entries?.[pluginId] ?? {};
414
+ }
415
+
416
+ function readStoredApiKey(authConfigPath) {
417
+ if (!fs.existsSync(authConfigPath)) {
418
+ return undefined;
419
+ }
420
+ try {
421
+ const raw = fs.readFileSync(authConfigPath, "utf-8");
422
+ const parsed = JSON.parse(raw);
423
+ if (typeof parsed?.auth?.key === "string" && parsed.auth.key.trim()) {
424
+ return parsed.auth.key.trim();
425
+ }
426
+ if (typeof parsed?.key === "string" && parsed.key.trim()) {
427
+ return parsed.key.trim();
428
+ }
429
+ } catch {
430
+ return undefined;
431
+ }
432
+ return undefined;
433
+ }
434
+
435
+ function maskApiKey(apiKey) {
436
+ if (!apiKey) {
437
+ return "<empty>";
438
+ }
439
+ if (apiKey.length <= 8) {
440
+ return `${apiKey.slice(0, 2)}***${apiKey.slice(-1)}`;
441
+ }
442
+ return `${apiKey.slice(0, 4)}***${apiKey.slice(-4)}`;
443
+ }
444
+
445
+ async function promptForSecret(prompt) {
446
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
447
+ fail(`当前终端不可交互,请通过 --api-key 或环境变量 ${API_KEY_ENV_NAME} 提供 API Key。`);
448
+ }
449
+
450
+ const rl = readline.createInterface({
451
+ input: process.stdin,
452
+ output: process.stdout,
453
+ terminal: true,
454
+ });
455
+
456
+ try {
457
+ const answer = await new Promise((resolve) => {
458
+ rl.question(prompt, resolve);
459
+ });
460
+ return typeof answer === "string" ? answer.trim() : "";
461
+ } finally {
462
+ rl.close();
463
+ }
464
+ }
465
+
466
+ async function resolveApiKey(params) {
467
+ const fromArg = params.apiKey?.trim();
468
+ if (fromArg) {
469
+ return fromArg;
470
+ }
471
+
472
+ const fromEnv = process.env[API_KEY_ENV_NAME]?.trim();
473
+ if (fromEnv) {
474
+ return fromEnv;
475
+ }
476
+
477
+ if (params.allowStoredValue !== false) {
478
+ const stored = readStoredApiKey(params.authConfigPath);
479
+ if (stored) {
480
+ return stored;
481
+ }
482
+ }
483
+
484
+ const prompted = await promptForSecret("请输入 AI Guardrails 平台 API Key: ");
485
+ if (!prompted) {
486
+ fail("API Key 不能为空。");
487
+ }
488
+ return prompted;
489
+ }
490
+
491
+ function writeApiKey(authConfigPath, apiKey) {
492
+ writeJsonFile(
493
+ authConfigPath,
494
+ {
495
+ auth: {
496
+ key: apiKey,
497
+ },
498
+ },
499
+ { mode: 0o600 },
500
+ );
501
+ }
502
+
503
+ function warnIfEnvApiKeyDiffers(storedApiKey) {
504
+ const envApiKey = process.env[API_KEY_ENV_NAME]?.trim();
505
+ if (!envApiKey || !storedApiKey || envApiKey === storedApiKey) {
506
+ return;
507
+ }
508
+
509
+ console.warn(
510
+ `[claw-shieldctl] 检测到环境变量 ${API_KEY_ENV_NAME} (${maskApiKey(envApiKey)}) 与本地已保存的 API Key (${maskApiKey(storedApiKey)}) 不一致;本次 install/set-api-key 会优先取环境变量并写回本地文件,但插件运行时只会读取当前 OpenClaw state dir 下的 remote-guard-auth.json。若环境变量是旧值,请先清理该环境变量后再重新执行命令。`,
511
+ );
512
+ }
513
+
514
+ /**
515
+ * 解析地址字符串,支持以下格式:
516
+ * ip:port → { protocol: "http", ip, port }
517
+ * http://ip:port → { protocol: "http", ip, port }
518
+ * https://ip:port → { protocol: "https", ip, port }
519
+ * ws://ip:port → { protocol: "ws", ip, port }
520
+ * wss://ip:port → { protocol: "wss", ip, port }
521
+ * @returns {{ protocol: string, ip: string, port: number }} 或 null(格式无效)
522
+ */
523
+ function parseAddress(address) {
524
+ const trimmed = (address || "").trim();
525
+ if (!trimmed) {
526
+ return null;
527
+ }
528
+
529
+ let protocol = "http";
530
+ let rest = trimmed;
531
+
532
+ // 提取协议前缀
533
+ const protoMatch = rest.match(/^(https?|wss?):\/\//i);
534
+ if (protoMatch) {
535
+ protocol = protoMatch[1].toLowerCase();
536
+ rest = rest.slice(protoMatch[0].length);
537
+ }
538
+
539
+ const lastColon = rest.lastIndexOf(":");
540
+ if (lastColon < 1) {
541
+ return null;
542
+ }
543
+ const ip = rest.slice(0, lastColon);
544
+ const portStr = rest.slice(lastColon + 1);
545
+ const port = Number(portStr);
546
+ if (!ip || !Number.isInteger(port) || port < 1 || port > 65535) {
547
+ return null;
548
+ }
549
+ return { protocol, ip, port };
550
+ }
551
+
552
+ /**
553
+ * 将 http/https 协议映射为 ws/wss(用于 WebSocket 地址)
554
+ */
555
+ function toWsProtocol(protocol) {
556
+ switch (protocol) {
557
+ case "https":
558
+ case "wss":
559
+ return "wss";
560
+ case "http":
561
+ case "ws":
562
+ default:
563
+ return "ws";
564
+ }
565
+ }
566
+
567
+ /**
568
+ * 交互式提示用户输入地址
569
+ */
570
+ async function promptForAddress(prompt, defaultValue) {
571
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
572
+ return undefined;
573
+ }
574
+
575
+ const rl = readline.createInterface({
576
+ input: process.stdin,
577
+ output: process.stdout,
578
+ terminal: true,
579
+ });
580
+
581
+ try {
582
+ const answer = await new Promise((resolve) => {
583
+ rl.question(prompt, resolve);
584
+ });
585
+ const trimmed = typeof answer === "string" ? answer.trim() : "";
586
+ return trimmed || defaultValue;
587
+ } finally {
588
+ rl.close();
589
+ }
590
+ }
591
+
592
+ /**
593
+ * 解析 server 地址(从命令行参数或交互式提示)
594
+ */
595
+ async function resolveServerAddress(params) {
596
+ if (params.serverAddress) {
597
+ const parsed = parseAddress(params.serverAddress);
598
+ if (!parsed) {
599
+ fail(`--server-address 格式无效,应为 [http|https://]ip:port,如 203.0.113.1:80 或 https://203.0.113.1:443`);
600
+ }
601
+ return parsed;
602
+ }
603
+
604
+ // 读取已存在的配置值
605
+ const existing = readExistingRemoteGuardConfig(params.pluginInstallPath);
606
+ if (params.allowStoredValue !== false && existing?.server?.ip && existing?.server?.port) {
607
+ return { protocol: existing.server.protocol || "http", ip: existing.server.ip, port: existing.server.port };
608
+ }
609
+
610
+ // 交互式提示
611
+ const currentProtocol = existing?.server?.protocol || "http";
612
+ const hint = existing?.server?.ip
613
+ ? ` [当前: ${currentProtocol}://${existing.server.ip}:${existing.server.port}]`
614
+ : "";
615
+ const input = await promptForAddress(
616
+ `请输入远端 Server 地址 ([http|https://]ip:port,如 203.0.113.1:80 或 https://203.0.113.1:443)${hint}: `,
617
+ );
618
+ if (!input) {
619
+ if (existing?.server?.ip && existing?.server?.port) {
620
+ return { protocol: currentProtocol, ip: existing.server.ip, port: existing.server.port };
621
+ }
622
+ fail("Server 地址不能为空。请通过 --server-address 或交互式输入提供。");
623
+ }
624
+ const parsed = parseAddress(input);
625
+ if (!parsed) {
626
+ fail(`Server 地址格式无效,应为 [http|https://]ip:port,如 203.0.113.1:80 或 https://203.0.113.1:443`);
627
+ }
628
+ return parsed;
629
+ }
630
+
631
+ /**
632
+ * 解析 websocket 地址(从命令行参数或交互式提示)
633
+ */
634
+ async function resolveWebSocketAddress(params) {
635
+ if (params.wsAddress) {
636
+ const parsed = parseAddress(params.wsAddress);
637
+ if (!parsed) {
638
+ fail(`--ws-address 格式无效,应为 [ws|wss|http|https://]ip:port,如 203.0.113.1:8081 或 wss://203.0.113.1:8081`);
639
+ }
640
+ return parsed;
641
+ }
642
+
643
+ // 读取已存在的配置值
644
+ const existing = readExistingRemoteGuardConfig(params.pluginInstallPath);
645
+ if (params.allowStoredValue !== false && existing?.websocket?.ip && existing?.websocket?.port) {
646
+ return { protocol: existing.websocket.protocol || "ws", ip: existing.websocket.ip, port: existing.websocket.port };
647
+ }
648
+
649
+ // 交互式提示
650
+ const currentProtocol = existing?.websocket?.protocol || "ws";
651
+ const hint = existing?.websocket?.ip
652
+ ? ` [当前: ${currentProtocol}://${existing.websocket.ip}:${existing.websocket.port}]`
653
+ : "";
654
+ const input = await promptForAddress(
655
+ `请输入 WebSocket 地址 ([ws|wss://]ip:port,如 203.0.113.1:8081 或 wss://203.0.113.1:8081)${hint}: `,
656
+ );
657
+ if (!input) {
658
+ if (existing?.websocket?.ip && existing?.websocket?.port) {
659
+ return { protocol: currentProtocol, ip: existing.websocket.ip, port: existing.websocket.port };
660
+ }
661
+ fail("WebSocket 地址不能为空。请通过 --ws-address 或交互式输入提供。");
662
+ }
663
+ const parsed = parseAddress(input);
664
+ if (!parsed) {
665
+ fail(`WebSocket 地址格式无效,应为 [ws|wss://]ip:port,如 203.0.113.1:8081 或 wss://203.0.113.1:8081`);
666
+ }
667
+ return parsed;
668
+ }
669
+
670
+ /**
671
+ * 读取已安装插件目录下的 remote-guard-config.json
672
+ */
673
+ function readExistingRemoteGuardConfig(pluginInstallPath) {
674
+ if (!pluginInstallPath) {
675
+ return undefined;
676
+ }
677
+ const configPath = path.join(pluginInstallPath, "remote-guard-config.json");
678
+ return readJsonFileIfExists(configPath);
679
+ }
680
+
681
+ /**
682
+ * 将 server / websocket 地址写入已安装插件目录的 remote-guard-config.json
683
+ */
684
+ function writeServerAndWebSocketConfig(pluginInstallPath, serverAddr, wsAddr) {
685
+ const configPath = path.join(pluginInstallPath, "remote-guard-config.json");
686
+ const existing = readJsonFileIfExists(configPath) || {};
687
+ const next = {
688
+ ...existing,
689
+ server: {
690
+ ...existing.server,
691
+ protocol: serverAddr.protocol,
692
+ ip: serverAddr.ip,
693
+ port: serverAddr.port,
694
+ },
695
+ websocket: {
696
+ ...existing.websocket,
697
+ ip: wsAddr.ip,
698
+ port: wsAddr.port,
699
+ protocol: toWsProtocol(wsAddr.protocol),
700
+ },
701
+ };
702
+ writeJsonFile(configPath, next);
703
+ console.log(
704
+ `[claw-shieldctl] Server 地址已写入: ${serverAddr.protocol}://${serverAddr.ip}:${serverAddr.port}`,
705
+ );
706
+ console.log(
707
+ `[claw-shieldctl] WebSocket 地址已写入: ${toWsProtocol(wsAddr.protocol)}://${wsAddr.ip}:${wsAddr.port}`,
708
+ );
709
+ console.log(`[claw-shieldctl] 配置文件: ${configPath}`);
710
+ }
711
+
712
+ /**
713
+ * 仅写入 server 地址
714
+ */
715
+ function writeServerConfig(pluginInstallPath, serverAddr) {
716
+ const configPath = path.join(pluginInstallPath, "remote-guard-config.json");
717
+ const existing = readJsonFileIfExists(configPath) || {};
718
+ const next = {
719
+ ...existing,
720
+ server: {
721
+ ...existing.server,
722
+ protocol: serverAddr.protocol,
723
+ ip: serverAddr.ip,
724
+ port: serverAddr.port,
725
+ },
726
+ };
727
+ writeJsonFile(configPath, next);
728
+ console.log(
729
+ `[claw-shieldctl] Server 地址已写入: ${serverAddr.protocol}://${serverAddr.ip}:${serverAddr.port}`,
730
+ );
731
+ console.log(`[claw-shieldctl] 配置文件: ${configPath}`);
732
+ }
733
+
734
+ /**
735
+ * 仅写入 websocket 地址
736
+ */
737
+ function writeWebSocketConfig(pluginInstallPath, wsAddr) {
738
+ const configPath = path.join(pluginInstallPath, "remote-guard-config.json");
739
+ const existing = readJsonFileIfExists(configPath) || {};
740
+ const next = {
741
+ ...existing,
742
+ websocket: {
743
+ ...existing.websocket,
744
+ ip: wsAddr.ip,
745
+ port: wsAddr.port,
746
+ protocol: toWsProtocol(wsAddr.protocol),
747
+ },
748
+ };
749
+ writeJsonFile(configPath, next);
750
+ console.log(
751
+ `[claw-shieldctl] WebSocket 地址已写入: ${toWsProtocol(wsAddr.protocol)}://${wsAddr.ip}:${wsAddr.port}`,
752
+ );
753
+ console.log(`[claw-shieldctl] 配置文件: ${configPath}`);
754
+ }
755
+
756
+ const OPENCLAW_NPM_EXTRACT_FAILURE_RE =
757
+ /failed to extract archive:\s*SafeOpenError:\s*path is not a regular file under root/i;
758
+ const OPENCLAW_PLUGIN_ALREADY_EXISTS_RE = /plugin already exists\b/i;
759
+ const OPENCLAW_SECURITY_SCAN_BLOCKED_RE =
760
+ /installation blocked:\s*dangerous code patterns detected/i;
761
+
762
+ function echoCapturedOutput(result) {
763
+ if (typeof result.stdout === "string" && result.stdout.length > 0) {
764
+ process.stdout.write(result.stdout);
765
+ }
766
+ if (typeof result.stderr === "string" && result.stderr.length > 0) {
767
+ process.stderr.write(result.stderr);
768
+ }
769
+ }
770
+
771
+ function readCombinedOutput(result) {
772
+ return `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
773
+ }
774
+
775
+ function normalizePluginInstallResult(result, params, options = {}) {
776
+ if (result.error) {
777
+ fail(`执行 ${params.openclawBin} plugins install ${params.installTarget} 失败:${result.error.message}`);
778
+ }
779
+ if ((result.status ?? 1) === 0) {
780
+ echoCapturedOutput(result);
781
+ return { alreadyInstalled: false, needsArchiveFallback: false, needsUnsafeRetry: false };
782
+ }
783
+
784
+ const combinedOutput = readCombinedOutput(result);
785
+ if (options.allowArchiveFallback && OPENCLAW_NPM_EXTRACT_FAILURE_RE.test(combinedOutput)) {
786
+ return { alreadyInstalled: false, needsArchiveFallback: true, needsUnsafeRetry: false };
787
+ }
788
+
789
+ if (options.allowUnsafeRetry && OPENCLAW_SECURITY_SCAN_BLOCKED_RE.test(combinedOutput)) {
790
+ return { alreadyInstalled: false, needsArchiveFallback: false, needsUnsafeRetry: true };
791
+ }
792
+
793
+ if (OPENCLAW_PLUGIN_ALREADY_EXISTS_RE.test(combinedOutput)) {
794
+ console.log(
795
+ `[claw-shieldctl] 检测到插件 ${params.pluginId} 已存在,将跳过重复安装并继续执行 API Key 配置。若需升级插件,请改用 update 命令。`,
796
+ );
797
+ return { alreadyInstalled: true, needsArchiveFallback: false, needsUnsafeRetry: false };
798
+ }
799
+
800
+ echoCapturedOutput(result);
801
+ process.exit(result.status ?? 1);
802
+ }
803
+
804
+ function runCommand(bin, args, options = {}) {
805
+ const captureOutput = options.captureOutput === true;
806
+ const echoOutput = captureOutput && options.echoOutput !== false;
807
+ const baseOptions = {
808
+ ...(options.cwd ? { cwd: options.cwd } : {}),
809
+ ...(options.env ? { env: options.env } : {}),
810
+ };
811
+ const result = spawnSync(bin, args, captureOutput
812
+ ? { ...baseOptions, stdio: "pipe", encoding: "utf-8" }
813
+ : { ...baseOptions, stdio: "inherit" });
814
+ if (echoOutput) {
815
+ echoCapturedOutput(result);
816
+ }
817
+ return result;
818
+ }
819
+
820
+ function runOrFail(bin, args, options = {}) {
821
+ const result = runCommand(bin, args, options);
822
+ if (result.error) {
823
+ fail(`执行 ${bin} ${args.join(" ")} 失败:${result.error.message}`);
824
+ }
825
+ if ((result.status ?? 1) !== 0) {
826
+ process.exit(result.status ?? 1);
827
+ }
828
+ return result;
829
+ }
830
+
831
+ function tryRun(bin, args, options = {}) {
832
+ const result = runCommand(bin, args, options);
833
+ if (result.error) {
834
+ return false;
835
+ }
836
+ return (result.status ?? 1) === 0;
837
+ }
838
+
839
+ function parsePackedArchiveFilename(raw) {
840
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
841
+ if (!trimmed) {
842
+ return undefined;
843
+ }
844
+ const candidates = [trimmed];
845
+ const arrayStart = trimmed.indexOf("[");
846
+ if (arrayStart > 0) {
847
+ candidates.push(trimmed.slice(arrayStart));
848
+ }
849
+ for (const candidate of candidates) {
850
+ try {
851
+ const parsed = JSON.parse(candidate);
852
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
853
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
854
+ const entry = entries[i];
855
+ const filename = entry && typeof entry === "object" ? entry.filename : undefined;
856
+ if (typeof filename === "string" && filename.trim()) {
857
+ return filename.trim();
858
+ }
859
+ }
860
+ } catch {
861
+ // ignore parse failure and fallback to directory scan
862
+ }
863
+ }
864
+ return undefined;
865
+ }
866
+
867
+ function findPackedArchiveInDir(dirPath) {
868
+ try {
869
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
870
+ const tgz = entries
871
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
872
+ .map((entry) => entry.name)
873
+ .sort();
874
+ return tgz[0];
875
+ } catch {
876
+ return undefined;
877
+ }
878
+ }
879
+
880
+ function resolveLocalPluginSpecPath(pluginSpec) {
881
+ const trimmed = typeof pluginSpec === "string" ? pluginSpec.trim() : "";
882
+ if (!trimmed) {
883
+ return undefined;
884
+ }
885
+
886
+ const normalized = trimmed.startsWith("file:") ? trimmed.slice("file:".length) : trimmed;
887
+ const looksLikePath =
888
+ normalized === "." ||
889
+ normalized === ".." ||
890
+ normalized.startsWith("./") ||
891
+ normalized.startsWith("../") ||
892
+ normalized.startsWith("~/") ||
893
+ normalized.startsWith("/") ||
894
+ normalized.endsWith(".tgz") ||
895
+ normalized.endsWith(".tar.gz");
896
+ if (!looksLikePath) {
897
+ return undefined;
898
+ }
899
+
900
+ const resolved = resolveUserPath(normalized);
901
+ if (!fs.existsSync(resolved)) {
902
+ return undefined;
903
+ }
904
+ return resolved;
905
+ }
906
+
907
+ function extractArchiveToPackageDir(archivePath, extractDir) {
908
+ fs.mkdirSync(extractDir, { recursive: true });
909
+ runOrFail("tar", ["-xzf", archivePath, "-C", extractDir], { captureOutput: true });
910
+
911
+ const packageDir = path.join(extractDir, "package");
912
+ if (!fs.existsSync(path.join(packageDir, "package.json"))) {
913
+ fail(`安装失败:解压后未找到 package/package.json(${packageDir})。`);
914
+ }
915
+ return packageDir;
916
+ }
917
+
918
+ function installPluginFromDirectory(params, packageDir) {
919
+ const installResult = runCommand(
920
+ params.openclawBin,
921
+ ["plugins", "install", packageDir],
922
+ { captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
923
+ );
924
+ const normalizedResult = normalizePluginInstallResult(installResult, {
925
+ ...params,
926
+ installTarget: packageDir,
927
+ }, { allowUnsafeRetry: true });
928
+
929
+ if (normalizedResult.needsUnsafeRetry) {
930
+ console.log(
931
+ "[claw-shieldctl] OpenClaw 安全扫描拦截了安装(误报:本插件需要 child_process 和网络通信能力)。" +
932
+ "正在使用 --dangerously-force-unsafe-install 重试...",
933
+ );
934
+ const retryResult = runCommand(
935
+ params.openclawBin,
936
+ ["plugins", "install", packageDir, "--dangerously-force-unsafe-install"],
937
+ { captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
938
+ );
939
+ return normalizePluginInstallResult(retryResult, {
940
+ ...params,
941
+ installTarget: packageDir,
942
+ });
943
+ }
944
+
945
+ return normalizedResult;
946
+ }
947
+
948
+ function installPluginFromPreparedSource(params) {
949
+ if (process.platform === "win32") {
950
+ fail(
951
+ "当前 OpenClaw 的 tgz 安装兼容模式未在 Windows 下启用。请先手动执行 npm pack 解压,再使用 openclaw plugins install <packageDir> 安装。",
952
+ );
953
+ }
954
+
955
+ const localSpecPath = resolveLocalPluginSpecPath(params.pluginSpec);
956
+ if (localSpecPath) {
957
+ const specStat = fs.statSync(localSpecPath);
958
+ if (specStat.isDirectory()) {
959
+ return installPluginFromDirectory(params, localSpecPath);
960
+ }
961
+ }
962
+
963
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "claw-shieldctl-pack-"));
964
+ try {
965
+ let archivePath;
966
+ if (localSpecPath) {
967
+ const specStat = fs.statSync(localSpecPath);
968
+ if (!specStat.isFile()) {
969
+ fail(`安装失败:本地插件来源不是可用文件或目录(${localSpecPath})。`);
970
+ }
971
+ archivePath = localSpecPath;
972
+ console.log("[claw-shieldctl] 检测到本地 tgz 包,将先解压后按目录方式安装到 OpenClaw。");
973
+ } else {
974
+ console.log("[claw-shieldctl] 将使用预打包目录安装方式(npm pack + 本地目录)安装插件。");
975
+ const packResult = runOrFail(
976
+ "npm",
977
+ ["pack", params.pluginSpec, "--ignore-scripts", "--json"],
978
+ { captureOutput: true, echoOutput: false, cwd: tempRoot },
979
+ );
980
+ const packedName =
981
+ parsePackedArchiveFilename(packResult.stdout) ?? findPackedArchiveInDir(tempRoot);
982
+ if (!packedName) {
983
+ fail("安装失败:npm pack 成功但未找到生成的 .tgz 文件。");
984
+ }
985
+
986
+ // npm pack --json 报告的 filename 可能带 scope 子目录(如 tencent-claw-shield-0.1.16.tgz),
987
+ // 但实际文件在 tempRoot 根目录下以展平命名存放(如 tencent-claw-shield-0.1.16.tgz)。
988
+ // 因此先尝试 JSON 报告的路径,若不存在则回退到目录扫描。
989
+ const sourceArchivePath = path.resolve(
990
+ path.isAbsolute(packedName)
991
+ ? packedName
992
+ : path.join(tempRoot, packedName),
993
+ );
994
+
995
+ if (fs.existsSync(sourceArchivePath)) {
996
+ archivePath = sourceArchivePath;
997
+ if (path.dirname(sourceArchivePath) !== tempRoot) {
998
+ archivePath = path.join(tempRoot, path.basename(sourceArchivePath));
999
+ fs.copyFileSync(sourceArchivePath, archivePath);
1000
+ }
1001
+ } else {
1002
+ // JSON 报告的路径不存在,回退到扫描 tempRoot 下的实际 .tgz 文件
1003
+ const fallbackName = findPackedArchiveInDir(tempRoot);
1004
+ if (!fallbackName) {
1005
+ fail(
1006
+ `安装失败:npm pack 输出的文件路径不存在(${sourceArchivePath}),且在临时目录中未找到 .tgz 文件。`,
1007
+ );
1008
+ }
1009
+ archivePath = path.join(tempRoot, fallbackName);
1010
+ console.log(`[claw-shieldctl] npm pack 报告的路径与实际文件不一致,已回退到 ${fallbackName}`);
1011
+ }
1012
+ }
1013
+
1014
+ const extractDir = path.join(tempRoot, "extract");
1015
+ const packageDir = extractArchiveToPackageDir(archivePath, extractDir);
1016
+ return installPluginFromDirectory(params, packageDir);
1017
+ } finally {
1018
+ fs.rmSync(tempRoot, { recursive: true, force: true });
1019
+ }
1020
+ }
1021
+
1022
+ function installPluginWithFallback(params) {
1023
+ const localSpecPath = resolveLocalPluginSpecPath(params.pluginSpec);
1024
+ if (process.platform !== "win32") {
1025
+ return installPluginFromPreparedSource(params);
1026
+ }
1027
+
1028
+ if (localSpecPath && fs.statSync(localSpecPath).isDirectory()) {
1029
+ return installPluginFromDirectory(params, localSpecPath);
1030
+ }
1031
+
1032
+ const result = runCommand(
1033
+ params.openclawBin,
1034
+ ["plugins", "install", params.pluginSpec],
1035
+ { captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
1036
+ );
1037
+ const normalizedResult = normalizePluginInstallResult(result, {
1038
+ ...params,
1039
+ installTarget: params.pluginSpec,
1040
+ }, { allowArchiveFallback: true, allowUnsafeRetry: true });
1041
+ if (normalizedResult.needsArchiveFallback) {
1042
+ return installPluginFromPreparedSource(params);
1043
+ }
1044
+ if (normalizedResult.needsUnsafeRetry) {
1045
+ console.log(
1046
+ "[claw-shieldctl] OpenClaw 安全扫描拦截了安装(误报:本插件需要 child_process 和网络通信能力)。" +
1047
+ "正在使用 --dangerously-force-unsafe-install 重试...",
1048
+ );
1049
+ const retryResult = runCommand(
1050
+ params.openclawBin,
1051
+ ["plugins", "install", params.pluginSpec, "--dangerously-force-unsafe-install"],
1052
+ { captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
1053
+ );
1054
+ return normalizePluginInstallResult(retryResult, {
1055
+ ...params,
1056
+ installTarget: params.pluginSpec,
1057
+ });
1058
+ }
1059
+ return normalizedResult;
1060
+ }
1061
+
1062
+ const OPENCLAW_INSTALL_STAGE_DIR_PREFIX = ".openclaw-install-stage-";
1063
+
1064
+ function cleanupOpenClawInstallStageDirs(resolvedStateDir) {
1065
+ const extensionsDir = path.join(resolvedStateDir, "extensions");
1066
+ if (!fs.existsSync(extensionsDir)) {
1067
+ return;
1068
+ }
1069
+
1070
+ try {
1071
+ const entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
1072
+ for (const entry of entries) {
1073
+ if (!entry.isDirectory() || !entry.name.startsWith(OPENCLAW_INSTALL_STAGE_DIR_PREFIX)) {
1074
+ continue;
1075
+ }
1076
+
1077
+ const targetPath = path.join(extensionsDir, entry.name);
1078
+ fs.rmSync(targetPath, { recursive: true, force: true });
1079
+ console.log(`[claw-shieldctl] 已清理 OpenClaw 安装阶段残留目录 ${targetPath}`);
1080
+ }
1081
+ } catch (error) {
1082
+ const message = error instanceof Error ? error.message : String(error);
1083
+ console.warn(`[claw-shieldctl] 清理 OpenClaw 安装阶段残留目录失败:${message}`);
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * 上报插件安装/卸载结果到远端服务
1089
+ * POST /api/guardrails/plugin/install_report
1090
+ * Authorization: Bearer <api-key>
1091
+ * Body: { action, status, reason }
1092
+ *
1093
+ * AppId、ServiceId、RequestId 由后端根据 API Key 自动注入,无需显式传入
1094
+ */
1095
+ async function reportInstallResult(params) {
1096
+ const { serverBaseUrl, apiKey, action, status, reason } = params;
1097
+ if (!serverBaseUrl || !apiKey) {
1098
+ console.log("[claw-shieldctl] 跳过安装结果上报:缺少 server 地址或 API Key。");
1099
+ return;
1100
+ }
1101
+
1102
+ const url = `${serverBaseUrl}${INSTALL_REPORT_PATH}`;
1103
+ const body = {
1104
+ action: action || "install",
1105
+ status,
1106
+ ...(reason ? { reason } : {}),
1107
+ };
1108
+
1109
+ console.log(`[claw-shieldctl] 正在上报${action === "uninstall" ? "卸载" : "安装"}结果到 ${url}...`);
1110
+
1111
+ try {
1112
+ // HTTPS 请求跳过证书校验
1113
+ if (url.startsWith("https://")) {
1114
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
1115
+ }
1116
+
1117
+ const controller = new AbortController();
1118
+ const timeoutId = setTimeout(() => controller.abort(), 15000);
1119
+
1120
+ const response = await fetch(url, {
1121
+ method: "POST",
1122
+ headers: {
1123
+ "Content-Type": "application/json",
1124
+ Authorization: `Bearer ${apiKey}`,
1125
+ },
1126
+ body: JSON.stringify(body),
1127
+ signal: controller.signal,
1128
+ });
1129
+ clearTimeout(timeoutId);
1130
+
1131
+ if (response.ok) {
1132
+ const responseBody = await response.text();
1133
+ console.log(`[claw-shieldctl] 上报成功:${response.status} ${responseBody.slice(0, 200)}`);
1134
+ } else {
1135
+ const errorText = await response.text();
1136
+ console.warn(`[claw-shieldctl] 上报返回非 2xx:${response.status} ${errorText.slice(0, 200)}`);
1137
+ }
1138
+ } catch (error) {
1139
+ const message = error instanceof Error ? error.message : String(error);
1140
+ console.warn(`[claw-shieldctl] 上报失败(不影响安装/卸载流程):${message}`);
1141
+ }
1142
+ }
1143
+
1144
+ /**
1145
+ * 构建 server base URL(复用 remote-guard-config.json 中的 server 配置或从 pluginInstallPath 读取)
1146
+ */
1147
+ function resolveServerBaseUrl(pluginInstallPath) {
1148
+ const existing = readExistingRemoteGuardConfig(pluginInstallPath);
1149
+ if (!existing?.server?.ip || !existing?.server?.port) {
1150
+ return undefined;
1151
+ }
1152
+ const protocol = existing.server.protocol || "http";
1153
+ return `${protocol}://${existing.server.ip}:${existing.server.port}`;
1154
+ }
1155
+
1156
+ function refreshOrRestart(params) {
1157
+ const refreshed = tryRun(params.openclawBin, [
1158
+ "gateway",
1159
+ "call",
1160
+ "shield.config.refresh",
1161
+ "--json",
1162
+ ], { env: params.openclawCommandEnv });
1163
+ if (refreshed) {
1164
+ return;
1165
+ }
1166
+ if (params.restart) {
1167
+ console.log("[claw-shieldctl] gateway call shield.config.refresh 未成功,将继续按 restart 策略处理。");
1168
+ runOrFail(params.openclawBin, ["gateway", "restart"], { env: params.openclawCommandEnv });
1169
+ return;
1170
+ }
1171
+ console.log("[claw-shieldctl] gateway call shield.config.refresh 未成功,且已按 --no-restart 跳过自动重启。");
1172
+ }
1173
+
1174
+ /**
1175
+ * 检测指定 bin 是否可执行
1176
+ */
1177
+ function isBinAvailable(bin) {
1178
+ try {
1179
+ const result = spawnSync(bin, ["--version"], {
1180
+ stdio: "pipe",
1181
+ encoding: "utf-8",
1182
+ timeout: 5000,
1183
+ });
1184
+ return !result.error && (result.status === 0 || result.status === null);
1185
+ } catch {
1186
+ return false;
1187
+ }
1188
+ }
1189
+
1190
+ /**
1191
+ * 通过 which/command -v 查找可执行文件的绝对路径
1192
+ */
1193
+ function whichBin(bin) {
1194
+ try {
1195
+ const result = spawnSync("which", [bin], {
1196
+ stdio: "pipe",
1197
+ encoding: "utf-8",
1198
+ timeout: 3000,
1199
+ });
1200
+ if (result.status === 0 && result.stdout?.trim()) {
1201
+ return result.stdout.trim();
1202
+ }
1203
+ } catch {
1204
+ // 忽略
1205
+ }
1206
+ return undefined;
1207
+ }
1208
+
1209
+ /**
1210
+ * 扫描 nvm 安装的所有 node 版本,查找包含 openclaw 的版本
1211
+ * 返回 openclaw 的绝对路径,或 undefined
1212
+ */
1213
+ function findOpenClawInNvm() {
1214
+ const nvmDir = process.env.NVM_DIR?.trim() || path.join(os.homedir(), ".nvm");
1215
+ const versionsDir = path.join(nvmDir, "versions", "node");
1216
+
1217
+ if (!fs.existsSync(versionsDir)) {
1218
+ return undefined;
1219
+ }
1220
+
1221
+ try {
1222
+ const versions = fs.readdirSync(versionsDir).sort().reverse(); // 优先尝试最新版本
1223
+ for (const version of versions) {
1224
+ const binDir = path.join(versionsDir, version, "bin");
1225
+ const openclawPath = path.join(binDir, "openclaw");
1226
+ if (fs.existsSync(openclawPath)) {
1227
+ // 验证可执行(用对应 node 版本的 PATH)
1228
+ try {
1229
+ const envWithCorrectNode = {
1230
+ ...process.env,
1231
+ PATH: `${binDir}${path.delimiter}${process.env.PATH || ""}`,
1232
+ };
1233
+ const result = spawnSync(openclawPath, ["--version"], {
1234
+ stdio: "pipe",
1235
+ encoding: "utf-8",
1236
+ timeout: 5000,
1237
+ env: envWithCorrectNode,
1238
+ });
1239
+ if (!result.error && (result.status === 0 || result.status === null)) {
1240
+ return openclawPath;
1241
+ }
1242
+ } catch {
1243
+ continue;
1244
+ }
1245
+ }
1246
+ }
1247
+ } catch {
1248
+ // 扫描失败,忽略
1249
+ }
1250
+
1251
+ return undefined;
1252
+ }
1253
+
1254
+ /**
1255
+ * 扫描常见全局安装路径查找 openclaw
1256
+ */
1257
+ function findOpenClawInGlobalPaths() {
1258
+ const candidates = [
1259
+ "/usr/local/bin/openclaw",
1260
+ "/usr/bin/openclaw",
1261
+ path.join(os.homedir(), ".npm-global", "bin", "openclaw"),
1262
+ ];
1263
+
1264
+ for (const candidate of candidates) {
1265
+ if (fs.existsSync(candidate)) {
1266
+ return candidate;
1267
+ }
1268
+ }
1269
+
1270
+ return undefined;
1271
+ }
1272
+
1273
+ /**
1274
+ * 确认 openclaw 可用,不可用时尝试自动定位,最终仍不可用则给出详细错误提示
1275
+ * @returns 可用的 openclaw 二进制路径
1276
+ */
1277
+ function resolveOpenClawBinOrFail(requestedBin) {
1278
+ // 1. 检查请求的 bin 是否直接可用
1279
+ if (isBinAvailable(requestedBin)) {
1280
+ return requestedBin;
1281
+ }
1282
+
1283
+ // 2. 用 which 查找
1284
+ const whichResult = whichBin(requestedBin);
1285
+ if (whichResult && isBinAvailable(whichResult)) {
1286
+ return whichResult;
1287
+ }
1288
+
1289
+ console.warn(`[claw-shieldctl] 当前 PATH 中未找到 "${requestedBin}",正在尝试自动定位...`);
1290
+
1291
+ // 3. 扫描 nvm 环境
1292
+ const nvmResult = findOpenClawInNvm();
1293
+ if (nvmResult) {
1294
+ console.log(`[claw-shieldctl] 在 nvm 环境中找到 OpenClaw: ${nvmResult}`);
1295
+ return nvmResult;
1296
+ }
1297
+
1298
+ // 4. 扫描全局路径
1299
+ const globalResult = findOpenClawInGlobalPaths();
1300
+ if (globalResult) {
1301
+ console.log(`[claw-shieldctl] 在全局路径中找到 OpenClaw: ${globalResult}`);
1302
+ return globalResult;
1303
+ }
1304
+
1305
+ // 5. 全部失败,给出详细错误提示
1306
+ const nvmDir = process.env.NVM_DIR?.trim() || path.join(os.homedir(), ".nvm");
1307
+ const hasNvm = fs.existsSync(nvmDir);
1308
+
1309
+ let message = `[claw-shieldctl] 错误:未找到 OpenClaw CLI("${requestedBin}")。\n\n`;
1310
+ message += "Claw Shield 是 OpenClaw Agent 的安全插件,需要先安装 OpenClaw 才能使用。\n\n";
1311
+ message += "请按以下步骤操作:\n\n";
1312
+ message += " 1. 安装 OpenClaw:\n";
1313
+ message += " npm install -g openclaw\n\n";
1314
+ message += " 2. 验证安装:\n";
1315
+ message += " openclaw --version\n\n";
1316
+ message += " 3. 重新运行本命令:\n";
1317
+ message += " npx -y tencent-claw-shield install --global\n\n";
1318
+
1319
+ if (hasNvm) {
1320
+ message += "检测到本机已安装 nvm,但在所有 node 版本中均未找到 openclaw。\n";
1321
+ message += "请确认在正确的 node 版本下安装了 openclaw:\n\n";
1322
+ message += " nvm ls # 查看已安装的 node 版本\n";
1323
+ message += " nvm use <version> # 切换到目标版本\n";
1324
+ message += " npm install -g openclaw # 在该版本下安装\n";
1325
+ message += " openclaw --version # 验证\n\n";
1326
+ }
1327
+
1328
+ message += "如果 openclaw 已安装但不在默认 PATH 中,可通过 --openclaw-bin 指定路径:\n\n";
1329
+ message += " npx -y tencent-claw-shield install --global --openclaw-bin /path/to/openclaw\n";
1330
+
1331
+ fail(message);
1332
+ }
1333
+
1334
+ async function main() {
1335
+ const { command, options } = parseArgs(process.argv.slice(2));
1336
+ if (command === "help") {
1337
+ printHelp();
1338
+ return;
1339
+ }
1340
+
1341
+ const normalizedCommand = command === "sync" ? "reload" : command;
1342
+ if (!["install", "update", "uninstall", "bypass", "resume", "set-api-key", "set-server", "set-websocket", "set-config", "reload"].includes(normalizedCommand)) {
1343
+ fail(`不支持的命令:${command}`);
1344
+ }
1345
+
1346
+ const resolvedStateDir = resolveOpenClawStateDir(options);
1347
+ const openclawConfigPath = resolveOpenClawConfigPath(options, resolvedStateDir);
1348
+ const existingConfig = readConfigFile(openclawConfigPath);
1349
+ const existingEntry = readPluginEntry(existingConfig, options.pluginId);
1350
+ const mergedOptions = {
1351
+ ...options,
1352
+ resolvedStateDir,
1353
+ openclawConfigPath,
1354
+ openclawCommandEnv: buildOpenClawCommandEnv(options, resolvedStateDir, openclawConfigPath),
1355
+ configPath: resolveManagedConfigPath(
1356
+ options.configPath || existingEntry?.config?.remoteGuard?.configPath,
1357
+ ),
1358
+ authConfigPath: resolveAuthConfigPath(options.pluginId, resolvedStateDir),
1359
+ pluginInstallPath: resolveManagedPluginInstallPath(options.pluginId, resolvedStateDir),
1360
+ };
1361
+
1362
+ console.log(`[claw-shieldctl] OpenClaw state dir: ${mergedOptions.resolvedStateDir}`);
1363
+ console.log(`[claw-shieldctl] OpenClaw config path: ${mergedOptions.openclawConfigPath}`);
1364
+ console.log(
1365
+ `[claw-shieldctl] OpenClaw extensions dir: ${path.join(mergedOptions.resolvedStateDir, "extensions")}`,
1366
+ );
1367
+
1368
+ // ── 前置检查:确认 openclaw 可用 ──────────────────────────────
1369
+ const resolvedOpenClawBin = resolveOpenClawBinOrFail(mergedOptions.openclawBin);
1370
+ if (resolvedOpenClawBin !== mergedOptions.openclawBin) {
1371
+ mergedOptions.openclawBin = resolvedOpenClawBin;
1372
+ // 重建 env,确保新 bin 路径的 node 环境在 PATH 中
1373
+ mergedOptions.openclawCommandEnv = buildOpenClawCommandEnv(
1374
+ { ...options, openclawBin: resolvedOpenClawBin },
1375
+ resolvedStateDir,
1376
+ openclawConfigPath,
1377
+ );
1378
+ console.log(`[claw-shieldctl] 已自动定位 OpenClaw: ${resolvedOpenClawBin}`);
1379
+ }
1380
+
1381
+ if (normalizedCommand === "install") {
1382
+ let installApiKey;
1383
+
1384
+ try {
1385
+ installApiKey = await resolveApiKey({
1386
+ apiKey: mergedOptions.apiKey,
1387
+ authConfigPath: mergedOptions.authConfigPath,
1388
+ allowStoredValue: true,
1389
+ });
1390
+ writeApiKey(mergedOptions.authConfigPath, installApiKey);
1391
+ console.log(
1392
+ `[claw-shieldctl] API Key 已写入 ${mergedOptions.authConfigPath} (${maskApiKey(installApiKey)})`,
1393
+ );
1394
+ warnIfEnvApiKeyDiffers(installApiKey);
1395
+
1396
+ try {
1397
+ installPluginWithFallback({
1398
+ openclawBin: mergedOptions.openclawBin,
1399
+ pluginId: mergedOptions.pluginId,
1400
+ pluginSpec: mergedOptions.pluginSpec,
1401
+ openclawCommandEnv: mergedOptions.openclawCommandEnv,
1402
+ });
1403
+ } finally {
1404
+ cleanupOpenClawInstallStageDirs(mergedOptions.resolvedStateDir);
1405
+ }
1406
+
1407
+ const installedConfig = readConfigFile(openclawConfigPath);
1408
+ const nextConfig = ensurePluginTrackedInstallRecord(installedConfig, mergedOptions);
1409
+ writeJsonFile(openclawConfigPath, nextConfig);
1410
+ console.log(`[claw-shieldctl] OpenClaw 配置已写入 ${openclawConfigPath}`);
1411
+
1412
+ // 配置 server 和 websocket 地址
1413
+ const serverAddr = await resolveServerAddress({
1414
+ serverAddress: mergedOptions.serverAddress,
1415
+ pluginInstallPath: mergedOptions.pluginInstallPath,
1416
+ allowStoredValue: true,
1417
+ });
1418
+ const wsAddr = await resolveWebSocketAddress({
1419
+ wsAddress: mergedOptions.wsAddress,
1420
+ pluginInstallPath: mergedOptions.pluginInstallPath,
1421
+ allowStoredValue: true,
1422
+ });
1423
+ writeServerAndWebSocketConfig(mergedOptions.pluginInstallPath, serverAddr, wsAddr);
1424
+
1425
+ if (mergedOptions.restart) {
1426
+ runOrFail(mergedOptions.openclawBin, ["gateway", "restart"], { env: mergedOptions.openclawCommandEnv });
1427
+ }
1428
+
1429
+ // 安装成功上报
1430
+ const serverBaseUrl = resolveServerBaseUrl(mergedOptions.pluginInstallPath);
1431
+ await reportInstallResult({
1432
+ serverBaseUrl,
1433
+ apiKey: installApiKey,
1434
+ action: "install",
1435
+ status: "success",
1436
+ });
1437
+ } catch (installError) {
1438
+ // 安装失败上报
1439
+ const serverBaseUrl = resolveServerBaseUrl(mergedOptions.pluginInstallPath);
1440
+ const reason = installError instanceof Error ? installError.message : String(installError);
1441
+ await reportInstallResult({
1442
+ serverBaseUrl,
1443
+ apiKey: installApiKey || readStoredApiKey(mergedOptions.authConfigPath),
1444
+ action: "install",
1445
+ status: "failed",
1446
+ reason,
1447
+ });
1448
+ throw installError;
1449
+ }
1450
+ } else if (normalizedCommand === "update") {
1451
+ console.log(
1452
+ `[claw-shieldctl] 将按“卸载旧版本 + 重装 ${mergedOptions.pluginSpec}”方式更新 ${mergedOptions.pluginId}。`,
1453
+ );
1454
+ uninstallPluginForReinstall(mergedOptions);
1455
+ removeManagedPluginInstallDir(mergedOptions.pluginId, mergedOptions.resolvedStateDir);
1456
+
1457
+ try {
1458
+ installPluginWithFallback({
1459
+ openclawBin: mergedOptions.openclawBin,
1460
+ pluginId: mergedOptions.pluginId,
1461
+ pluginSpec: mergedOptions.pluginSpec,
1462
+ openclawCommandEnv: mergedOptions.openclawCommandEnv,
1463
+ });
1464
+ } finally {
1465
+ cleanupOpenClawInstallStageDirs(mergedOptions.resolvedStateDir);
1466
+ }
1467
+
1468
+ const currentConfig = readConfigFile(openclawConfigPath);
1469
+ const nextConfig = ensurePluginTrackedInstallRecord(currentConfig, mergedOptions);
1470
+ writeJsonFile(openclawConfigPath, nextConfig);
1471
+ console.log(
1472
+ `[claw-shieldctl] 已为 ${mergedOptions.pluginId} 写入可追踪的安装记录(${mergedOptions.pluginSpec})。`,
1473
+ );
1474
+ console.log(`[claw-shieldctl] OpenClaw 配置已写入 ${openclawConfigPath}`);
1475
+
1476
+ const installedVersion = readInstalledPluginVersion(mergedOptions.pluginId, mergedOptions.resolvedStateDir);
1477
+ if (installedVersion) {
1478
+ console.log(`[claw-shieldctl] 当前已安装版本:${installedVersion}`);
1479
+ }
1480
+
1481
+ const existingApiKey = readStoredApiKey(mergedOptions.authConfigPath);
1482
+ if (!existingApiKey) {
1483
+ console.log(
1484
+ `[claw-shieldctl] 尚未检测到 API Key,请执行 claw-shieldctl set-api-key 完成本地配置。`,
1485
+ );
1486
+ } else {
1487
+ warnIfEnvApiKeyDiffers(existingApiKey);
1488
+ }
1489
+ if (mergedOptions.restart) {
1490
+ runOrFail(mergedOptions.openclawBin, ["gateway", "restart"], { env: mergedOptions.openclawCommandEnv });
1491
+ }
1492
+ } else if (normalizedCommand === "uninstall") {
1493
+ console.log(`[claw-shieldctl] 开始卸载插件 ${mergedOptions.pluginId}...`);
1494
+
1495
+ // 在卸载前先读取 server 地址和 API Key,因为卸载后可能读不到了
1496
+ const serverBaseUrl = resolveServerBaseUrl(mergedOptions.pluginInstallPath);
1497
+ const uninstallApiKey = readStoredApiKey(mergedOptions.authConfigPath);
1498
+
1499
+ try {
1500
+ // 1. 通过 OpenClaw CLI 卸载插件
1501
+ const uninstallResult = runCommand(
1502
+ mergedOptions.openclawBin,
1503
+ ["plugins", "uninstall", mergedOptions.pluginId, "--force"],
1504
+ { captureOutput: true, echoOutput: false, env: mergedOptions.openclawCommandEnv },
1505
+ );
1506
+ if (uninstallResult.error) {
1507
+ console.warn(
1508
+ `[claw-shieldctl] openclaw plugins uninstall 执行出错:${uninstallResult.error.message},将继续清理本地文件。`,
1509
+ );
1510
+ } else {
1511
+ echoCapturedOutput(uninstallResult);
1512
+ }
1513
+
1514
+ // 2. 移除插件安装目录
1515
+ removeManagedPluginInstallDir(mergedOptions.pluginId, mergedOptions.resolvedStateDir);
1516
+
1517
+ // 3. 从 openclaw.json 中移除插件配置
1518
+ removePluginFromConfig(mergedOptions.openclawConfigPath, mergedOptions.pluginId);
1519
+
1520
+ // 4. 可选:清理认证文件
1521
+ if (mergedOptions.purge) {
1522
+ removeAuthConfig(mergedOptions.authConfigPath);
1523
+ console.log(`[claw-shieldctl] --purge 已启用,认证文件已清理。`);
1524
+ } else {
1525
+ console.log(
1526
+ `[claw-shieldctl] 认证文件已保留(${mergedOptions.authConfigPath})。如需同时删除,请使用 --purge 参数。`,
1527
+ );
1528
+ }
1529
+
1530
+ // 5. 重启 gateway 使卸载生效
1531
+ if (mergedOptions.restart) {
1532
+ console.log(`[claw-shieldctl] 正在重启 OpenClaw gateway...`);
1533
+ runOrFail(mergedOptions.openclawBin, ["gateway", "restart"], { env: mergedOptions.openclawCommandEnv });
1534
+ }
1535
+
1536
+ console.log(`[claw-shieldctl] 插件 ${mergedOptions.pluginId} 已成功卸载。`);
1537
+
1538
+ // 卸载成功上报
1539
+ await reportInstallResult({
1540
+ serverBaseUrl,
1541
+ apiKey: uninstallApiKey,
1542
+ action: "uninstall",
1543
+ status: "success",
1544
+ });
1545
+ } catch (uninstallError) {
1546
+ // 卸载失败上报
1547
+ const reason = uninstallError instanceof Error ? uninstallError.message : String(uninstallError);
1548
+ await reportInstallResult({
1549
+ serverBaseUrl,
1550
+ apiKey: uninstallApiKey,
1551
+ action: "uninstall",
1552
+ status: "failed",
1553
+ reason,
1554
+ });
1555
+ throw uninstallError;
1556
+ }
1557
+ } else if (normalizedCommand === "bypass") {
1558
+ console.log(`[claw-shieldctl] 正在激活 bypass 模式...`);
1559
+ const callResult = runCommand(
1560
+ mergedOptions.openclawBin,
1561
+ ["gateway", "call", "shield.bypass", "--json"],
1562
+ { captureOutput: true, echoOutput: false, env: mergedOptions.openclawCommandEnv },
1563
+ );
1564
+ if (callResult.error) {
1565
+ fail(`执行 shield.bypass 失败:${callResult.error.message}`);
1566
+ }
1567
+ echoCapturedOutput(callResult);
1568
+ if ((callResult.status ?? 1) !== 0) {
1569
+ fail("shield.bypass 调用返回非 0,请检查 Gateway 是否正在运行。");
1570
+ }
1571
+ console.log(`[claw-shieldctl] Bypass 模式已激活:所有安全检测、遥测上报、Skills 上报均已暂停。`);
1572
+ console.log(`[claw-shieldctl] 如需恢复防护,请执行:npx -y tencent-claw-shield resume`);
1573
+ } else if (normalizedCommand === "resume") {
1574
+ console.log(`[claw-shieldctl] 正在恢复防护模式...`);
1575
+ const callResult = runCommand(
1576
+ mergedOptions.openclawBin,
1577
+ ["gateway", "call", "shield.resume", "--json"],
1578
+ { captureOutput: true, echoOutput: false, env: mergedOptions.openclawCommandEnv },
1579
+ );
1580
+ if (callResult.error) {
1581
+ fail(`执行 shield.resume 失败:${callResult.error.message}`);
1582
+ }
1583
+ echoCapturedOutput(callResult);
1584
+ if ((callResult.status ?? 1) !== 0) {
1585
+ fail("shield.resume 调用返回非 0,请检查 Gateway 是否正在运行。");
1586
+ }
1587
+ console.log(`[claw-shieldctl] 防护模式已恢复:所有安全检测、遥测上报已重新启用。`);
1588
+ } else if (normalizedCommand === "set-api-key") {
1589
+ const apiKey = await resolveApiKey({
1590
+ apiKey: mergedOptions.apiKey,
1591
+ authConfigPath: mergedOptions.authConfigPath,
1592
+ allowStoredValue: false,
1593
+ });
1594
+ writeApiKey(mergedOptions.authConfigPath, apiKey);
1595
+ console.log(
1596
+ `[claw-shieldctl] API Key 已写入 ${mergedOptions.authConfigPath} (${maskApiKey(apiKey)})`,
1597
+ );
1598
+ warnIfEnvApiKeyDiffers(apiKey);
1599
+ refreshOrRestart(mergedOptions);
1600
+ } else if (normalizedCommand === "set-server") {
1601
+ const serverAddr = await resolveServerAddress({
1602
+ serverAddress: mergedOptions.serverAddress,
1603
+ pluginInstallPath: mergedOptions.pluginInstallPath,
1604
+ allowStoredValue: false,
1605
+ });
1606
+ writeServerConfig(mergedOptions.pluginInstallPath, serverAddr);
1607
+ refreshOrRestart(mergedOptions);
1608
+ } else if (normalizedCommand === "set-websocket") {
1609
+ const wsAddr = await resolveWebSocketAddress({
1610
+ wsAddress: mergedOptions.wsAddress,
1611
+ pluginInstallPath: mergedOptions.pluginInstallPath,
1612
+ allowStoredValue: false,
1613
+ });
1614
+ writeWebSocketConfig(mergedOptions.pluginInstallPath, wsAddr);
1615
+ refreshOrRestart(mergedOptions);
1616
+ } else if (normalizedCommand === "set-config") {
1617
+ // 聚合命令:依次设置 API Key、Server 地址、WebSocket 地址,最后一次性热刷新
1618
+ console.log("[claw-shieldctl] 开始配置 API Key、Server 地址和 WebSocket 地址...\n");
1619
+
1620
+ // 1. API Key
1621
+ const apiKey = await resolveApiKey({
1622
+ apiKey: mergedOptions.apiKey,
1623
+ authConfigPath: mergedOptions.authConfigPath,
1624
+ allowStoredValue: false,
1625
+ });
1626
+ writeApiKey(mergedOptions.authConfigPath, apiKey);
1627
+ console.log(
1628
+ `[claw-shieldctl] API Key 已写入 ${mergedOptions.authConfigPath} (${maskApiKey(apiKey)})`,
1629
+ );
1630
+ warnIfEnvApiKeyDiffers(apiKey);
1631
+
1632
+ // 2. Server 地址
1633
+ const serverAddr = await resolveServerAddress({
1634
+ serverAddress: mergedOptions.serverAddress,
1635
+ pluginInstallPath: mergedOptions.pluginInstallPath,
1636
+ allowStoredValue: false,
1637
+ });
1638
+ writeServerConfig(mergedOptions.pluginInstallPath, serverAddr);
1639
+
1640
+ // 3. WebSocket 地址
1641
+ const wsAddr = await resolveWebSocketAddress({
1642
+ wsAddress: mergedOptions.wsAddress,
1643
+ pluginInstallPath: mergedOptions.pluginInstallPath,
1644
+ allowStoredValue: false,
1645
+ });
1646
+ writeWebSocketConfig(mergedOptions.pluginInstallPath, wsAddr);
1647
+
1648
+ // 一次性热刷新
1649
+ refreshOrRestart(mergedOptions);
1650
+ } else if (normalizedCommand === "reload") {
1651
+ refreshOrRestart(mergedOptions);
1652
+ }
1653
+
1654
+ console.log(`[claw-shieldctl] ${normalizedCommand} 完成。`);
1655
+ if (mergedOptions.configPath) {
1656
+ console.log(`[claw-shieldctl] 自定义配置路径:${mergedOptions.configPath}`);
1657
+ } else {
1658
+ console.log("[claw-shieldctl] 当前使用插件随包发布的静态基础配置。");
1659
+ }
1660
+ console.log(`[claw-shieldctl] API Key 配置路径:${mergedOptions.authConfigPath}`);
1661
+ }
1662
+
1663
+ await main();