@pwddd/skills-scanner 2.3.0 → 2.4.1
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.
Potentially problematic release.
This version of @pwddd/skills-scanner might be problematic. Click here for more details.
- package/README.md +65 -13
- package/index.ts +55 -325
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,13 +21,11 @@ OpenClaw Plugin,通过 HTTP API 调用远程 [cisco-ai-skill-scanner](https://
|
|
|
21
21
|
| 功能 | 实现方式 | 状态 |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| 启动时自动安装 Python 依赖(requests) | `registerService` | ✅ 全自动 |
|
|
24
|
-
| `/
|
|
25
|
-
| `/scan-report` 立即日报 | `registerCommand` | ✅ 全自动 |
|
|
26
|
-
| `/scan-status` 状态查看 | `registerCommand` | ✅ 全自动 |
|
|
24
|
+
| `/skills-scanner` 统一命令入口 | `registerCommand` | ✅ 全自动 |
|
|
27
25
|
| `openclaw skills-scan` CLI | `registerCli` | ✅ 全自动 |
|
|
28
26
|
| 安装前扫描(新 Skill 出现时自动扫描) | `fs.watch` | ✅ 全自动 |
|
|
29
27
|
| Gateway 启动时在日志里打印配置提示 | Plugin Hook `gateway:startup` | ✅ 全自动 |
|
|
30
|
-
| 每日定期日报 | Cron Job |
|
|
28
|
+
| 每日定期日报 | Cron Job | ✅ 智能自动注册 |
|
|
31
29
|
|
|
32
30
|
---
|
|
33
31
|
|
|
@@ -86,7 +84,7 @@ openclaw gateway restart
|
|
|
86
84
|
- ✅ **自动更新**:配置变更时自动更新任务
|
|
87
85
|
- ✅ **防止冲突**:即使手动创建过任务也能正确识别
|
|
88
86
|
|
|
89
|
-
如果自动注册失败,可以使用 `/
|
|
87
|
+
如果自动注册失败,可以使用 `/skills-scanner cron register` 命令手动注册。
|
|
90
88
|
|
|
91
89
|
---
|
|
92
90
|
|
|
@@ -102,9 +100,9 @@ openclaw gateway restart
|
|
|
102
100
|
|
|
103
101
|
```bash
|
|
104
102
|
# 使用聊天命令(推荐)
|
|
105
|
-
/
|
|
106
|
-
/
|
|
107
|
-
/
|
|
103
|
+
/skills-scanner cron register # 注册定时任务
|
|
104
|
+
/skills-scanner cron unregister # 删除定时任务
|
|
105
|
+
/skills-scanner cron status # 查看状态
|
|
108
106
|
|
|
109
107
|
# 或使用 CLI 命令
|
|
110
108
|
openclaw cron add \
|
|
@@ -112,7 +110,7 @@ openclaw cron add \
|
|
|
112
110
|
--cron "0 8 * * *" \
|
|
113
111
|
--tz "Asia/Shanghai" \
|
|
114
112
|
--session isolated \
|
|
115
|
-
--message "请执行 /scan
|
|
113
|
+
--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
|
|
116
114
|
--announce
|
|
117
115
|
```
|
|
118
116
|
|
|
@@ -184,6 +182,60 @@ openclaw cron list
|
|
|
184
182
|
|
|
185
183
|
---
|
|
186
184
|
|
|
185
|
+
## CLI 命令
|
|
186
|
+
|
|
187
|
+
### 可用命令
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
openclaw skills-scan <子命令> [参数]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 子命令
|
|
194
|
+
|
|
195
|
+
| 命令 | 说明 |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `scan <路径>` | 扫描单个 Skill |
|
|
198
|
+
| `batch <目录>` | 批量扫描目录 |
|
|
199
|
+
| `report` | 生成完整日报 |
|
|
200
|
+
| `health` | 检查 API 服务状态 |
|
|
201
|
+
|
|
202
|
+
### 使用示例
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# 扫描单个 Skill
|
|
206
|
+
openclaw skills-scan scan ~/.openclaw/skills/my-skill
|
|
207
|
+
|
|
208
|
+
# 批量扫描
|
|
209
|
+
openclaw skills-scan batch ~/.openclaw/skills --recursive
|
|
210
|
+
|
|
211
|
+
# 生成日报
|
|
212
|
+
openclaw skills-scan report
|
|
213
|
+
|
|
214
|
+
# 健康检查
|
|
215
|
+
openclaw skills-scan health
|
|
216
|
+
|
|
217
|
+
# 详细扫描
|
|
218
|
+
openclaw skills-scan scan ~/my-skill --detailed --behavioral
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**注意**:CLI 命令是 `skills-scan`(不是 `skills-scanner`)
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 命令对比
|
|
226
|
+
|
|
227
|
+
| 功能 | 聊天命令 | CLI 命令 |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| 扫描单个 Skill | `/skills-scanner scan <路径>` | `openclaw skills-scan scan <路径>` |
|
|
230
|
+
| 批量扫描 | `/skills-scanner scan <目录> --recursive` | `openclaw skills-scan batch <目录> --recursive` |
|
|
231
|
+
| 生成日报 | `/skills-scanner scan --report` | `openclaw skills-scan report` |
|
|
232
|
+
| 查看状态 | `/skills-scanner status` | - |
|
|
233
|
+
| 健康检查 | `/skills-scanner status`(包含) | `openclaw skills-scan health` |
|
|
234
|
+
| 配置管理 | `/skills-scanner config` | - |
|
|
235
|
+
| 定时任务 | `/skills-scanner cron` | - |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
187
239
|
## 定时任务说明
|
|
188
240
|
|
|
189
241
|
插件会在启动时**自动智能注册**定时任务,无需手动操作:
|
|
@@ -200,7 +252,7 @@ openclaw cron list
|
|
|
200
252
|
- **任务名称**:`skills-daily-report`
|
|
201
253
|
- **执行时间**:每天 08:00
|
|
202
254
|
- **时区**:Asia/Shanghai
|
|
203
|
-
- **执行内容**:发送 `/scan
|
|
255
|
+
- **执行内容**:发送 `/skills-scanner scan --report` 命令到指定渠道
|
|
204
256
|
|
|
205
257
|
### 手动管理
|
|
206
258
|
|
|
@@ -216,7 +268,7 @@ openclaw cron add \
|
|
|
216
268
|
--cron "0 8 * * *" \
|
|
217
269
|
--tz "Asia/Shanghai" \
|
|
218
270
|
--session isolated \
|
|
219
|
-
--message "请执行 /scan
|
|
271
|
+
--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
|
|
220
272
|
--announce
|
|
221
273
|
|
|
222
274
|
# 删除定时任务
|
|
@@ -237,7 +289,7 @@ openclaw cron add \
|
|
|
237
289
|
--cron "0 8 * * *" \
|
|
238
290
|
--tz "Asia/Shanghai" \
|
|
239
291
|
--session isolated \
|
|
240
|
-
--message "请执行 /scan
|
|
292
|
+
--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
|
|
241
293
|
--announce \
|
|
242
294
|
--channel telegram \
|
|
243
295
|
--to "+8613312345678"
|
|
@@ -249,7 +301,7 @@ openclaw cron add \
|
|
|
249
301
|
|
|
250
302
|
Plugin 启动后用 `fs.watch` 监听所有 Skills 目录。任何新 Skill 出现(无论通过 `clawhub install`、CLI 还是手动复制)都会触发扫描。
|
|
251
303
|
|
|
252
|
-
扫描结果通过 `persistWatcherAlert` 写入 `~/.openclaw/skills-scanner/state.json`,运行 `/
|
|
304
|
+
扫描结果通过 `persistWatcherAlert` 写入 `~/.openclaw/skills-scanner/state.json`,运行 `/skills-scanner status` 查看并清空告警列表。
|
|
253
305
|
|
|
254
306
|
> **为什么不直接发聊天消息?**
|
|
255
307
|
> OpenClaw Plugin API 没有提供在后台任务里主动推送消息给用户的方法。`event.messages.push()` 只在 Hook handler 的同步上下文中有效,`registerCommand` 的 handler 需要用户主动触发。这是平台限制,不是实现缺陷。
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 已确认可用的功能(全部有文档依据):
|
|
5
5
|
* 1. registerService — 启动时自动安装 Python 依赖(uv venv)
|
|
6
|
-
* 2. registerCommand — 聊天命令 /
|
|
6
|
+
* 2. registerCommand — 聊天命令 /skills-scanner (统一命名空间)
|
|
7
7
|
* 3. registerGatewayMethod — RPC 供 Control UI 调用
|
|
8
8
|
* 4. registerCli — CLI 命令 openclaw skills-scan
|
|
9
9
|
* 5. registerPluginHooksFromDir — 捆绑 gateway:startup hook(日报提醒)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - api.runtime.gateway.call("cron.add") ← 文档无记录
|
|
17
17
|
* - message:preprocessed hook 拦截 ← 该事件不存在
|
|
18
18
|
*
|
|
19
|
-
* Cron
|
|
19
|
+
* Cron 日报:插件启动时智能自动注册(幂等操作,防止重复)。
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { execSync, exec } from "child_process";
|
|
@@ -247,9 +247,9 @@ function generateConfigGuide(cfg: ScannerConfig, apiUrl: string, scanDirs: strin
|
|
|
247
247
|
" openclaw gateway restart",
|
|
248
248
|
"",
|
|
249
249
|
"3. 验证配置:",
|
|
250
|
-
" /
|
|
250
|
+
" /skills-scanner status",
|
|
251
251
|
"",
|
|
252
|
-
"💬 或者在聊天中执行 /
|
|
252
|
+
"💬 或者在聊天中执行 /skills-scanner status 查看当前状态",
|
|
253
253
|
"",
|
|
254
254
|
"提示:此消息只在首次运行时显示。",
|
|
255
255
|
"════════════════════════════════════════════════════════════════",
|
|
@@ -440,7 +440,7 @@ async function ensureCronJob(logger: any): Promise<void> {
|
|
|
440
440
|
`--cron "${CRON_SCHEDULE}"`,
|
|
441
441
|
`--tz "${CRON_TIMEZONE}"`,
|
|
442
442
|
'--session isolated',
|
|
443
|
-
'--message "请执行 /scan
|
|
443
|
+
'--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道"',
|
|
444
444
|
'--announce'
|
|
445
445
|
].join(' ');
|
|
446
446
|
|
|
@@ -481,7 +481,7 @@ async function ensureCronJob(logger: any): Promise<void> {
|
|
|
481
481
|
logger.info(`[skills-scanner] --cron "${CRON_SCHEDULE}" \\`);
|
|
482
482
|
logger.info(`[skills-scanner] --tz "${CRON_TIMEZONE}" \\`);
|
|
483
483
|
logger.info('[skills-scanner] --session isolated \\');
|
|
484
|
-
logger.info('[skills-scanner] --message "请执行 /scan
|
|
484
|
+
logger.info('[skills-scanner] --message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \\');
|
|
485
485
|
logger.info('[skills-scanner] --announce');
|
|
486
486
|
logger.info("[skills-scanner]");
|
|
487
487
|
}
|
|
@@ -553,7 +553,7 @@ async function buildDailyReport(
|
|
|
553
553
|
const r = allResults.find(x => (x.name || basename(x.path ?? "")) === name);
|
|
554
554
|
lines.push(` • ${name} [${r?.max_severity ?? "?"}] — ${r?.findings ?? "?"} 条发现`);
|
|
555
555
|
}
|
|
556
|
-
lines.push("", "💡 运行 `/
|
|
556
|
+
lines.push("", "💡 运行 `/skills-scanner scan <路径> --detailed` 查看详情");
|
|
557
557
|
} else {
|
|
558
558
|
lines.push("", "🎉 所有 Skills 安全,未发现威胁。");
|
|
559
559
|
}
|
|
@@ -661,7 +661,7 @@ function startWatcher(
|
|
|
661
661
|
|
|
662
662
|
// ── Plugin 入口 ───────────────────────────────────────────────────────────────
|
|
663
663
|
|
|
664
|
-
export default
|
|
664
|
+
export default function register(api: any) {
|
|
665
665
|
const cfg: ScannerConfig = api.config?.plugins?.entries?.["skills-scanner"]?.config ?? {};
|
|
666
666
|
const apiUrl = cfg.apiUrl ?? "http://localhost:8000";
|
|
667
667
|
const scanDirs = (cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
|
|
@@ -733,7 +733,7 @@ export default async function register(api: any) {
|
|
|
733
733
|
|
|
734
734
|
if (!depsReady) {
|
|
735
735
|
api.logger.error("[skills-scanner] ❌ 依赖安装失败,服务无法启动");
|
|
736
|
-
api.logger.error("[skills-scanner] 请手动运行: uv pip install --python \"" + VENV_PYTHON + "\"
|
|
736
|
+
api.logger.error("[skills-scanner] 请手动运行: uv pip install --python \"" + VENV_PYTHON + "\" requests>=2.31.0");
|
|
737
737
|
return;
|
|
738
738
|
}
|
|
739
739
|
|
|
@@ -745,6 +745,15 @@ export default async function register(api: any) {
|
|
|
745
745
|
api.logger.info("[skills-scanner] ⏭️ 安装前扫描已禁用");
|
|
746
746
|
}
|
|
747
747
|
|
|
748
|
+
// 智能注册定时任务(仅在 Gateway 模式下,CLI 模式跳过)
|
|
749
|
+
// 检测方式:Gateway 模式下 api.runtime 存在,CLI 模式下不存在
|
|
750
|
+
const isGatewayMode = !!(api as any).runtime;
|
|
751
|
+
if (isGatewayMode) {
|
|
752
|
+
api.logger.info("[skills-scanner] ─────────────────────────────────────");
|
|
753
|
+
api.logger.info("[skills-scanner] 🕐 检查定时任务...");
|
|
754
|
+
await ensureCronJob(api.logger);
|
|
755
|
+
}
|
|
756
|
+
|
|
748
757
|
api.logger.info("[skills-scanner] ─────────────────────────────────────");
|
|
749
758
|
},
|
|
750
759
|
stop: () => {
|
|
@@ -754,11 +763,7 @@ export default async function register(api: any) {
|
|
|
754
763
|
},
|
|
755
764
|
});
|
|
756
765
|
|
|
757
|
-
// ── 2.
|
|
758
|
-
await ensureCronJob(api.logger);
|
|
759
|
-
api.logger.info("[skills-scanner] ═══════════════════════════════════════");
|
|
760
|
-
|
|
761
|
-
// ── 3. /skills-scanner 主命令(命名空间)─────────────────────────────────
|
|
766
|
+
// ── 2. /skills-scanner 主命令(命名空间)─────────────────────────────────
|
|
762
767
|
api.registerCommand({
|
|
763
768
|
name: "skills-scanner",
|
|
764
769
|
description: "Skills 安全扫描工具。用法: /skills-scanner <子命令> [参数]",
|
|
@@ -1038,317 +1043,7 @@ export default async function register(api: any) {
|
|
|
1038
1043
|
"• `/skills-scanner config [show|reset]` - 配置管理",
|
|
1039
1044
|
"• `/skills-scanner cron [register|unregister|status]` - 定时任务",
|
|
1040
1045
|
].join("\n");
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// ── 3. /scan-skill ────────────────────────────────────────────────────────
|
|
1044
|
-
api.registerCommand({
|
|
1045
|
-
name: "scan-skill",
|
|
1046
|
-
description: "扫描单个 Skill 目录。用法: /scan-skill <路径> [--detailed] [--behavioral]",
|
|
1047
|
-
acceptsArgs: true,
|
|
1048
|
-
requireAuth: true,
|
|
1049
|
-
handler: async (ctx: any) => {
|
|
1050
|
-
const raw = (ctx.args ?? "").trim();
|
|
1051
|
-
if (!raw) return { text: "用法:`/scan-skill <路径> [--detailed] [--behavioral]`" };
|
|
1052
|
-
|
|
1053
|
-
// 先检查依赖
|
|
1054
|
-
if (!isVenvReady()) {
|
|
1055
|
-
return { text: "⏳ Python 依赖尚未就绪,请稍后重试或查看日志" };
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
const parts = raw.split(/\s+/);
|
|
1059
|
-
const skillPath = expandPath(parts.find(p => !p.startsWith("--")) ?? "");
|
|
1060
|
-
const detailed = parts.includes("--detailed");
|
|
1061
|
-
const useBehav = parts.includes("--behavioral") || behavioral;
|
|
1062
|
-
|
|
1063
|
-
const res = await runScan("scan", skillPath, {
|
|
1064
|
-
detailed,
|
|
1065
|
-
behavioral: useBehav,
|
|
1066
|
-
apiUrl,
|
|
1067
|
-
useLLM,
|
|
1068
|
-
policy
|
|
1069
|
-
});
|
|
1070
|
-
const icon = res.exitCode === 0 ? "✅" : "❌";
|
|
1071
|
-
return { text: `${icon} 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
|
|
1072
|
-
},
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
// ── 4. /scan-skills ───────────────────────────────────────────────────────
|
|
1076
|
-
api.registerCommand({
|
|
1077
|
-
name: "scan-skills",
|
|
1078
|
-
description: "批量扫描目录下所有 Skills。用法: /scan-skills <目录> [--recursive] [--detailed]",
|
|
1079
|
-
acceptsArgs: true,
|
|
1080
|
-
requireAuth: true,
|
|
1081
|
-
handler: async (ctx: any) => {
|
|
1082
|
-
const raw = (ctx.args ?? "").trim();
|
|
1083
|
-
if (!raw) return { text: "用法:`/scan-skills <目录> [--recursive] [--detailed]`" };
|
|
1084
|
-
if (!isVenvReady()) return { text: "⏳ Python 依赖尚未就绪,请稍后重试" };
|
|
1085
|
-
|
|
1086
|
-
const parts = raw.split(/\s+/);
|
|
1087
|
-
const dirPath = expandPath(parts.find(p => !p.startsWith("--")) ?? "");
|
|
1088
|
-
const recursive = parts.includes("--recursive");
|
|
1089
|
-
const detailed = parts.includes("--detailed");
|
|
1090
|
-
const useBehav = parts.includes("--behavioral") || behavioral;
|
|
1091
|
-
|
|
1092
|
-
const res = await runScan("batch", dirPath, {
|
|
1093
|
-
recursive,
|
|
1094
|
-
detailed,
|
|
1095
|
-
behavioral: useBehav,
|
|
1096
|
-
apiUrl,
|
|
1097
|
-
useLLM,
|
|
1098
|
-
policy
|
|
1099
|
-
});
|
|
1100
|
-
const icon = res.exitCode === 0 ? "✅" : "❌";
|
|
1101
|
-
return { text: `${icon} 批量扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
|
|
1102
|
-
},
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
// ── 5. /scan-report ───────────────────────────────────────────────────────
|
|
1106
|
-
api.registerCommand({
|
|
1107
|
-
name: "scan-report",
|
|
1108
|
-
description: "立即执行全量扫描并生成安全日报",
|
|
1109
|
-
acceptsArgs: false,
|
|
1110
|
-
requireAuth: true,
|
|
1111
|
-
handler: async (_ctx: any) => {
|
|
1112
|
-
if (!isVenvReady()) return { text: "⏳ Python 依赖尚未就绪,请稍后重试" };
|
|
1113
|
-
if (scanDirs.length === 0) return { text: "⚠️ 未找到可扫描目录,请检查配置" };
|
|
1114
|
-
const report = await buildDailyReport(scanDirs, behavioral, apiUrl, useLLM, policy, api.logger);
|
|
1115
|
-
return { text: report };
|
|
1116
|
-
},
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
// ── 6. /scan-status ───────────────────────────────────────────────────────
|
|
1120
|
-
api.registerCommand({
|
|
1121
|
-
name: "scan-status",
|
|
1122
|
-
description: "查看 Skills Scanner 状态和待查告警",
|
|
1123
|
-
acceptsArgs: false,
|
|
1124
|
-
requireAuth: true,
|
|
1125
|
-
handler: async (_ctx: any) => {
|
|
1126
|
-
const state = loadState() as any;
|
|
1127
|
-
const alerts: string[] = state.pendingAlerts ?? [];
|
|
1128
|
-
|
|
1129
|
-
const lines = [
|
|
1130
|
-
"📋 *Skills Scanner 状态*",
|
|
1131
|
-
`API 服务地址:${apiUrl}`,
|
|
1132
|
-
`Python 依赖:${isVenvReady() ? "✅ 就绪" : "❌ 未就绪"}`,
|
|
1133
|
-
`Python 路径:${VENV_PYTHON}`,
|
|
1134
|
-
`scan.py 路径:${SCAN_SCRIPT}`,
|
|
1135
|
-
`安装前扫描:${preInstallScan === "on" ? `✅ 监听中(${onUnsafe})` : "❌ 已禁用"}`,
|
|
1136
|
-
`扫描策略:${policy}`,
|
|
1137
|
-
`LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
|
|
1138
|
-
`行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
|
|
1139
|
-
`上次扫描:${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
|
|
1140
|
-
`上次问题 Skills:${state.lastUnsafeSkills?.length ? state.lastUnsafeSkills.join(", ") : "无"}`,
|
|
1141
|
-
`扫描目录:\n${scanDirs.map(d => ` • ${d}`).join("\n")}`,
|
|
1142
|
-
];
|
|
1143
|
-
|
|
1144
|
-
// API 健康检查
|
|
1145
|
-
if (isVenvReady()) {
|
|
1146
|
-
lines.push("", "🔍 *API 服务检查*");
|
|
1147
|
-
try {
|
|
1148
|
-
// 调用 scan.py health 命令
|
|
1149
|
-
const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
|
|
1150
|
-
|
|
1151
|
-
// 清除代理环境变量
|
|
1152
|
-
const env = { ...process.env };
|
|
1153
|
-
delete env.http_proxy;
|
|
1154
|
-
delete env.https_proxy;
|
|
1155
|
-
delete env.HTTP_PROXY;
|
|
1156
|
-
delete env.HTTPS_PROXY;
|
|
1157
|
-
delete env.all_proxy;
|
|
1158
|
-
delete env.ALL_PROXY;
|
|
1159
|
-
|
|
1160
|
-
const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
|
|
1161
|
-
const output = (stdout + stderr).trim();
|
|
1162
|
-
|
|
1163
|
-
if (output.includes("✓") || output.includes("正常")) {
|
|
1164
|
-
lines.push(`API 服务:✅ 正常`);
|
|
1165
|
-
|
|
1166
|
-
// 尝试解析可用的分析器信息
|
|
1167
|
-
if (output.includes("analyzers_available")) {
|
|
1168
|
-
try {
|
|
1169
|
-
// 从输出中提取 JSON(如果有的话)
|
|
1170
|
-
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
1171
|
-
if (jsonMatch) {
|
|
1172
|
-
const healthData = JSON.parse(jsonMatch[0]);
|
|
1173
|
-
if (healthData.analyzers_available) {
|
|
1174
|
-
lines.push(`可用分析器:${healthData.analyzers_available.join(", ")}`);
|
|
1175
|
-
}
|
|
1176
|
-
if (healthData.version) {
|
|
1177
|
-
lines.push(`API 版本:${healthData.version}`);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
} catch {}
|
|
1181
|
-
}
|
|
1182
|
-
} else {
|
|
1183
|
-
lines.push(`API 服务:❌ 不可用`);
|
|
1184
|
-
lines.push(`响应:${output}`);
|
|
1185
|
-
}
|
|
1186
|
-
} catch (err: any) {
|
|
1187
|
-
lines.push(`API 服务:❌ 连接失败`);
|
|
1188
|
-
const errorMsg = err.message || err.toString();
|
|
1189
|
-
if (errorMsg.includes("ECONNREFUSED") || errorMsg.includes("无法连接")) {
|
|
1190
|
-
lines.push(`错误:无法连接到 ${apiUrl}`);
|
|
1191
|
-
} else {
|
|
1192
|
-
lines.push(`错误:${errorMsg}`);
|
|
1193
|
-
}
|
|
1194
|
-
lines.push("", "💡 请确保 skill-scanner-api 服务正在运行:");
|
|
1195
|
-
lines.push("```");
|
|
1196
|
-
lines.push("skill-scanner-api");
|
|
1197
|
-
lines.push("# 或指定端口");
|
|
1198
|
-
lines.push("skill-scanner-api --port 8080");
|
|
1199
|
-
lines.push("```");
|
|
1200
|
-
}
|
|
1201
|
-
} else {
|
|
1202
|
-
lines.push("", "⚠️ Python 依赖未就绪,无法检查 API 服务");
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
if (alerts.length > 0) {
|
|
1206
|
-
lines.push("", `🔔 *待查告警(${alerts.length} 条):*`);
|
|
1207
|
-
alerts.slice(-5).forEach(a => lines.push(` ${a}`));
|
|
1208
|
-
// 读取后清空
|
|
1209
|
-
saveState({ ...state, pendingAlerts: [] });
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// 定时任务状态
|
|
1213
|
-
lines.push("", "🕐 *定时任务状态*");
|
|
1214
|
-
if (state.cronJobId && state.cronJobId !== 'manual-created' && state.cronJobId !== 'created-unknown-id') {
|
|
1215
|
-
lines.push(`任务 ID:${state.cronJobId}`);
|
|
1216
|
-
lines.push(`任务名称:${CRON_JOB_NAME}`);
|
|
1217
|
-
lines.push(`执行时间:每天 ${CRON_SCHEDULE.split(' ')[1]}:${CRON_SCHEDULE.split(' ')[0]} (${CRON_TIMEZONE})`);
|
|
1218
|
-
lines.push("状态:✅ 已注册");
|
|
1219
|
-
lines.push("", "💡 查看所有定时任务:`openclaw cron list`");
|
|
1220
|
-
lines.push("💡 删除定时任务:`openclaw cron remove " + state.cronJobId + "`");
|
|
1221
|
-
} else if (state.cronJobId === 'manual-created') {
|
|
1222
|
-
lines.push("状态:✅ 已手动创建(无法获取 ID)");
|
|
1223
|
-
lines.push("", "💡 查看所有定时任务:`openclaw cron list`");
|
|
1224
|
-
} else {
|
|
1225
|
-
lines.push("状态:❌ 未注册");
|
|
1226
|
-
lines.push("", "💡 手动注册定时任务:");
|
|
1227
|
-
lines.push("```");
|
|
1228
|
-
lines.push("openclaw cron add \\");
|
|
1229
|
-
lines.push(` --name "${CRON_JOB_NAME}" \\`);
|
|
1230
|
-
lines.push(` --cron "${CRON_SCHEDULE}" \\`);
|
|
1231
|
-
lines.push(` --tz "${CRON_TIMEZONE}" \\`);
|
|
1232
|
-
lines.push(" --session isolated \\");
|
|
1233
|
-
lines.push(' --message "请执行 /scan-report 并把结果发送到此渠道" \\');
|
|
1234
|
-
lines.push(" --announce");
|
|
1235
|
-
lines.push("```");
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
return { text: lines.join("\n") };
|
|
1239
|
-
},
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
|
-
// ── 7. /scan-cron ─────────────────────────────────────────────────────────
|
|
1243
|
-
api.registerCommand({
|
|
1244
|
-
name: "scan-cron",
|
|
1245
|
-
description: "管理定时任务。用法: /scan-cron [register|unregister|status]",
|
|
1246
|
-
acceptsArgs: true,
|
|
1247
|
-
requireAuth: true,
|
|
1248
|
-
handler: async (ctx: any) => {
|
|
1249
|
-
const action = (ctx.args ?? "").trim().toLowerCase() || "status";
|
|
1250
|
-
const state = loadState() as any;
|
|
1251
|
-
|
|
1252
|
-
if (action === "register" || action === "add") {
|
|
1253
|
-
// 强制重新注册
|
|
1254
|
-
api.logger.info("[skills-scanner] 用户请求重新注册定时任务");
|
|
1255
|
-
|
|
1256
|
-
// 清除旧的 cronJobId,强制重新创建
|
|
1257
|
-
const oldJobId = state.cronJobId;
|
|
1258
|
-
if (oldJobId && oldJobId !== 'manual-created' && oldJobId !== 'created-unknown-id') {
|
|
1259
|
-
try {
|
|
1260
|
-
execSync(`openclaw cron remove ${oldJobId}`, { encoding: 'utf-8', timeout: 5000 });
|
|
1261
|
-
api.logger.info(`[skills-scanner] 已删除旧任务: ${oldJobId}`);
|
|
1262
|
-
} catch (err: any) {
|
|
1263
|
-
api.logger.warn(`[skills-scanner] 删除旧任务失败: ${err.message}`);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
saveState({ ...state, cronJobId: undefined });
|
|
1268
|
-
await ensureCronJob(api.logger);
|
|
1269
|
-
|
|
1270
|
-
const newState = loadState() as any;
|
|
1271
|
-
if (newState.cronJobId) {
|
|
1272
|
-
return { text: `✅ 定时任务注册成功\n任务 ID: ${newState.cronJobId}\n执行时间: 每天 ${CRON_SCHEDULE.split(' ')[1]}:${CRON_SCHEDULE.split(' ')[0]} (${CRON_TIMEZONE})` };
|
|
1273
|
-
} else {
|
|
1274
|
-
return { text: "❌ 定时任务注册失败,请查看日志或手动注册" };
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
} else if (action === "unregister" || action === "remove" || action === "delete") {
|
|
1278
|
-
// 删除定时任务
|
|
1279
|
-
if (!state.cronJobId) {
|
|
1280
|
-
return { text: "⚠️ 未找到已注册的定时任务" };
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
if (state.cronJobId === 'manual-created' || state.cronJobId === 'created-unknown-id') {
|
|
1284
|
-
return { text: "⚠️ 无法自动删除手动创建的任务,请使用:\n`openclaw cron list` 查看任务\n`openclaw cron remove <job-id>` 删除任务" };
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
try {
|
|
1288
|
-
execSync(`openclaw cron remove ${state.cronJobId}`, { encoding: 'utf-8', timeout: 5000 });
|
|
1289
|
-
saveState({ ...state, cronJobId: undefined });
|
|
1290
|
-
return { text: `✅ 定时任务已删除: ${state.cronJobId}` };
|
|
1291
|
-
} catch (err: any) {
|
|
1292
|
-
return { text: `❌ 删除失败: ${err.message}\n请手动执行: \`openclaw cron remove ${state.cronJobId}\`` };
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
} else if (action === "status" || action === "info") {
|
|
1296
|
-
// 显示状态
|
|
1297
|
-
const lines = ["🕐 *定时任务状态*"];
|
|
1298
|
-
|
|
1299
|
-
if (state.cronJobId && state.cronJobId !== 'manual-created' && state.cronJobId !== 'created-unknown-id') {
|
|
1300
|
-
lines.push(`任务 ID: ${state.cronJobId}`);
|
|
1301
|
-
lines.push(`任务名称: ${CRON_JOB_NAME}`);
|
|
1302
|
-
lines.push(`执行时间: 每天 ${CRON_SCHEDULE.split(' ')[1]}:${CRON_SCHEDULE.split(' ')[0]} (${CRON_TIMEZONE})`);
|
|
1303
|
-
lines.push("状态: ✅ 已注册");
|
|
1304
|
-
lines.push("");
|
|
1305
|
-
lines.push("可用操作:");
|
|
1306
|
-
lines.push("• `/scan-cron unregister` - 删除定时任务");
|
|
1307
|
-
lines.push("• `/scan-cron register` - 重新注册定时任务");
|
|
1308
|
-
lines.push("• `openclaw cron list` - 查看所有定时任务");
|
|
1309
|
-
} else if (state.cronJobId === 'manual-created') {
|
|
1310
|
-
lines.push("状态: ✅ 已手动创建(无法获取 ID)");
|
|
1311
|
-
lines.push("");
|
|
1312
|
-
lines.push("💡 使用 `openclaw cron list` 查看所有定时任务");
|
|
1313
|
-
} else {
|
|
1314
|
-
lines.push("状态: ❌ 未注册");
|
|
1315
|
-
lines.push("");
|
|
1316
|
-
lines.push("💡 使用 `/scan-cron register` 注册定时任务");
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
return { text: lines.join("\n") };
|
|
1320
|
-
|
|
1321
|
-
} else {
|
|
1322
|
-
return { text: "用法: `/scan-cron [register|unregister|status]`\n\n• `register` - 注册定时任务\n• `unregister` - 删除定时任务\n• `status` - 查看状态(默认)" };
|
|
1323
|
-
}
|
|
1324
|
-
},
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1327
|
-
// ── 8. /scan-config ───────────────────────────────────────────────────────
|
|
1328
|
-
api.registerCommand({
|
|
1329
|
-
name: "scan-config",
|
|
1330
|
-
description: "显示配置向导和当前配置",
|
|
1331
|
-
acceptsArgs: true,
|
|
1332
|
-
requireAuth: true,
|
|
1333
|
-
handler: async (ctx: any) => {
|
|
1334
|
-
const action = (ctx.args ?? "").trim().toLowerCase() || "show";
|
|
1335
|
-
|
|
1336
|
-
if (action === "show" || action === "guide" || action === "help") {
|
|
1337
|
-
// 显示配置向导
|
|
1338
|
-
const configGuide = generateConfigGuide(cfg, apiUrl, scanDirs, behavioral, useLLM, policy, preInstallScan, onUnsafe);
|
|
1339
|
-
return { text: "```\n" + configGuide + "\n```" };
|
|
1340
|
-
} else if (action === "reset") {
|
|
1341
|
-
// 重置配置审查标记,下次启动会再次显示向导
|
|
1342
|
-
const state = loadState() as any;
|
|
1343
|
-
saveState({ ...state, configReviewed: false });
|
|
1344
|
-
return { text: "✅ 配置审查标记已重置\n下次重启 Gateway 时将再次显示配置向导" };
|
|
1345
|
-
} else {
|
|
1346
|
-
return { text: "用法: `/scan-config [show|reset]`\n\n• `show` - 显示配置向导(默认)\n• `reset` - 重置首次运行标记" };
|
|
1347
|
-
}
|
|
1348
|
-
},
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
// ── 9. Gateway RPC ────────────────────────────────────────────────────────
|
|
1046
|
+
} // ── 9. Gateway RPC ────────────────────────────────────────────────────────
|
|
1352
1047
|
api.registerGatewayMethod("skillsScanner.scan", async ({ respond, params }: any) => {
|
|
1353
1048
|
const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
|
|
1354
1049
|
if (!p) return respond(false, { error: "缺少 path 参数" });
|
|
@@ -1412,6 +1107,41 @@ export default async function register(api: any) {
|
|
|
1412
1107
|
const report = await buildDailyReport(scanDirs, behavioral, apiUrl, useLLM, policy, console);
|
|
1413
1108
|
console.log(report);
|
|
1414
1109
|
});
|
|
1110
|
+
|
|
1111
|
+
cmd.command("health")
|
|
1112
|
+
.description("检查 API 服务健康状态")
|
|
1113
|
+
.action(async () => {
|
|
1114
|
+
if (!isVenvReady()) {
|
|
1115
|
+
console.error("❌ Python 依赖未就绪");
|
|
1116
|
+
process.exit(1);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
|
|
1121
|
+
const env = { ...process.env };
|
|
1122
|
+
delete env.http_proxy;
|
|
1123
|
+
delete env.https_proxy;
|
|
1124
|
+
delete env.HTTP_PROXY;
|
|
1125
|
+
delete env.HTTPS_PROXY;
|
|
1126
|
+
delete env.all_proxy;
|
|
1127
|
+
delete env.ALL_PROXY;
|
|
1128
|
+
|
|
1129
|
+
const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
|
|
1130
|
+
const output = (stdout + stderr).trim();
|
|
1131
|
+
console.log(output);
|
|
1132
|
+
|
|
1133
|
+
if (output.includes("✓") || output.includes("正常")) {
|
|
1134
|
+
process.exit(0);
|
|
1135
|
+
} else {
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
} catch (err: any) {
|
|
1139
|
+
console.error(`❌ 连接失败: ${err.message}`);
|
|
1140
|
+
console.error(`\n💡 请确保 skill-scanner-api 服务正在运行:`);
|
|
1141
|
+
console.error(` skill-scanner-api`);
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1415
1145
|
}, { commands: ["skills-scan"] });
|
|
1416
1146
|
|
|
1417
1147
|
api.logger.info("[skills-scanner] ✅ Plugin 注册完成");
|