@pwddd/skills-scanner 2.1.0 → 2.3.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.

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 +140 -22
  2. package/index.ts +721 -38
  3. package/package.json +3 -4
package/README.md CHANGED
@@ -73,15 +73,40 @@ openclaw gateway restart
73
73
  重启后 Gateway 会自动:
74
74
  1. 创建 Python venv 并安装 `requests` 库
75
75
  2. 开始监听 Skills 目录(安装前扫描)
76
- 3. 在启动日志里打印是否需要注册 Cron Job
76
+ 3. **智能注册定时任务**(检测已有任务,防止重复)
77
77
 
78
78
  ---
79
79
 
80
- ## 注册每日日报(一次性操作)
80
+ ## 定时任务自动注册
81
81
 
82
- Plugin 无法自动注册 Cron Job(OpenClaw Plugin API 不提供此能力),需要手动执行一次:
82
+ 插件启动时会**自动智能注册**定时任务,特点:
83
+
84
+ - ✅ **幂等操作**:多次启动不会创建重复任务
85
+ - ✅ **智能检测**:自动发现已存在的同名任务
86
+ - ✅ **自动更新**:配置变更时自动更新任务
87
+ - ✅ **防止冲突**:即使手动创建过任务也能正确识别
88
+
89
+ 如果自动注册失败,可以使用 `/scan-cron register` 命令手动注册。
90
+
91
+ ---
92
+
93
+ ## 注册每日日报(已自动化)
94
+
95
+ ~~Plugin 无法自动注册 Cron Job(OpenClaw Plugin API 不提供此能力),需要手动执行一次:~~
96
+
97
+ **更新**:从 v2.2.0 开始,插件会在启动时自动注册定时任务,无需手动操作!
98
+
99
+ 如果需要手动管理,使用以下命令:
100
+
101
+ 如果需要手动管理,使用以下命令:
83
102
 
84
103
  ```bash
104
+ # 使用聊天命令(推荐)
105
+ /scan-cron register # 注册定时任务
106
+ /scan-cron unregister # 删除定时任务
107
+ /scan-cron status # 查看状态
108
+
109
+ # 或使用 CLI 命令
85
110
  openclaw cron add \
86
111
  --name "skills-daily-report" \
87
112
  --cron "0 8 * * *" \
@@ -91,39 +116,132 @@ openclaw cron add \
91
116
  --announce
92
117
  ```
93
118
 
94
- 想投递到特定渠道(如 Telegram):
119
+ 验证已注册:
120
+
121
+ ```bash
122
+ openclaw cron list
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 聊天命令
128
+
129
+ ### 主命令
130
+
131
+ ```bash
132
+ /skills-scanner [子命令] [参数]
133
+ ```
134
+
135
+ ### 子命令
136
+
137
+ | 命令 | 说明 |
138
+ |---|---|
139
+ | `/skills-scanner scan <路径> [选项]` | 扫描 Skill(智能判断单个/批量) |
140
+ | `/skills-scanner status` | 查看状态、API 服务、定时任务 |
141
+ | `/skills-scanner config [操作]` | 配置管理(show/reset) |
142
+ | `/skills-scanner cron [操作]` | 定时任务管理(register/unregister/status) |
143
+ | `/skills-scanner help` | 显示帮助文档 |
144
+
145
+ ### 扫描选项
146
+
147
+ | 选项 | 说明 |
148
+ |---|---|
149
+ | `--detailed` | 显示所有 findings 详情 |
150
+ | `--behavioral` | 启用 AST 行为分析(更准确但较慢) |
151
+ | `--recursive` | 递归扫描子目录 |
152
+ | `--report` | 生成日报格式输出 |
153
+
154
+ ### 使用示例
155
+
156
+ ```bash
157
+ # 扫描单个 Skill
158
+ /skills-scanner scan ~/.openclaw/skills/my-skill
159
+
160
+ # 批量扫描(递归)
161
+ /skills-scanner scan ~/.openclaw/skills --recursive
162
+
163
+ # 生成日报
164
+ /skills-scanner scan ~/.openclaw/skills --report
165
+
166
+ # 详细扫描 + 行为分析
167
+ /skills-scanner scan ~/my-skill --detailed --behavioral
168
+
169
+ # 查看状态
170
+ /skills-scanner status
171
+
172
+ # 配置管理
173
+ /skills-scanner config show
174
+ /skills-scanner config reset
175
+
176
+ # 定时任务管理
177
+ /skills-scanner cron status
178
+ /skills-scanner cron register
179
+ /skills-scanner cron unregister
180
+
181
+ # 查看帮助
182
+ /skills-scanner help
183
+ ```
184
+
185
+ ---
186
+
187
+ ## 定时任务说明
188
+
189
+ 插件会在启动时**自动智能注册**定时任务,无需手动操作:
190
+
191
+ ### 智能注册机制
192
+
193
+ 1. **检测已有任务**:启动时查询系统中是否已有同名任务
194
+ 2. **幂等操作**:如果任务已存在,保存 ID 并跳过创建
195
+ 3. **自动更新**:如果任务配置变更(时间、时区),自动删除旧任务并创建新任务
196
+ 4. **防止重复**:即使多次重启或重新安装,也不会创建重复任务
197
+
198
+ ### 默认配置
199
+
200
+ - **任务名称**:`skills-daily-report`
201
+ - **执行时间**:每天 08:00
202
+ - **时区**:Asia/Shanghai
203
+ - **执行内容**:发送 `/scan-report` 命令到指定渠道
204
+
205
+ ### 手动管理
206
+
207
+ 如果自动注册失败,可以手动操作:
95
208
 
96
209
  ```bash
