@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.

Files changed (3) hide show
  1. package/README.md +65 -13
  2. package/index.ts +55 -325
  3. 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
- | `/scan-skill` `/scan-skills` 按需扫描 | `registerCommand` | ✅ 全自动 |
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
- 如果自动注册失败,可以使用 `/scan-cron register` 命令手动注册。
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
- /scan-cron register # 注册定时任务
106
- /scan-cron unregister # 删除定时任务
107
- /scan-cron status # 查看状态
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-report 并把结果发送到此渠道" \
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-report` 命令到指定渠道
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-report 并把结果发送到此渠道" \
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-report 并把结果发送到此渠道" \
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`,运行 `/scan-status` 查看并清空告警列表。
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 — 聊天命令 /scan-skill /scan-skills /scan-report /scan-status
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 日报:需用户安装后手动执行一次 CLI 命令注册(见 README)。
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
- " /scan-status",
250
+ " /skills-scanner status",
251
251
  "",
252
- "💬 或者在聊天中执行 /scan-status 查看当前状态",
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-report 并把结果发送到此渠道"',
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-report 并把结果发送到此渠道" \\');
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("", "💡 运行 `/scan-skill <路径> --detailed` 查看详情");
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 async function register(api: any) {
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 + "\" cisco-ai-skill-scanner");
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. 智能 Cron 任务注册 ────────────────────────────────────────────────
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 注册完成");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pwddd/skills-scanner",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "OpenClaw Plugin:Skills 安全扫描、安装前拦截、安全日报(命名空间命令、智能定时任务)",
5
5
  "main": "index.ts",
6
6
  "files": [