@pwddd/skills-scanner 2.1.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +199 -29
  2. package/index.ts +601 -188
  3. package/package.json +3 -4
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
 
@@ -73,36 +71,47 @@ openclaw gateway restart
73
71
  重启后 Gateway 会自动:
74
72
  1. 创建 Python venv 并安装 `requests` 库
75
73
  2. 开始监听 Skills 目录(安装前扫描)
76
- 3. 在启动日志里打印是否需要注册 Cron Job
74
+ 3. **智能注册定时任务**(检测已有任务,防止重复)
77
75
 
78
76
  ---
79
77
 
80
- ## 注册每日日报(一次性操作)
78
+ ## 定时任务自动注册
81
79
 
82
- Plugin 无法自动注册 Cron Job(OpenClaw Plugin API 不提供此能力),需要手动执行一次:
80
+ 插件启动时会**自动智能注册**定时任务,特点:
83
81
 
84
- ```bash
85
- openclaw cron add \
86
- --name "skills-daily-report" \
87
- --cron "0 8 * * *" \
88
- --tz "Asia/Shanghai" \
89
- --session isolated \
90
- --message "请执行 /scan-report 并把结果发送到此渠道" \
91
- --announce
92
- ```
82
+ - ✅ **幂等操作**:多次启动不会创建重复任务
83
+ - **智能检测**:自动发现已存在的同名任务
84
+ - ✅ **自动更新**:配置变更时自动更新任务
85
+ - **防止冲突**:即使手动创建过任务也能正确识别
86
+
87
+ 如果自动注册失败,可以使用 `/skills-scanner cron register` 命令手动注册。
88
+
89
+ ---
90
+
91
+ ## 注册每日日报(已自动化)
92
+
93
+ ~~Plugin 无法自动注册 Cron Job(OpenClaw Plugin API 不提供此能力),需要手动执行一次:~~
93
94
 
94
- 想投递到特定渠道(如 Telegram):
95
+ **更新**:从 v2.2.0 开始,插件会在启动时自动注册定时任务,无需手动操作!
96
+
97
+ 如果需要手动管理,使用以下命令:
98
+
99
+ 如果需要手动管理,使用以下命令:
95
100
 
96
101
  ```bash
102
+ # 使用聊天命令(推荐)
103
+ /skills-scanner cron register # 注册定时任务
104
+ /skills-scanner cron unregister # 删除定时任务
105
+ /skills-scanner cron status # 查看状态
106
+
107
+ # 或使用 CLI 命令
97
108
  openclaw cron add \
98
109
  --name "skills-daily-report" \
99
110
  --cron "0 8 * * *" \
100
111
  --tz "Asia/Shanghai" \
101
112
  --session isolated \
102
- --message "请执行 /scan-report 并把结果发送到此渠道" \
103
- --announce \
104
- --channel telegram \
105
- --to "+8613312345678"
113
+ --message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
114
+ --announce
106
115
  ```
107
116
 
108
117
  验证已注册:
@@ -115,15 +124,176 @@ openclaw cron list
115
124
 
116
125
  ## 聊天命令
117
126
 
127
+ ### 主命令
128
+
129
+ ```bash
130
+ /skills-scanner [子命令] [参数]
131
+ ```
132
+
133
+ ### 子命令
134
+
135
+ | 命令 | 说明 |
136
+ |---|---|
137
+ | `/skills-scanner scan <路径> [选项]` | 扫描 Skill(智能判断单个/批量) |
138
+ | `/skills-scanner status` | 查看状态、API 服务、定时任务 |
139
+ | `/skills-scanner config [操作]` | 配置管理(show/reset) |
140
+ | `/skills-scanner cron [操作]` | 定时任务管理(register/unregister/status) |
141
+ | `/skills-scanner help` | 显示帮助文档 |
142
+
143
+ ### 扫描选项
144
+
145
+ | 选项 | 说明 |
146
+ |---|---|
147
+ | `--detailed` | 显示所有 findings 详情 |
148
+ | `--behavioral` | 启用 AST 行为分析(更准确但较慢) |
149
+ | `--recursive` | 递归扫描子目录 |
150
+ | `--report` | 生成日报格式输出 |
151
+
152
+ ### 使用示例
153
+
154
+ ```bash
155
+ # 扫描单个 Skill
156
+ /skills-scanner scan ~/.openclaw/skills/my-skill
157
+
158
+ # 批量扫描(递归)
159
+ /skills-scanner scan ~/.openclaw/skills --recursive
160
+
161
+ # 生成日报
162
+ /skills-scanner scan ~/.openclaw/skills --report
163
+
164
+ # 详细扫描 + 行为分析
165
+ /skills-scanner scan ~/my-skill --detailed --behavioral
166
+
167
+ # 查看状态
168
+ /skills-scanner status
169
+
170
+ # 配置管理
171
+ /skills-scanner config show
172
+ /skills-scanner config reset
173
+
174
+ # 定时任务管理
175
+ /skills-scanner cron status
176
+ /skills-scanner cron register
177
+ /skills-scanner cron unregister
178
+
179
+ # 查看帮助
180
+ /skills-scanner help
181
+ ```
182
+
183
+ ---
184
+
185
+ ## CLI 命令
186
+
187
+ ### 可用命令
188
+
189
+ ```bash
190
+ openclaw skills-scan <子命令> [参数]
191
+ ```
192
+
193
+ ### 子命令
194
+
118
195
  | 命令 | 说明 |
119
196
  |---|---|