210
+ # 查看所有定时任务
211
+ openclaw cron list
212
+
213
+ # 手动注册
97
214
  openclaw cron add \
98
215
  --name "skills-daily-report" \
99
216
  --cron "0 8 * * *" \
100
217
  --tz "Asia/Shanghai" \
101
218
  --session isolated \
102
219
  --message "请执行 /scan-report 并把结果发送到此渠道" \
103
- --announce \
104
- --channel telegram \
105
- --to "+8613312345678"
106
- ```
220
+ --announce
107
221
 
108
- 验证已注册:
222
+ # 删除定时任务
223
+ openclaw cron remove <job-id>
109
224
 
110
- ```bash
111
- openclaw cron list
225
+ # 修改执行时间(先删除再创建)
226
+ openclaw cron remove <job-id>
227
+ openclaw cron add --name "skills-daily-report" --cron "0 9 * * *" ...
112
228
  ```
113
229
 
114
- ---
230
+ ### 投递到特定渠道
115
231
 
116
- ## 聊天命令
232
+ 如果想将日报发送到特定渠道(如 Telegram):
117
233
 
118
- | 命令 | 说明 |
119
- |---|---|
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 注册命令 |
234
+ ```bash
235
+ openclaw cron add \
236
+ --name "skills-daily-report" \
237
+ --cron "0 8 * * *" \
238
+ --tz "Asia/Shanghai" \
239
+ --session isolated \
240
+ --message "请执行 /scan-report 并把结果发送到此渠道" \
241
+ --announce \
242
+ --channel telegram \
243
+ --to "+8613312345678"
244
+ ```
127
245
 
128
246
  ---
129
247
 
package/index.ts CHANGED
@@ -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
+ " /scan-status",
251
+ "",
252
+ "💬 或者在聊天中执行 /scan-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 "请执行 /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 "请执行 /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(
@@ -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 依赖...");
@@ -450,35 +754,291 @@ export default function register(api: any) {
450
754
  },
451
755
  });
452
756
 
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] 尝试自动注册...");
757
+ // ── 2. 智能 Cron 任务注册 ────────────────────────────────────────────────
758
+ await ensureCronJob(api.logger);
759
+ api.logger.info("[skills-scanner] ═══════════════════════════════════════");
760
+
761
+ // ── 3. /skills-scanner 主命令(命名空间)─────────────────────────────────
762
+ api.registerCommand({
763
+ name: "skills-scanner",
764
+ description: "Skills 安全扫描工具。用法: /skills-scanner <子命令> [参数]",
765
+ acceptsArgs: true,
766
+ requireAuth: true,
767
+ handler: async (ctx: any) => {
768
+ const args = (ctx.args ?? "").trim();
769
+
770
+ // 无参数时显示帮助
771
+ if (!args) {
772
+ return {
773
+ text: [
774
+ "🔍 *Skills Scanner - 安全扫描工具*",
775
+ "",
776
+ "可用命令:",
777
+ "• `/skills-scanner scan <路径> [选项]` - 扫描 Skill",
778
+ "• `/skills-scanner status` - 查看状态",
779
+ "• `/skills-scanner config [操作]` - 配置管理",
780
+ "• `/skills-scanner cron [操作]` - 定时任务管理",
781
+ "",
782
+ "扫描选项:",
783
+ "• `--detailed` - 显示详细发现",
784
+ "• `--behavioral` - 启用行为分析",
785
+ "• `--recursive` - 递归扫描子目录",
786
+ "• `--report` - 生成日报格式",
787
+ "",
788
+ "示例:",
789
+ "```",
790
+ "/skills-scanner scan ~/my-skill",
791
+ "/skills-scanner scan ~/skills --recursive",
792
+ "/skills-scanner scan ~/skills --report",
793
+ "/skills-scanner status",
794
+ "```",
795
+ "",
796
+ "💡 使用 `/skills-scanner help` 查看详细帮助"
797
+ ].join("\n")
798
+ };
799
+ }
800
+
801
+ const parts = args.split(/\s+/);
802
+ const subCommand = parts[0].toLowerCase();
803
+ const subArgs = parts.slice(1).join(" ");
804
+
805
+ // 路由到子命令
806
+ if (subCommand === "scan") {
807
+ return await handleScanCommand(subArgs);
808
+ } else if (subCommand === "status") {
809
+ return await handleStatusCommand();
810
+ } else if (subCommand === "config") {
811
+ return await handleConfigCommand(subArgs);
812
+ } else if (subCommand === "cron") {
813
+ return await handleCronCommand(subArgs);
814
+ } else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
815
+ return { text: getHelpText() };
816
+ } else {
817
+ return { text: `❌ 未知子命令: ${subCommand}\n\n使用 \`/skills-scanner help\` 查看帮助` };
818
+ }
819
+ },
820
+ });
821
+
822
+ // ── 子命令处理器 ──────────────────────────────────────────────────────────
823
+
824
+ async function handleScanCommand(args: string): Promise<any> {
825
+ if (!args) {
826
+ return { text: "用法:`/skills-scanner scan <路径> [--detailed] [--behavioral] [--recursive] [--report]`" };
827
+ }
458
828
 
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' });
829
+ if (!isVenvReady()) {
830
+ return { text: " Python 依赖尚未就绪,请稍后重试或查看日志" };
831
+ }
832
+
833
+ const parts = args.split(/\s+/);
834
+ const targetPath = expandPath(parts.find(p => !p.startsWith("--")) ?? "");
835
+ const detailed = parts.includes("--detailed");
836
+ const useBehav = parts.includes("--behavioral") || behavioral;
837
+ const recursive = parts.includes("--recursive");
838
+ const isReport = parts.includes("--report");
839
+
840
+ if (!targetPath) {
841
+ return { text: "❌ 请指定扫描路径" };
842
+ }
843
+
844
+ if (!existsSync(targetPath)) {
845
+ return { text: `❌ 路径不存在: ${targetPath}` };
846
+ }
847
+
848
+ // 判断是单个 Skill 还是批量扫描
849
+ const isSingleSkill = existsSync(join(targetPath, "SKILL.md"));
850
+
851
+ if (isReport) {
852
+ // 生成日报
853
+ if (scanDirs.length === 0) {
854
+ return { text: "⚠️ 未找到可扫描目录,请检查配置" };
855
+ }
856
+ const report = await buildDailyReport(scanDirs, useBehav, apiUrl, useLLM, policy, api.logger);
857
+ return { text: report };
858
+ } else if (isSingleSkill) {
859
+ // 单个 Skill 扫描
860
+ const res = await runScan("scan", targetPath, {
861
+ detailed,
862
+ behavioral: useBehav,
863
+ apiUrl,
864
+ useLLM,
865
+ policy
866
+ });
867
+ const icon = res.exitCode === 0 ? "✅" : "❌";
868
+ return { text: `${icon} 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
869
+ } else {
870
+ // 批量扫描
871
+ const res = await runScan("batch", targetPath, {
872
+ recursive,
873
+ detailed,
874
+ behavioral: useBehav,
875
+ apiUrl,
876
+ useLLM,
877
+ policy
878
+ });
879
+ const icon = res.exitCode === 0 ? "✅" : "❌";
880
+ return { text: `${icon} 批量扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
881
+ }
882
+ }
883
+
884
+ async function handleStatusCommand(): Promise<any> {
885
+ const state = loadState() as any;
886
+ const alerts: string[] = state.pendingAlerts ?? [];
887
+
888
+ const lines = [
889
+ "📋 *Skills Scanner 状态*",
890
+ `API 服务地址:${apiUrl}`,
891
+ `Python 依赖:${isVenvReady() ? "✅ 就绪" : "❌ 未就绪"}`,
892
+ `安装前扫描:${preInstallScan === "on" ? `✅ 监听中(${onUnsafe})` : "❌ 已禁用"}`,
893
+ `扫描策略:${policy}`,
894
+ `LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
895
+ `行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
896
+ `上次扫描:${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
897
+ `扫描目录:\n${scanDirs.map(d => ` • ${d}`).join("\n")}`,
898
+ ];
899
+
900
+ // API 健康检查
901
+ if (isVenvReady()) {
902
+ lines.push("", "🔍 *API 服务检查*");
903
+ try {
904
+ const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
905
+ const env = { ...process.env };
906
+ delete env.http_proxy;
907
+ delete env.https_proxy;
908
+ delete env.HTTP_PROXY;
909
+ delete env.HTTPS_PROXY;
910
+ delete env.all_proxy;
911
+ delete env.ALL_PROXY;
912
+
913
+ const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
914
+ const output = (stdout + stderr).trim();
915
+
916
+ if (output.includes("✓") || output.includes("正常")) {
917
+ lines.push(`API 服务:✅ 正常`);
918
+ const jsonMatch = output.match(/\{[\s\S]*\}/);
919
+ if (jsonMatch) {
920
+ try {
921
+ const healthData = JSON.parse(jsonMatch[0]);
922
+ if (healthData.analyzers_available) {
923
+ lines.push(`可用分析器:${healthData.analyzers_available.join(", ")}`);
924
+ }
925
+ } catch {}
926
+ }
927
+ } else {
928
+ lines.push(`API 服务:❌ 不可用`);
929
+ }
930
+ } catch (err: any) {
931
+ lines.push(`API 服务:❌ 连接失败`);
932
+ lines.push(`错误:无法连接到 ${apiUrl}`);
933
+ }
934
+ }
935
+
936
+ if (alerts.length > 0) {
937
+ lines.push("", `🔔 *待查告警(${alerts.length} 条):*`);
938
+ alerts.slice(-5).forEach(a => lines.push(` ${a}`));
939
+ saveState({ ...state, pendingAlerts: [] });
940
+ }
941
+
942
+ // 定时任务状态
943
+ lines.push("", "🕐 *定时任务*");
944
+ if (state.cronJobId && state.cronJobId !== 'manual-created') {
945
+ lines.push(`状态:✅ 已注册 (${state.cronJobId})`);
946
+ } else {
947
+ lines.push("状态:❌ 未注册");
948
+ lines.push("💡 使用 `/skills-scanner cron register` 注册");
949
+ }
950
+
951
+ return { text: lines.join("\n") };
952
+ }
953
+
954
+ async function handleConfigCommand(args: string): Promise<any> {
955
+ const action = args.trim().toLowerCase() || "show";
956
+
957
+ if (action === "show" || action === "") {
958
+ const configGuide = generateConfigGuide(cfg, apiUrl, scanDirs, behavioral, useLLM, policy, preInstallScan, onUnsafe);
959
+ return { text: "```\n" + configGuide + "\n```" };
960
+ } else if (action === "reset") {
961
+ const state = loadState() as any;
962
+ saveState({ ...state, configReviewed: false });
963
+ return { text: "✅ 配置审查标记已重置\n下次重启 Gateway 时将再次显示配置向导" };
964
+ } else {
965
+ return { text: "用法: `/skills-scanner config [show|reset]`" };
966
+ }
967
+ }
968
+
969
+ async function handleCronCommand(args: string): Promise<any> {
970
+ const action = args.trim().toLowerCase() || "status";
971
+ const state = loadState() as any;
972
+
973
+ if (action === "register") {
974
+ const oldJobId = state.cronJobId;
975
+ if (oldJobId && oldJobId !== 'manual-created') {
976
+ try {
977
+ execSync(`openclaw cron remove ${oldJobId}`, { encoding: 'utf-8', timeout: 5000 });
978
+ } catch {}
979
+ }
462
980
 
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}`);
981
+ saveState({ ...state, cronJobId: undefined });
982
+ await ensureCronJob(api.logger);
983
+
984
+ const newState = loadState() as any;
985
+ if (newState.cronJobId) {
986
+ return { text: `✅ 定时任务注册成功\n任务 ID: ${newState.cronJobId}` };
987
+ } else {
988
+ return { text: "❌ 定时任务注册失败,请查看日志" };
989
+ }
990
+ } else if (action === "unregister") {
991
+ if (!state.cronJobId) {
992
+ return { text: "⚠️ 未找到已注册的定时任务" };
993
+ }
994
+
995
+ try {
996
+ execSync(`openclaw cron remove ${state.cronJobId}`, { encoding: 'utf-8', timeout: 5000 });
997
+ saveState({ ...state, cronJobId: undefined });
998
+ return { text: `✅ 定时任务已删除: ${state.cronJobId}` };
999
+ } catch (err: any) {
1000
+ return { text: `❌ 删除失败: ${err.message}` };
1001
+ }
1002
+ } else {
1003
+ const lines = ["🕐 *定时任务状态*"];
1004
+ if (state.cronJobId && state.cronJobId !== 'manual-created') {
1005
+ lines.push(`任务 ID: ${state.cronJobId}`);
1006
+ lines.push(`执行时间: 每天 08:00 (Asia/Shanghai)`);
1007
+ lines.push("状态: ✅ 已注册");
469
1008
  } else {
470
- api.logger.info("[skills-scanner] Cron Job 注册命令已执行");
471
- api.logger.info("[skills-scanner] 输出: " + result.trim());
1009
+ lines.push("状态: 未注册");
1010
+ lines.push("", "💡 使用 `/skills-scanner cron register` 注册");
472
1011
  }
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}`);
1012
+ return { text: lines.join("\n") };
477
1013
  }
478
- } else {
479
- api.logger.info(`[skills-scanner] ✅ 日报 Cron Job 已存在: ${state.cronJobId}`);
480
1014
  }
481
- api.logger.info("[skills-scanner] ═══════════════════════════════════════");
1015
+
1016
+ function getHelpText(): string {
1017
+ return [
1018
+ "🔍 *Skills Scanner - 帮助文档*",
1019
+ "",
1020
+ "═══ 扫描命令 ═══",
1021
+ "`/skills-scanner scan <路径> [选项]`",
1022
+ "",
1023
+ "选项:",
1024
+ "• `--detailed` - 显示详细发现",
1025
+ "• `--behavioral` - 启用行为分析",
1026
+ "• `--recursive` - 递归扫描子目录",
1027
+ "• `--report` - 生成日报格式",
1028
+ "",
1029
+ "示例:",
1030
+ "```",
1031
+ "/skills-scanner scan ~/.openclaw/skills/my-skill",
1032
+ "/skills-scanner scan ~/.openclaw/skills --recursive",
1033
+ "/skills-scanner scan ~/.openclaw/skills --report",
1034
+ "```",
1035
+ "",
1036
+ "═══ 其他命令 ═══",
1037
+ "• `/skills-scanner status` - 查看状态",
1038
+ "• `/skills-scanner config [show|reset]` - 配置管理",
1039
+ "• `/skills-scanner cron [register|unregister|status]` - 定时任务",
1040
+ ].join("\n");
1041
+ }
482
1042
 
483
1043
  // ── 3. /scan-skill ────────────────────────────────────────────────────────
484
1044
  api.registerCommand({
@@ -649,23 +1209,146 @@ export default function register(api: any) {
649
1209
  saveState({ ...state, pendingAlerts: [] });
650
1210
  }
651
1211
 
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
- );
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
+
664
1238
  return { text: lines.join("\n") };
665
1239
  },
666
1240
  });
667
1241
 
668
- // ── 7. Gateway RPC ────────────────────────────────────────────────────────
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 ────────────────────────────────────────────────────────
669
1352
  api.registerGatewayMethod("skillsScanner.scan", async ({ respond, params }: any) => {
670
1353
  const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
671
1354
  if (!p) return respond(false, { error: "缺少 path 参数" });
@@ -688,7 +1371,7 @@ export default function register(api: any) {
688
1371
  respond(true, { report, state: loadState() });
689
1372
  });
690
1373
 
691
- // ── 8. CLI ────────────────────────────────────────────────────────────────
1374
+ // ── 9. CLI ────────────────────────────────────────────────────────────────
692
1375
  api.registerCli(({ program }: any) => {
693
1376
  const cmd = program.command("skills-scan").description("OpenClaw Skills 安全扫描");
694
1377
 
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.3.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"