120
- | `/scan-skill <路径>` | 扫描单个 Skill 目录 |
121
- | `/scan-skill <路径> --detailed` | 显示完整 findings |
122
- | `/scan-skill <路径> --behavioral` | 启用 AST 行为分析 |
123
- | `/scan-skills <目录>` | 批量扫描目录 |
124
- | `/scan-skills <目录> --recursive` | 递归扫描子目录 |
125
- | `/scan-report` | 立即执行全量扫描并输出日报 |
126
- | `/scan-status` | 查看状态、待查告警、Cron 注册命令 |
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
+
239
+ ## 定时任务说明
240
+
241
+ 插件会在启动时**自动智能注册**定时任务,无需手动操作:
242
+
243
+ ### 智能注册机制
244
+
245
+ 1. **检测已有任务**:启动时查询系统中是否已有同名任务
246
+ 2. **幂等操作**:如果任务已存在,保存 ID 并跳过创建
247
+ 3. **自动更新**:如果任务配置变更(时间、时区),自动删除旧任务并创建新任务
248
+ 4. **防止重复**:即使多次重启或重新安装,也不会创建重复任务
249
+
250
+ ### 默认配置
251
+
252
+ - **任务名称**:`skills-daily-report`
253
+ - **执行时间**:每天 08:00
254
+ - **时区**:Asia/Shanghai
255
+ - **执行内容**:发送 `/skills-scanner scan --report` 命令到指定渠道
256
+
257
+ ### 手动管理
258
+
259
+ 如果自动注册失败,可以手动操作:
260
+
261
+ ```bash
262
+ # 查看所有定时任务
263
+ openclaw cron list
264
+
265
+ # 手动注册
266
+ openclaw cron add \
267
+ --name "skills-daily-report" \
268
+ --cron "0 8 * * *" \
269
+ --tz "Asia/Shanghai" \
270
+ --session isolated \
271
+ --message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
272
+ --announce
273
+
274
+ # 删除定时任务
275
+ openclaw cron remove <job-id>
276
+
277
+ # 修改执行时间(先删除再创建)
278
+ openclaw cron remove <job-id>
279
+ openclaw cron add --name "skills-daily-report" --cron "0 9 * * *" ...
280
+ ```
281
+
282
+ ### 投递到特定渠道
283
+
284
+ 如果想将日报发送到特定渠道(如 Telegram):
285
+
286
+ ```bash
287
+ openclaw cron add \
288
+ --name "skills-daily-report" \
289
+ --cron "0 8 * * *" \
290
+ --tz "Asia/Shanghai" \
291
+ --session isolated \
292
+ --message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
293
+ --announce \
294
+ --channel telegram \
295
+ --to "+8613312345678"
296
+ ```
127
297
 
128
298
  ---
129
299
 
@@ -131,7 +301,7 @@ openclaw cron list
131
301
 
132
302
  Plugin 启动后用 `fs.watch` 监听所有 Skills 目录。任何新 Skill 出现(无论通过 `clawhub install`、CLI 还是手动复制)都会触发扫描。
133
303
 
134
- 扫描结果通过 `persistWatcherAlert` 写入 `~/.openclaw/skills-scanner/state.json`,运行 `/scan-status` 查看并清空告警列表。
304
+ 扫描结果通过 `persistWatcherAlert` 写入 `~/.openclaw/skills-scanner/state.json`,运行 `/skills-scanner status` 查看并清空告警列表。
135
305
 
136
306
  > **为什么不直接发聊天消息?**
137
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";
@@ -70,6 +70,7 @@ interface ScannerConfig {
70
70
  interface ScanState {
71
71
  lastScanAt?: string;
72
72
  lastUnsafeSkills?: string[];
73
+ configReviewed?: boolean; // 标记配置是否已审查
73
74
  }
74
75
 
75
76
  // ── 工具函数 ──────────────────────────────────────────────────────────────────
@@ -119,6 +120,144 @@ function saveState(s: ScanState) {
119
120
  writeFileSync(STATE_FILE, JSON.stringify(s, null, 2));
120
121
  }
121
122
 
123
+ /**
124
+ * 检查是否是首次运行(使用默认配置)
125
+ */
126
+ function isFirstRun(cfg: ScannerConfig): boolean {
127
+ const state = loadState() as any;
128
+
129
+ // 如果 state 中已标记为非首次运行,直接返回 false
130
+ if (state.configReviewed) {
131
+ return false;
132
+ }
133
+
134
+ // 检查是否所有配置都是默认值
135
+ const isDefaultConfig =
136
+ !cfg.apiUrl && // 未设置 apiUrl
137
+ (!cfg.scanDirs || cfg.scanDirs.length === 0) && // 未设置 scanDirs
138
+ cfg.behavioral !== true && // 未启用 behavioral
139
+ cfg.useLLM !== true && // 未启用 useLLM
140
+ cfg.policy !== "strict" && // 未设置为 strict
141
+ cfg.policy !== "permissive" && // 未设置为 permissive
142
+ cfg.preInstallScan !== "off" && // 未禁用 preInstallScan
143
+ cfg.onUnsafe !== "delete" && // 未设置为 delete
144
+ cfg.onUnsafe !== "warn"; // 未设置为 warn
145
+
146
+ return isDefaultConfig;
147
+ }
148
+
149
+ /**
150
+ * 标记配置已审查
151
+ */
152
+ function markConfigReviewed() {
153
+ const state = loadState() as any;
154
+ saveState({ ...state, configReviewed: true });
155
+ }
156
+
157
+ /**
158
+ * 生成配置建议提示
159
+ */
160
+ function generateConfigGuide(cfg: ScannerConfig, apiUrl: string, scanDirs: string[], behavioral: boolean, useLLM: boolean, policy: string, preInstallScan: string, onUnsafe: string): string {
161
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
162
+
163
+ const lines = [
164
+ "",
165
+ "╔════════════════════════════════════════════════════════════════╗",
166
+ "║ 🎉 Skills Scanner 首次运行 - 配置向导 ║",
167
+ "╚════════════════════════════════════════════════════════════════╝",
168
+ "",
169
+ "当前使用默认配置。建议根据您的需求自定义配置:",
170
+ "",
171
+ "📋 当前配置:",
172
+ ` • API 服务地址: ${apiUrl}`,
173
+ ` • 扫描目录: ${scanDirs.length} 个(自动检测)`,
174
+ ` • 行为分析: ${behavioral ? "✅ 启用" : "❌ 禁用"}`,
175
+ ` • LLM 分析: ${useLLM ? "✅ 启用" : "❌ 禁用"}`,
176
+ ` • 扫描策略: ${policy}`,
177
+ ` • 安装前扫描: ${preInstallScan === "on" ? "✅ 启用" : "❌ 禁用"}`,
178
+ ` • 不安全处理: ${onUnsafe}`,
179
+ "",
180
+ "🔧 配置文件位置:",
181
+ ` ${configPath}`,
182
+ "",
183
+ "📝 推荐配置示例:",
184
+ "",
185
+ "```json",
186
+ "{",
187
+ ' "plugins": {',
188
+ ' "entries": {',
189
+ ' "skills-scanner": {',
190
+ ' "enabled": true,',
191
+ ' "config": {',
192
+ ' "apiUrl": "http://localhost:8000", // API 服务地址',
193
+ ' "scanDirs": [',
194
+ ' "~/.openclaw/skills", // 全局 skills',
195
+ ' "~/.openclaw/workspace/skills" // workspace skills',
196
+ ' ],',
197
+ ' "behavioral": false, // 启用行为分析(更准确但较慢)',
198
+ ' "useLLM": false, // 启用 LLM 分析(需 API 支持)',
199
+ ' "policy": "balanced", // strict | balanced | permissive',
200
+ ' "preInstallScan": "on", // 安装前扫描(推荐)',
201
+ ' "onUnsafe": "quarantine" // quarantine | delete | warn',
202
+ ' }',
203
+ ' }',
204
+ ' }',
205
+ ' }',
206
+ "}",
207
+ "```",
208
+ "",
209
+ "💡 配置说明:",
210
+ "",
211
+ "1. apiUrl - API 服务地址",
212
+ " • 默认: http://localhost:8000",
213
+ " • 需要先启动 skill-scanner-api 服务",
214
+ "",
215
+ "2. scanDirs - 扫描目录列表",
216
+ " • 默认: 自动检测 ~/.openclaw/skills",
217
+ " • 可添加多个目录",
218
+ "",
219
+ "3. behavioral - 行为分析",
220
+ " • false: 快速扫描(推荐)",
221
+ " • true: 深度分析(更准确但较慢)",
222
+ "",
223
+ "4. useLLM - LLM 分析",
224
+ " • false: 不使用 LLM(推荐)",
225
+ " • true: 使用 LLM 语义分析(需 API 服务配置 LLM)",
226
+ "",
227
+ "5. policy - 扫描策略",
228
+ " • strict: 严格模式(更多误报)",
229
+ " • balanced: 平衡模式(推荐)",
230
+ " • permissive: 宽松模式(可能漏报)",
231
+ "",
232
+ "6. preInstallScan - 安装前扫描",
233
+ " • on: 监听新 Skill 并自动扫描(推荐)",
234
+ " • off: 禁用自动扫描",
235
+ "",
236
+ "7. onUnsafe - 不安全 Skill 处理",
237
+ " • quarantine: 移入隔离目录(推荐)",
238
+ " • delete: 直接删除",
239
+ " • warn: 仅警告,保留文件",
240
+ "",
241
+ "🚀 快速开始:",
242
+ "",
243
+ "1. 编辑配置文件:",
244
+ ` nano ${configPath}`,
245
+ "",
246
+ "2. 重启 Gateway 使配置生效:",
247
+ " openclaw gateway restart",
248
+ "",
249
+ "3. 验证配置:",
250
+ " /skills-scanner status",
251
+ "",
252
+ "💬 或者在聊天中执行 /skills-scanner status 查看当前状态",
253
+ "",
254
+ "提示:此消息只在首次运行时显示。",
255
+ "════════════════════════════════════════════════════════════════",
256
+ ];
257
+
258
+ return lines.join("\n");
259
+ }
260
+
122
261
  async function ensureDeps(logger: any): Promise<boolean> {
123
262
  if (isVenvReady()) {
124
263
  logger.info("[skills-scanner] Python 依赖已就绪(requests 已安装)");
@@ -199,6 +338,156 @@ async function runScan(
199
338
  }
200
339
  }
201
340
 
341
+ // ── 智能 Cron 任务管理 ───────────────────────────────────────────────────
342
+
343
+ const CRON_JOB_NAME = "skills-daily-report";
344
+ const CRON_SCHEDULE = "0 8 * * *";
345
+ const CRON_TIMEZONE = "Asia/Shanghai";
346
+
347
+ /**
348
+ * 智能注册 Cron 任务(幂等操作)
349
+ * 1. 先查询系统中是否已有同名任务
350
+ * 2. 如果存在,保存 ID 并跳过
351
+ * 3. 如果不存在,创建新任务
352
+ * 4. 支持配置自定义时间和时区
353
+ */
354
+ async function ensureCronJob(logger: any): Promise<void> {
355
+ const state = loadState() as any;
356
+
357
+ logger.info("[skills-scanner] ─────────────────────────────────────");
358
+ logger.info("[skills-scanner] 🕐 检查定时任务...");
359
+
360
+ try {
361
+ // 1. 查询所有定时任务
362
+ let jobs: any[] = [];
363
+ try {
364
+ const listResult = execSync('openclaw cron list --format json', {
365
+ encoding: 'utf-8',
366
+ timeout: 5000
367
+ });
368
+ jobs = JSON.parse(listResult.trim());
369
+ } catch (listErr: any) {
370
+ // 如果 --format json 不支持,尝试解析文本输出
371
+ logger.debug("[skills-scanner] JSON 格式不支持,尝试文本解析");
372
+ try {
373
+ const listResult = execSync('openclaw cron list', {
374
+ encoding: 'utf-8',
375
+ timeout: 5000
376
+ });
377
+ // 简单解析:查找包含任务名的行
378
+ if (listResult.includes(CRON_JOB_NAME)) {
379
+ logger.info(`[skills-scanner] ✅ 发现已存在的任务: ${CRON_JOB_NAME}`);
380
+ // 无法获取 ID,但至少知道任务存在
381
+ if (!state.cronJobId) {
382
+ saveState({ ...state, cronJobId: 'manual-created' });
383
+ }
384
+ return;
385
+ }
386
+ } catch {
387
+ logger.debug("[skills-scanner] 无法列出定时任务,可能是权限问题");
388
+ }
389
+ }
390
+
391
+ // 2. 查找同名任务
392
+ const existingJob = jobs.find((j: any) =>
393
+ j.name === CRON_JOB_NAME ||
394
+ j.jobName === CRON_JOB_NAME ||
395
+ j.id === state.cronJobId
396
+ );
397
+
398
+ if (existingJob) {
399
+ const jobId = existingJob.id || existingJob.jobId || state.cronJobId;
400
+
401
+ // 检查任务配置是否需要更新
402
+ const needsUpdate =
403
+ existingJob.schedule !== CRON_SCHEDULE ||
404
+ existingJob.timezone !== CRON_TIMEZONE;
405
+
406
+ if (needsUpdate) {
407
+ logger.info(`[skills-scanner] 🔄 任务配置已变更,尝试更新...`);
408
+ try {
409
+ // 先删除旧任务
410
+ execSync(`openclaw cron remove ${jobId}`, { encoding: 'utf-8', timeout: 5000 });
411
+ logger.info(`[skills-scanner] ✅ 已删除旧任务: ${jobId}`);
412
+ // 继续创建新任务(不 return)
413
+ } catch (removeErr: any) {
414
+ logger.warn(`[skills-scanner] ⚠️ 删除旧任务失败: ${removeErr.message}`);
415
+ // 如果删除失败,保留现有任务
416
+ if (state.cronJobId !== jobId) {
417
+ saveState({ ...state, cronJobId: jobId });
418
+ }
419
+ logger.info(`[skills-scanner] ✅ 保留现有任务: ${jobId}`);
420
+ return;
421
+ }
422
+ } else {
423
+ // 配置未变,保存 ID 并返回
424
+ if (state.cronJobId !== jobId) {
425
+ saveState({ ...state, cronJobId: jobId });
426
+ logger.info(`[skills-scanner] ✅ 发现已存在的任务: ${jobId}`);
427
+ } else {
428
+ logger.info(`[skills-scanner] ✅ 任务已存在: ${jobId}`);
429
+ }
430
+ return;
431
+ }
432
+ }
433
+
434
+ // 3. 任务不存在或已删除,创建新任务
435
+ logger.info("[skills-scanner] 📝 正在创建定时任务...");
436
+
437
+ const cronCmd = [
438
+ 'openclaw cron add',
439
+ `--name "${CRON_JOB_NAME}"`,
440
+ `--cron "${CRON_SCHEDULE}"`,
441
+ `--tz "${CRON_TIMEZONE}"`,
442
+ '--session isolated',
443
+ '--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道"',
444
+ '--announce'
445
+ ].join(' ');
446
+
447
+ const result = execSync(cronCmd, { encoding: 'utf-8', timeout: 10000 });
448
+
449
+ // 4. 提取并保存 Job ID
450
+ const jobIdMatch = result.match(/Job ID[:\s]+([a-zA-Z0-9-]+)/i) ||
451
+ result.match(/jobId[:\s]+([a-zA-Z0-9-]+)/i) ||
452
+ result.match(/id[:\s]+([a-zA-Z0-9-]+)/i);
453
+
454
+ if (jobIdMatch) {
455
+ const cronJobId = jobIdMatch[1];
456
+ saveState({ ...state, cronJobId });
457
+ logger.info(`[skills-scanner] ✅ 任务创建成功: ${cronJobId}`);
458
+ logger.info(`[skills-scanner] 📅 执行时间: 每天 ${CRON_SCHEDULE.split(' ')[1]}:${CRON_SCHEDULE.split(' ')[0]} (${CRON_TIMEZONE})`);
459
+ } else {
460
+ // 无法提取 ID,但命令执行成功
461
+ logger.info("[skills-scanner] ✅ 任务创建命令已执行");
462
+ logger.debug(`[skills-scanner] 输出: ${result.trim()}`);
463
+ saveState({ ...state, cronJobId: 'created-unknown-id' });
464
+ }
465
+
466
+ } catch (err: any) {
467
+ // 5. 创建失败,提供手动注册指引
468
+ logger.warn("[skills-scanner] ⚠️ 自动注册失败");
469
+ logger.debug(`[skills-scanner] 错误详情: ${err.message}`);
470
+
471
+ // 检查是否是权限问题
472
+ if (err.message.includes('permission') || err.message.includes('EACCES')) {
473
+ logger.error("[skills-scanner] ❌ 权限不足,请使用管理员权限运行");
474
+ } else if (err.message.includes('command not found') || err.message.includes('ENOENT')) {
475
+ logger.error("[skills-scanner] ❌ openclaw 命令未找到,请检查安装");
476
+ } else {
477
+ logger.info("[skills-scanner] 💡 请手动执行以下命令注册定时任务:");
478
+ logger.info("[skills-scanner]");
479
+ logger.info('[skills-scanner] openclaw cron add \\');
480
+ logger.info(`[skills-scanner] --name "${CRON_JOB_NAME}" \\`);
481
+ logger.info(`[skills-scanner] --cron "${CRON_SCHEDULE}" \\`);
482
+ logger.info(`[skills-scanner] --tz "${CRON_TIMEZONE}" \\`);
483
+ logger.info('[skills-scanner] --session isolated \\');
484
+ logger.info('[skills-scanner] --message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \\');
485
+ logger.info('[skills-scanner] --announce');
486
+ logger.info("[skills-scanner]");
487
+ }
488
+ }
489
+ }
490
+
202
491
  // ── 日报 ──────────────────────────────────────────────────────────────────────
203
492
 
204
493
  async function buildDailyReport(
@@ -264,7 +553,7 @@ async function buildDailyReport(
264
553
  const r = allResults.find(x => (x.name || basename(x.path ?? "")) === name);
265
554
  lines.push(` • ${name} [${r?.max_severity ?? "?"}] — ${r?.findings ?? "?"} 条发现`);
266
555
  }
267
- lines.push("", "💡 运行 `/scan-skill <路径> --detailed` 查看详情");
556
+ lines.push("", "💡 运行 `/skills-scanner scan <路径> --detailed` 查看详情");
268
557
  } else {
269
558
  lines.push("", "🎉 所有 Skills 安全,未发现威胁。");
270
559
  }
@@ -372,7 +661,7 @@ function startWatcher(
372
661
 
373
662
  // ── Plugin 入口 ───────────────────────────────────────────────────────────────
374
663
 
375
- export default function register(api: any) {
664
+ export default async function register(api: any) {
376
665
  const cfg: ScannerConfig = api.config?.plugins?.entries?.["skills-scanner"]?.config ?? {};
377
666
  const apiUrl = cfg.apiUrl ?? "http://localhost:8000";
378
667
  const scanDirs = (cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
@@ -394,6 +683,21 @@ export default function register(api: any) {
394
683
  api.logger.info(`[skills-scanner] 扫描目录: ${scanDirs.join(", ")}`);
395
684
  api.logger.info(`[skills-scanner] Python 依赖状态: ${isVenvReady() ? "✅ 已就绪" : "❌ 未安装"}`);
396
685
 
686
+ // 检查是否首次运行
687
+ const firstRun = isFirstRun(cfg);
688
+ if (firstRun) {
689
+ api.logger.info("[skills-scanner] ═══════════════════════════════════════");
690
+ api.logger.info("[skills-scanner] 🎉 首次运行检测");
691
+ api.logger.info("[skills-scanner] ═══════════════════════════════════════");
692
+
693
+ // 输出配置向导
694
+ const configGuide = generateConfigGuide(cfg, apiUrl, scanDirs, behavioral, useLLM, policy, preInstallScan, onUnsafe);
695
+ console.log(configGuide);
696
+
697
+ // 标记为已审查(避免每次启动都显示)
698
+ markConfigReviewed();
699
+ }
700
+
397
701
  // 立即尝试安装依赖(不等待 service start)
398
702
  if (!isVenvReady()) {
399
703
  api.logger.info("[skills-scanner] 开始安装 Python 依赖...");
@@ -429,7 +733,7 @@ export default function register(api: any) {
429
733
 
430
734
  if (!depsReady) {
431
735
  api.logger.error("[skills-scanner] ❌ 依赖安装失败,服务无法启动");
432
- 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");
433
737
  return;
434
738
  }
435
739
 
@@ -441,6 +745,15 @@ export default function register(api: any) {
441
745
  api.logger.info("[skills-scanner] ⏭️ 安装前扫描已禁用");
442
746
  }
443
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
+
444
757
  api.logger.info("[skills-scanner] ─────────────────────────────────────");
445
758
  },
446
759
  stop: () => {
@@ -450,57 +763,106 @@ export default function register(api: any) {
450
763
  },
451
764
  });
452
765
 
453
- // ── 2. 启动日志和 Cron 自动注册 ────────────────────────────────────────────
454
- const state = loadState() as any;
455
- if (!state.cronJobId) {
456
- api.logger.warn("[skills-scanner] ⚠️ 未检测到日报 Cron Job");
457
- api.logger.info("[skills-scanner] 尝试自动注册...");
458
-
459
- try {
460
- const cronCmd = 'openclaw cron add --name "skills-daily-report" --cron "0 8 * * *" --tz "Asia/Shanghai" --session isolated --message "请执行 /scan-report 并把结果发送到此渠道" --announce';
461
- const result = execSync(cronCmd, { encoding: 'utf-8' });
462
-
463
- // 尝试从输出中提取 job ID
464
- const jobIdMatch = result.match(/Job ID[:\s]+([a-zA-Z0-9-]+)/i) || result.match(/jobId[:\s]+([a-zA-Z0-9-]+)/i);
465
- if (jobIdMatch) {
466
- const cronJobId = jobIdMatch[1];
467
- saveState({ ...state, cronJobId });
468
- api.logger.info(`[skills-scanner] ✅ Cron Job 自动注册成功: ${cronJobId}`);
469
- } else {
470
- api.logger.info("[skills-scanner] ✅ Cron Job 注册命令已执行");
471
- api.logger.info("[skills-scanner] 输出: " + result.trim());
472
- }
473
- } catch (err: any) {
474
- api.logger.warn("[skills-scanner] ⚠️ 自动注册失败,请手动执行:");
475
- api.logger.info('[skills-scanner] openclaw cron add --name "skills-daily-report" --cron "0 8 * * *" --tz "Asia/Shanghai" --session isolated --message "请执行 /scan-report 并把结果发送到此渠道" --announce');
476
- api.logger.debug(`[skills-scanner] 错误详情: ${err.message}`);
477
- }
478
- } else {
479
- api.logger.info(`[skills-scanner] ✅ 日报 Cron Job 已存在: ${state.cronJobId}`);
480
- }
481
- api.logger.info("[skills-scanner] ═══════════════════════════════════════");
482
-
483
- // ── 3. /scan-skill ────────────────────────────────────────────────────────
766
+ // ── 2. /skills-scanner 主命令(命名空间)─────────────────────────────────
484
767
  api.registerCommand({
485
- name: "scan-skill",
486
- description: "扫描单个 Skill 目录。用法: /scan-skill <路径> [--detailed] [--behavioral]",
768
+ name: "skills-scanner",
769
+ description: "Skills 安全扫描工具。用法: /skills-scanner <子命令> [参数]",
487
770
  acceptsArgs: true,
488
771
  requireAuth: true,
489
772
  handler: async (ctx: any) => {
490
- const raw = (ctx.args ?? "").trim();
491
- if (!raw) return { text: "用法:`/scan-skill <路径> [--detailed] [--behavioral]`" };
773
+ const args = (ctx.args ?? "").trim();
492
774
 
493
- // 先检查依赖
494
- if (!isVenvReady()) {
495
- return { text: "⏳ Python 依赖尚未就绪,请稍后重试或查看日志" };
775
+ // 无参数时显示帮助
776
+ if (!args) {
777
+ return {
778
+ text: [
779
+ "🔍 *Skills Scanner - 安全扫描工具*",
780
+ "",
781
+ "可用命令:",
782
+ "• `/skills-scanner scan <路径> [选项]` - 扫描 Skill",
783
+ "• `/skills-scanner status` - 查看状态",
784
+ "• `/skills-scanner config [操作]` - 配置管理",
785
+ "• `/skills-scanner cron [操作]` - 定时任务管理",
786
+ "",
787
+ "扫描选项:",
788
+ "• `--detailed` - 显示详细发现",
789
+ "• `--behavioral` - 启用行为分析",
790
+ "• `--recursive` - 递归扫描子目录",
791
+ "• `--report` - 生成日报格式",
792
+ "",
793
+ "示例:",
794
+ "```",
795
+ "/skills-scanner scan ~/my-skill",
796
+ "/skills-scanner scan ~/skills --recursive",
797
+ "/skills-scanner scan ~/skills --report",
798
+ "/skills-scanner status",
799
+ "```",
800
+ "",
801
+ "💡 使用 `/skills-scanner help` 查看详细帮助"
802
+ ].join("\n")
803
+ };
804
+ }
805
+
806
+ const parts = args.split(/\s+/);
807
+ const subCommand = parts[0].toLowerCase();
808
+ const subArgs = parts.slice(1).join(" ");
809
+
810
+ // 路由到子命令
811
+ if (subCommand === "scan") {
812
+ return await handleScanCommand(subArgs);
813
+ } else if (subCommand === "status") {
814
+ return await handleStatusCommand();
815
+ } else if (subCommand === "config") {
816
+ return await handleConfigCommand(subArgs);
817
+ } else if (subCommand === "cron") {
818
+ return await handleCronCommand(subArgs);
819
+ } else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
820
+ return { text: getHelpText() };
821
+ } else {
822
+ return { text: `❌ 未知子命令: ${subCommand}\n\n使用 \`/skills-scanner help\` 查看帮助` };
496
823
  }
824
+ },
825
+ });
826
+
827
+ // ── 子命令处理器 ──────────────────────────────────────────────────────────
828
+
829
+ async function handleScanCommand(args: string): Promise<any> {
830
+ if (!args) {
831
+ return { text: "用法:`/skills-scanner scan <路径> [--detailed] [--behavioral] [--recursive] [--report]`" };
832
+ }
833
+
834
+ if (!isVenvReady()) {
835
+ return { text: "⏳ Python 依赖尚未就绪,请稍后重试或查看日志" };
836
+ }
837
+
838
+ const parts = args.split(/\s+/);
839
+ const targetPath = expandPath(parts.find(p => !p.startsWith("--")) ?? "");
840
+ const detailed = parts.includes("--detailed");
841
+ const useBehav = parts.includes("--behavioral") || behavioral;
842
+ const recursive = parts.includes("--recursive");
843
+ const isReport = parts.includes("--report");
844
+
845
+ if (!targetPath) {
846
+ return { text: "❌ 请指定扫描路径" };
847
+ }
497
848
 
498
- const parts = raw.split(/\s+/);
499
- const skillPath = expandPath(parts.find(p => !p.startsWith("--")) ?? "");
500
- const detailed = parts.includes("--detailed");
501
- const useBehav = parts.includes("--behavioral") || behavioral;
849
+ if (!existsSync(targetPath)) {
850
+ return { text: `❌ 路径不存在: ${targetPath}` };
851
+ }
502
852
 
503
- const res = await runScan("scan", skillPath, {
853
+ // 判断是单个 Skill 还是批量扫描
854
+ const isSingleSkill = existsSync(join(targetPath, "SKILL.md"));
855
+
856
+ if (isReport) {
857
+ // 生成日报
858
+ if (scanDirs.length === 0) {
859
+ return { text: "⚠️ 未找到可扫描目录,请检查配置" };
860
+ }
861
+ const report = await buildDailyReport(scanDirs, useBehav, apiUrl, useLLM, policy, api.logger);
862
+ return { text: report };
863
+ } else if (isSingleSkill) {
864
+ // 单个 Skill 扫描
865
+ const res = await runScan("scan", targetPath, {
504
866
  detailed,
505
867
  behavioral: useBehav,
506
868
  apiUrl,
@@ -509,27 +871,9 @@ export default function register(api: any) {
509
871
  });
510
872
  const icon = res.exitCode === 0 ? "✅" : "❌";
511
873
  return { text: `${icon} 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
512
- },
513
- });
514
-
515
- // ── 4. /scan-skills ───────────────────────────────────────────────────────
516
- api.registerCommand({
517
- name: "scan-skills",
518
- description: "批量扫描目录下所有 Skills。用法: /scan-skills <目录> [--recursive] [--detailed]",
519
- acceptsArgs: true,
520
- requireAuth: true,
521
- handler: async (ctx: any) => {
522
- const raw = (ctx.args ?? "").trim();
523
- if (!raw) return { text: "用法:`/scan-skills <目录> [--recursive] [--detailed]`" };
524
- if (!isVenvReady()) return { text: "⏳ Python 依赖尚未就绪,请稍后重试" };
525
-
526
- const parts = raw.split(/\s+/);
527
- const dirPath = expandPath(parts.find(p => !p.startsWith("--")) ?? "");
528
- const recursive = parts.includes("--recursive");
529
- const detailed = parts.includes("--detailed");
530
- const useBehav = parts.includes("--behavioral") || behavioral;
531
-
532
- const res = await runScan("batch", dirPath, {
874
+ } else {
875
+ // 批量扫描
876
+ const res = await runScan("batch", targetPath, {
533
877
  recursive,
534
878
  detailed,
535
879
  behavioral: useBehav,
@@ -539,133 +883,167 @@ export default function register(api: any) {
539
883
  });
540
884
  const icon = res.exitCode === 0 ? "✅" : "❌";
541
885
  return { text: `${icon} 批量扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
542
- },
543
- });
886
+ }
887
+ }
544
888
 
545
- // ── 5. /scan-report ───────────────────────────────────────────────────────
546
- api.registerCommand({
547
- name: "scan-report",
548
- description: "立即执行全量扫描并生成安全日报",
549
- acceptsArgs: false,
550
- requireAuth: true,
551
- handler: async (_ctx: any) => {
552
- if (!isVenvReady()) return { text: " Python 依赖尚未就绪,请稍后重试" };
553
- if (scanDirs.length === 0) return { text: "⚠️ 未找到可扫描目录,请检查配置" };
554
- const report = await buildDailyReport(scanDirs, behavioral, apiUrl, useLLM, policy, api.logger);
555
- return { text: report };
556
- },
557
- });
889
+ async function handleStatusCommand(): Promise<any> {
890
+ const state = loadState() as any;
891
+ const alerts: string[] = state.pendingAlerts ?? [];
892
+
893
+ const lines = [
894
+ "📋 *Skills Scanner 状态*",
895
+ `API 服务地址:${apiUrl}`,
896
+ `Python 依赖:${isVenvReady() ? "✅ 就绪" : " 未就绪"}`,
897
+ `安装前扫描:${preInstallScan === "on" ? `✅ 监听中(${onUnsafe})` : " 已禁用"}`,
898
+ `扫描策略:${policy}`,
899
+ `LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
900
+ `行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
901
+ `上次扫描:${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
902
+ `扫描目录:\n${scanDirs.map(d => ` • ${d}`).join("\n")}`,
903
+ ];
904
+
905
+ // API 健康检查
906
+ if (isVenvReady()) {
907
+ lines.push("", "🔍 *API 服务检查*");
908
+ try {
909
+ const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
910
+ const env = { ...process.env };
911
+ delete env.http_proxy;
912
+ delete env.https_proxy;
913
+ delete env.HTTP_PROXY;
914
+ delete env.HTTPS_PROXY;
915
+ delete env.all_proxy;
916
+ delete env.ALL_PROXY;
917
+
918
+ const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
919
+ const output = (stdout + stderr).trim();
920
+
921
+ if (output.includes("✓") || output.includes("正常")) {
922
+ lines.push(`API 服务:✅ 正常`);
923
+ const jsonMatch = output.match(/\{[\s\S]*\}/);
924
+ if (jsonMatch) {
925
+ try {
926
+ const healthData = JSON.parse(jsonMatch[0]);
927
+ if (healthData.analyzers_available) {
928
+ lines.push(`可用分析器:${healthData.analyzers_available.join(", ")}`);
929
+ }
930
+ } catch {}
931
+ }
932
+ } else {
933
+ lines.push(`API 服务:❌ 不可用`);
934
+ }
935
+ } catch (err: any) {
936
+ lines.push(`API 服务:❌ 连接失败`);
937
+ lines.push(`错误:无法连接到 ${apiUrl}`);
938
+ }
939
+ }
558
940
 
559
- // ── 6. /scan-status ───────────────────────────────────────────────────────
560
- api.registerCommand({
561
- name: "scan-status",
562
- description: "查看 Skills Scanner 状态和待查告警",
563
- acceptsArgs: false,
564
- requireAuth: true,
565
- handler: async (_ctx: any) => {
941
+ if (alerts.length > 0) {
942
+ lines.push("", `🔔 *待查告警(${alerts.length} 条):*`);
943
+ alerts.slice(-5).forEach(a => lines.push(` ${a}`));
944
+ saveState({ ...state, pendingAlerts: [] });
945
+ }
946
+
947
+ // 定时任务状态
948
+ lines.push("", "🕐 *定时任务*");
949
+ if (state.cronJobId && state.cronJobId !== 'manual-created') {
950
+ lines.push(`状态:✅ 已注册 (${state.cronJobId})`);
951
+ } else {
952
+ lines.push("状态:❌ 未注册");
953
+ lines.push("💡 使用 `/skills-scanner cron register` 注册");
954
+ }
955
+
956
+ return { text: lines.join("\n") };
957
+ }
958
+
959
+ async function handleConfigCommand(args: string): Promise<any> {
960
+ const action = args.trim().toLowerCase() || "show";
961
+
962
+ if (action === "show" || action === "") {
963
+ const configGuide = generateConfigGuide(cfg, apiUrl, scanDirs, behavioral, useLLM, policy, preInstallScan, onUnsafe);
964
+ return { text: "```\n" + configGuide + "\n```" };
965
+ } else if (action === "reset") {
566
966
  const state = loadState() as any;
567
- const alerts: string[] = state.pendingAlerts ?? [];
568
-
569
- const lines = [
570
- "📋 *Skills Scanner 状态*",
571
- `API 服务地址:${apiUrl}`,
572
- `Python 依赖:${isVenvReady() ? "✅ 就绪" : "❌ 未就绪"}`,
573
- `Python 路径:${VENV_PYTHON}`,
574
- `scan.py 路径:${SCAN_SCRIPT}`,
575
- `安装前扫描:${preInstallScan === "on" ? `✅ 监听中(${onUnsafe})` : "❌ 已禁用"}`,
576
- `扫描策略:${policy}`,
577
- `LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
578
- `行为分析:${behavioral ? "✅ 启用" : " 禁用"}`,
579
- `上次扫描:${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
580
- `上次问题 Skills:${state.lastUnsafeSkills?.length ? state.lastUnsafeSkills.join(", ") : "无"}`,
581
- `扫描目录:\n${scanDirs.map(d => ` • ${d}`).join("\n")}`,
582
- ];
583
-
584
- // API 健康检查
585
- if (isVenvReady()) {
586
- lines.push("", "🔍 *API 服务检查*");
967
+ saveState({ ...state, configReviewed: false });
968
+ return { text: "✅ 配置审查标记已重置\n下次重启 Gateway 时将再次显示配置向导" };
969
+ } else {
970
+ return { text: "用法: `/skills-scanner config [show|reset]`" };
971
+ }
972
+ }
973
+
974
+ async function handleCronCommand(args: string): Promise<any> {
975
+ const action = args.trim().toLowerCase() || "status";
976
+ const state = loadState() as any;
977
+
978
+ if (action === "register") {
979
+ const oldJobId = state.cronJobId;
980
+ if (oldJobId && oldJobId !== 'manual-created') {
587
981
  try {
588
- // 调用 scan.py health 命令
589
- const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
590
-
591
- // 清除代理环境变量
592
- const env = { ...process.env };
593
- delete env.http_proxy;
594
- delete env.https_proxy;
595
- delete env.HTTP_PROXY;
596
- delete env.HTTPS_PROXY;
597
- delete env.all_proxy;
598
- delete env.ALL_PROXY;
599
-
600
- const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
601
- const output = (stdout + stderr).trim();
602
-
603
- if (output.includes("✓") || output.includes("正常")) {
604
- lines.push(`API 服务:✅ 正常`);
605
-
606
- // 尝试解析可用的分析器信息
607
- if (output.includes("analyzers_available")) {
608
- try {
609
- // 从输出中提取 JSON(如果有的话)
610
- const jsonMatch = output.match(/\{[\s\S]*\}/);
611
- if (jsonMatch) {
612
- const healthData = JSON.parse(jsonMatch[0]);
613
- if (healthData.analyzers_available) {
614
- lines.push(`可用分析器:${healthData.analyzers_available.join(", ")}`);
615
- }
616
- if (healthData.version) {
617
- lines.push(`API 版本:${healthData.version}`);
618
- }
619
- }
620
- } catch {}
621
- }
622
- } else {
623
- lines.push(`API 服务:❌ 不可用`);
624
- lines.push(`响应:${output}`);
625
- }
626
- } catch (err: any) {
627
- lines.push(`API 服务:❌ 连接失败`);
628
- const errorMsg = err.message || err.toString();
629
- if (errorMsg.includes("ECONNREFUSED") || errorMsg.includes("无法连接")) {
630
- lines.push(`错误:无法连接到 ${apiUrl}`);
631
- } else {
632
- lines.push(`错误:${errorMsg}`);
633
- }
634
- lines.push("", "💡 请确保 skill-scanner-api 服务正在运行:");
635
- lines.push("```");
636
- lines.push("skill-scanner-api");
637
- lines.push("# 或指定端口");
638
- lines.push("skill-scanner-api --port 8080");
639
- lines.push("```");
640
- }
982
+ execSync(`openclaw cron remove ${oldJobId}`, { encoding: 'utf-8', timeout: 5000 });
983
+ } catch {}
984
+ }
985
+
986
+ saveState({ ...state, cronJobId: undefined });
987
+ await ensureCronJob(api.logger);
988
+
989
+ const newState = loadState() as any;
990
+ if (newState.cronJobId) {
991
+ return { text: `✅ 定时任务注册成功\n任务 ID: ${newState.cronJobId}` };
641
992
  } else {
642
- lines.push("", "⚠️ Python 依赖未就绪,无法检查 API 服务");
993
+ return { text: "❌ 定时任务注册失败,请查看日志" };
643
994
  }
644
-
645
- if (alerts.length > 0) {
646
- lines.push("", `🔔 *待查告警(${alerts.length} 条):*`);
647
- alerts.slice(-5).forEach(a => lines.push(` ${a}`));
648
- // 读取后清空
649
- saveState({ ...state, pendingAlerts: [] });
995
+ } else if (action === "unregister") {
996
+ if (!state.cronJobId) {
997
+ return { text: "⚠️ 未找到已注册的定时任务" };
998
+ }
999
+
1000
+ try {
1001
+ execSync(`openclaw cron remove ${state.cronJobId}`, { encoding: 'utf-8', timeout: 5000 });
1002
+ saveState({ ...state, cronJobId: undefined });
1003
+ return { text: `✅ 定时任务已删除: ${state.cronJobId}` };
1004
+ } catch (err: any) {
1005
+ return { text: `❌ 删除失败: ${err.message}` };
1006
+ }
1007
+ } else {
1008
+ const lines = ["🕐 *定时任务状态*"];
1009
+ if (state.cronJobId && state.cronJobId !== 'manual-created') {
1010
+ lines.push(`任务 ID: ${state.cronJobId}`);
1011
+ lines.push(`执行时间: 每天 08:00 (Asia/Shanghai)`);
1012
+ lines.push("状态: ✅ 已注册");
1013
+ } else {
1014
+ lines.push("状态: ❌ 未注册");
1015
+ lines.push("", "💡 使用 `/skills-scanner cron register` 注册");
650
1016
  }
651
-
652
- lines.push(
653
- "",
654
- "定期日报(需手动注册一次):",
655
- "```",
656
- "openclaw cron add \\",
657
- ' --name "skills-daily-report" \\',
658
- ' --cron "0 8 * * *" --tz "Asia/Shanghai" \\',
659
- " --session isolated \\",
660
- ' --message "请执行 /scan-report 并把结果发送到此渠道" \\',
661
- " --announce",
662
- "```",
663
- );
664
1017
  return { text: lines.join("\n") };
665
- },
666
- });
1018
+ }
1019
+ }
667
1020
 
668
- // ── 7. Gateway RPC ────────────────────────────────────────────────────────
1021
+ function getHelpText(): string {
1022
+ return [
1023
+ "🔍 *Skills Scanner - 帮助文档*",
1024
+ "",
1025
+ "═══ 扫描命令 ═══",
1026
+ "`/skills-scanner scan <路径> [选项]`",
1027
+ "",
1028
+ "选项:",
1029
+ "• `--detailed` - 显示详细发现",
1030
+ "• `--behavioral` - 启用行为分析",
1031
+ "• `--recursive` - 递归扫描子目录",
1032
+ "• `--report` - 生成日报格式",
1033
+ "",
1034
+ "示例:",
1035
+ "```",
1036
+ "/skills-scanner scan ~/.openclaw/skills/my-skill",
1037
+ "/skills-scanner scan ~/.openclaw/skills --recursive",
1038
+ "/skills-scanner scan ~/.openclaw/skills --report",
1039
+ "```",
1040
+ "",
1041
+ "═══ 其他命令 ═══",
1042
+ "• `/skills-scanner status` - 查看状态",
1043
+ "• `/skills-scanner config [show|reset]` - 配置管理",
1044
+ "• `/skills-scanner cron [register|unregister|status]` - 定时任务",
1045
+ ].join("\n");
1046
+ } // ── 9. Gateway RPC ────────────────────────────────────────────────────────
669
1047
  api.registerGatewayMethod("skillsScanner.scan", async ({ respond, params }: any) => {
670
1048
  const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
671
1049
  if (!p) return respond(false, { error: "缺少 path 参数" });
@@ -688,7 +1066,7 @@ export default function register(api: any) {
688
1066
  respond(true, { report, state: loadState() });
689
1067
  });
690
1068
 
691
- // ── 8. CLI ────────────────────────────────────────────────────────────────
1069
+ // ── 9. CLI ────────────────────────────────────────────────────────────────
692
1070
  api.registerCli(({ program }: any) => {
693
1071
  const cmd = program.command("skills-scan").description("OpenClaw Skills 安全扫描");
694
1072
 
@@ -729,6 +1107,41 @@ export default function register(api: any) {
729
1107
  const report = await buildDailyReport(scanDirs, behavioral, apiUrl, useLLM, policy, console);
730
1108
  console.log(report);
731
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
+ });
732
1145
  }, { commands: ["skills-scan"] });
733
1146
 
734
1147
  api.logger.info("[skills-scanner] ✅ Plugin 注册完成");
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@pwddd/skills-scanner",
3
- "version": "2.1.0",
4
- "description": "OpenClaw Plugin:Skills 安全扫描、安装前拦截、安全日报(HTTP API 客户端模式)",
3
+ "version": "2.4.0",
4
+ "description": "OpenClaw Plugin:Skills 安全扫描、安装前拦截、安全日报(命名空间命令、智能定时任务)",
5
5
  "main": "index.ts",
6
6
  "files": [
7
7
  "index.ts",
8
8
  "openclaw.plugin.json",
9
- "hooks/**",
10
9
  "skills/**"
11
10
  ],
12
11
  "openclaw": {
13
12
  "extensions": ["./index.ts"]
14
13
  },
15
- "keywords": ["openclaw", "openclaw-plugin", "security", "skill-scanner", "http-client"],
14
+ "keywords": ["openclaw", "openclaw-plugin", "security", "skill-scanner", "http-client", "cron", "namespace"],
16
15
  "license": "MIT",
17
16
  "publishConfig": {
18
17
  "access": "public"