@pwddd/skills-scanner 1.0.0-beta.2 → 1.0.0-beta.22
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.
- package/README.md +4 -255
- package/index.ts +13 -266
- package/openclaw.plugin.json +1 -11
- package/package.json +4 -4
- package/skills/skills-scanner/SKILL.md +15 -51
- package/src/api-client.ts +32 -2
- package/src/before-install-hook.ts +42 -9
- package/src/commands.ts +21 -98
- package/src/config-validator.ts +0 -16
- package/src/config.ts +8 -50
- package/src/cron-manager.ts +117 -169
- package/src/prompt-guidance.ts +8 -18
- package/src/state.ts +0 -17
- package/src/types.ts +0 -4
- package/src/high-risk-operation-guard.ts +0 -62
- package/src/prompt-injection-guard.ts +0 -56
- package/src/report.ts +0 -128
- package/src/watcher.ts +0 -178
package/README.md
CHANGED
|
@@ -1,255 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 功能特性
|
|
6
|
-
|
|
7
|
-
- 🔍 **自动扫描**: 监听 Skills 目录,自动扫描新安装的 Skill
|
|
8
|
-
- 🛡️ **安装前拦截**: 使用 before_install hook 在安装前强制拦截不安全的 Skills
|
|
9
|
-
- 🌐 **ClawHub 扫描**: 直接扫描 ClawHub 上的 Skill,无需手动下载
|
|
10
|
-
- 📊 **定时周报**: 每周一自动生成安全扫描报告
|
|
11
|
-
- 🛡️ **多种策略**: 支持 strict/balanced/permissive 三种扫描策略
|
|
12
|
-
- 🤖 **LLM 分析**: 可选的 LLM 语义分析
|
|
13
|
-
- 🔒 **自动隔离**: 检测到不安全的 Skill 自动隔离或删除
|
|
14
|
-
|
|
15
|
-
## 安装
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
# 从本地安装(开发)
|
|
19
|
-
openclaw plugins install ./skills-scanner
|
|
20
|
-
|
|
21
|
-
# 从 npm 安装(发布后)
|
|
22
|
-
openclaw plugins install @pwddd/skills-scanner
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## 配置
|
|
26
|
-
|
|
27
|
-
在 `~/.openclaw/openclaw.json` 或工作区配置中添加:
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"plugins": {
|
|
32
|
-
"entries": {
|
|
33
|
-
"skills-scanner": {
|
|
34
|
-
"enabled": true,
|
|
35
|
-
"config": {
|
|
36
|
-
"apiUrl": "https://110.vemic.com/skills-scanner",
|
|
37
|
-
"scanDirs": ["~/.openclaw/skills", "~/.openclaw/workspace/skills"],
|
|
38
|
-
"behavioral": false,
|
|
39
|
-
"useLLM": false,
|
|
40
|
-
"policy": "balanced",
|
|
41
|
-
"preInstallScan": "on",
|
|
42
|
-
"onUnsafe": "warn",
|
|
43
|
-
"enableBeforeInstallHook": true
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### 配置说明
|
|
52
|
-
|
|
53
|
-
- `apiUrl`: 扫描 API 服务地址
|
|
54
|
-
- `scanDirs`: 要监控的 Skills 目录列表
|
|
55
|
-
- `behavioral`: 是否启用行为分析(深度扫描,较慢)
|
|
56
|
-
- `useLLM`: 是否使用 LLM 进行语义分析
|
|
57
|
-
- `policy`: 扫描策略
|
|
58
|
-
- `strict`: 严格模式,发现任何可疑行为都标记为不安全
|
|
59
|
-
- `balanced`: 平衡模式(推荐)
|
|
60
|
-
- `permissive`: 宽松模式,只标记明确的威胁
|
|
61
|
-
- `preInstallScan`: 是否启用文件监控(安装后扫描)
|
|
62
|
-
- `on`: 启用
|
|
63
|
-
- `off`: 禁用
|
|
64
|
-
- `onUnsafe`: 发现不安全 Skill 的处理方式
|
|
65
|
-
- `warn`: 仅警告,不处理(推荐)
|
|
66
|
-
- `quarantine`: 移入隔离目录
|
|
67
|
-
- `delete`: 直接删除
|
|
68
|
-
- `enableBeforeInstallHook`: 是否启用 before_install hook(安装前拦截)
|
|
69
|
-
- `true`: 启用(强烈推荐)
|
|
70
|
-
- `false`: 禁用
|
|
71
|
-
- `scanTimeoutMs`: 扫描超时时间(毫秒),默认 180000 (3分钟)
|
|
72
|
-
- `reportDir`: 自定义报告目录
|
|
73
|
-
- `quarantineDir`: 自定义隔离目录
|
|
74
|
-
|
|
75
|
-
### 高级功能
|
|
76
|
-
|
|
77
|
-
#### 配置热重载
|
|
78
|
-
|
|
79
|
-
插件支持配置热重载,修改配置后无需重启 Gateway:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
# 修改配置文件
|
|
83
|
-
vim ~/.openclaw/config.json
|
|
84
|
-
|
|
85
|
-
# 重载配置
|
|
86
|
-
openclaw config reload
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
配置变更会自动:
|
|
90
|
-
- 更新 API URL
|
|
91
|
-
- 重启文件监控器(如果扫描目录变更)
|
|
92
|
-
- 应用新的扫描策略
|
|
93
|
-
|
|
94
|
-
#### 调试模式
|
|
95
|
-
|
|
96
|
-
启用调试模式可以查看详细的运行日志:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
# 启用调试模式
|
|
100
|
-
export SKILLS_SCANNER_DEBUG=1
|
|
101
|
-
openclaw gateway start
|
|
102
|
-
|
|
103
|
-
# 或者一次性启用
|
|
104
|
-
SKILLS_SCANNER_DEBUG=1 openclaw gateway start
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
调试模式会输出:
|
|
108
|
-
- 完整的配置信息
|
|
109
|
-
- 详细的扫描过程
|
|
110
|
-
- API 请求和响应
|
|
111
|
-
- 文件监控事件
|
|
112
|
-
|
|
113
|
-
#### 健康检查端点
|
|
114
|
-
|
|
115
|
-
插件提供 HTTP 健康检查端点:
|
|
116
|
-
|
|
117
|
-
```bash
|
|
118
|
-
# 通过 HTTP 访问
|
|
119
|
-
curl http://localhost:3000/health/skills-scanner
|
|
120
|
-
|
|
121
|
-
# 返回示例
|
|
122
|
-
{
|
|
123
|
-
"status": "healthy",
|
|
124
|
-
"plugin": {
|
|
125
|
-
"version": "1.0.0",
|
|
126
|
-
"id": "skills-scanner",
|
|
127
|
-
"name": "Skills Scanner"
|
|
128
|
-
},
|
|
129
|
-
"api": {
|
|
130
|
-
"url": "https://110.vemic.com/skills-scanner",
|
|
131
|
-
"status": "available"
|
|
132
|
-
},
|
|
133
|
-
"watcher": {
|
|
134
|
-
"enabled": true,
|
|
135
|
-
"running": true,
|
|
136
|
-
"directories": 2
|
|
137
|
-
},
|
|
138
|
-
"metrics": {
|
|
139
|
-
"totalScans": 42,
|
|
140
|
-
"successRate": "95.24%",
|
|
141
|
-
"averageDurationMs": 1234
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## 使用方法
|
|
147
|
-
|
|
148
|
-
### 聊天命令
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
/skills-scanner scan <路径> [选项] # 扫描 Skill
|
|
152
|
-
/skills-scanner scan clawhub <URL> [选项] # 扫描 ClawHub Skill
|
|
153
|
-
/skills-scanner health # 健康检查
|
|
154
|
-
/skills-scanner config [操作] # 配置管理
|
|
155
|
-
/skills-scanner cron [操作] # 定时任务管理
|
|
156
|
-
/skills-scanner help # 帮助信息
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
#### 扫描选项
|
|
160
|
-
|
|
161
|
-
- `--detailed`: 显示详细的安全发现
|
|
162
|
-
- `--behavioral`: 启用行为分析
|
|
163
|
-
- `--recursive`: 递归扫描子目录
|
|
164
|
-
- `--report`: 生成日报格式
|
|
165
|
-
|
|
166
|
-
#### 示例
|
|
167
|
-
|
|
168
|
-
```
|
|
169
|
-
/skills-scanner scan ~/.openclaw/skills/my-skill
|
|
170
|
-
/skills-scanner scan ~/.openclaw/skills --recursive
|
|
171
|
-
/skills-scanner scan ~/.openclaw/skills --report
|
|
172
|
-
/skills-scanner scan clawhub https://clawhub.ai/username/project
|
|
173
|
-
/skills-scanner scan clawhub https://clawhub.ai/Asleep123/caldav-calendar --detailed
|
|
174
|
-
/skills-scanner health
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### CLI 命令
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
# 扫描单个 Skill
|
|
181
|
-
openclaw skills-scanner scan <path> [--detailed] [--behavioral]
|
|
182
|
-
|
|
183
|
-
# 扫描 ClawHub Skill
|
|
184
|
-
openclaw skills-scanner clawhub <url> [--detailed] [--behavioral]
|
|
185
|
-
|
|
186
|
-
# 批量扫描目录
|
|
187
|
-
openclaw skills-scanner batch <directory> [--recursive] [--detailed]
|
|
188
|
-
|
|
189
|
-
# 生成日报
|
|
190
|
-
openclaw skills-scanner report
|
|
191
|
-
|
|
192
|
-
# 检查 API 服务健康状态
|
|
193
|
-
openclaw skills-scanner health
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
## 工作流程
|
|
197
|
-
|
|
198
|
-
1. **插件启动**: 自动初始化并连接 API 服务
|
|
199
|
-
2. **文件监控**: 监听配置的 Skills 目录
|
|
200
|
-
3. **自动扫描**: 检测到新 Skill 时自动触发扫描
|
|
201
|
-
4. **结果处理**: 根据配置隔离/删除/警告不安全的 Skill
|
|
202
|
-
5. **定时周报**: 每周一 12:05 自动生成安全报告
|
|
203
|
-
|
|
204
|
-
## 故障排除
|
|
205
|
-
|
|
206
|
-
### API 服务连接失败
|
|
207
|
-
|
|
208
|
-
1. 确保 API 服务地址配置正确
|
|
209
|
-
2. 运行健康检查:`/skills-scanner health`
|
|
210
|
-
3. 检查网络连接
|
|
211
|
-
|
|
212
|
-
### 定时任务未注册
|
|
213
|
-
|
|
214
|
-
定时任务会在插件启动时自动注册。如果需要手动注册:
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
# 手动注册定时任务
|
|
218
|
-
/skills-scanner cron setup
|
|
219
|
-
|
|
220
|
-
# 或使用 CLI
|
|
221
|
-
openclaw cron add \
|
|
222
|
-
--name "skills-weekly-report" \
|
|
223
|
-
--cron "5 12 * * 1" \
|
|
224
|
-
--tz "Asia/Shanghai" \
|
|
225
|
-
--session isolated \
|
|
226
|
-
--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
|
|
227
|
-
--announce
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## 开发
|
|
231
|
-
|
|
232
|
-
### 目录结构
|
|
233
|
-
|
|
234
|
-
```
|
|
235
|
-
extensions/skills-scanner/
|
|
236
|
-
├── package.json # npm 包配置
|
|
237
|
-
├── openclaw.plugin.json # 插件元数据
|
|
238
|
-
├── README.md # 文档
|
|
239
|
-
├── index.ts # 插件入口
|
|
240
|
-
├── src/ # 源代码
|
|
241
|
-
│ ├── api-client.ts # HTTP API 客户端
|
|
242
|
-
│ ├── config.ts # 配置管理
|
|
243
|
-
│ ├── scanner.ts # 扫描逻辑
|
|
244
|
-
│ ├── watcher.ts # 文件监控
|
|
245
|
-
│ ├── cron.ts # 定时任务
|
|
246
|
-
│ ├── commands.ts # 命令处理
|
|
247
|
-
│ └── types.ts # 类型定义
|
|
248
|
-
└── skills/
|
|
249
|
-
└── skills-scanner/
|
|
250
|
-
└── SKILL.md # Skill 文档
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## 许可证
|
|
254
|
-
|
|
255
|
-
MIT
|
|
1
|
+
# deprecated
|
|
2
|
+
|
|
3
|
+
# 废弃
|
|
4
|
+
该插件已被弃用,建议使用其他插件替代
|
package/index.ts
CHANGED
|
@@ -6,27 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
9
|
import type { ScannerConfig } from "./src/types.js";
|
|
12
10
|
import { skillsScannerConfigSchema, generateConfigGuide } from "./src/config.js";
|
|
13
11
|
import {
|
|
14
12
|
loadState,
|
|
15
13
|
saveState,
|
|
16
14
|
expandPath,
|
|
17
|
-
defaultScanDirs,
|
|
18
15
|
isFirstRun,
|
|
19
16
|
markConfigReviewed,
|
|
20
17
|
getStateDir,
|
|
21
18
|
} from "./src/state.js";
|
|
22
19
|
import { runScan } from "./src/scanner.js";
|
|
23
|
-
import {
|
|
24
|
-
import { ensureCronJobViaGateway, checkCronJobStatus } from "./src/cron-manager.js";
|
|
25
|
-
import { startWatcher } from "./src/watcher.js";
|
|
20
|
+
import { ensureCronJob } from "./src/cron-manager.js";
|
|
26
21
|
import { createCommandHandlers } from "./src/commands.js";
|
|
27
22
|
import { SKILLS_SECURITY_GUIDANCE } from "./src/prompt-guidance.js";
|
|
28
|
-
import { PROMPT_INJECTION_GUARD } from "./src/prompt-injection-guard.js";
|
|
29
|
-
import { HIGH_RISK_OPERATION_GUARD } from "./src/high-risk-operation-guard.js";
|
|
30
23
|
import { handleBeforeInstall } from "./src/before-install-hook.js";
|
|
31
24
|
import type { BeforeInstallEvent } from "./src/before-install-hook.js";
|
|
32
25
|
import { validateConfig } from "./src/config-validator.js";
|
|
@@ -57,24 +50,16 @@ export default definePluginEntry({
|
|
|
57
50
|
}
|
|
58
51
|
|
|
59
52
|
const apiUrl = cfg.apiUrl ?? "https://110.vemic.com/skills-scanner";
|
|
60
|
-
const scanDirs =
|
|
61
|
-
(cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
|
|
62
|
-
? cfg.scanDirs!.map(expandPath)
|
|
63
|
-
: defaultScanDirs();
|
|
64
53
|
const behavioral = cfg.behavioral ?? false;
|
|
65
54
|
const useLLM = cfg.useLLM ?? false;
|
|
66
55
|
const policy = cfg.policy ?? "balanced";
|
|
67
|
-
const preInstallScan = cfg.preInstallScan ?? "on";
|
|
68
56
|
const onUnsafe = cfg.onUnsafe ?? "warn";
|
|
69
57
|
const injectSecurityGuidance = cfg.injectSecurityGuidance ?? true;
|
|
70
|
-
const enablePromptInjectionGuard = cfg.enablePromptInjectionGuard ?? false;
|
|
71
|
-
const enableHighRiskOperationGuard = cfg.enableHighRiskOperationGuard ?? false;
|
|
72
58
|
const enableBeforeInstallHook = cfg.enableBeforeInstallHook ?? true;
|
|
73
59
|
|
|
74
60
|
api.logger.info("[skills-scanner] ═══════════════════════════════════════");
|
|
75
61
|
api.logger.info("[skills-scanner] Plugin loading...");
|
|
76
62
|
api.logger.info(`[skills-scanner] API URL: ${apiUrl}`);
|
|
77
|
-
api.logger.info(`[skills-scanner] Scan directories: ${scanDirs.join(", ")}`);
|
|
78
63
|
api.logger.info(`[skills-scanner] Before-install hook: ${enableBeforeInstallHook ? "✅ ENABLED" : "❌ DISABLED"}`);
|
|
79
64
|
|
|
80
65
|
if (isDebugMode()) {
|
|
@@ -84,30 +69,11 @@ export default definePluginEntry({
|
|
|
84
69
|
|
|
85
70
|
// Inject system prompt guidance (can be disabled via config)
|
|
86
71
|
if (injectSecurityGuidance) {
|
|
87
|
-
// Build combined guidance
|
|
88
|
-
const guidanceParts = [SKILLS_SECURITY_GUIDANCE];
|
|
89
|
-
|
|
90
|
-
if (enablePromptInjectionGuard) {
|
|
91
|
-
guidanceParts.push(PROMPT_INJECTION_GUARD);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (enableHighRiskOperationGuard) {
|
|
95
|
-
guidanceParts.push(HIGH_RISK_OPERATION_GUARD);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const combinedGuidance = guidanceParts.join("\n\n");
|
|
99
|
-
|
|
100
72
|
api.on("before_prompt_build", async () => ({
|
|
101
|
-
prependSystemContext:
|
|
73
|
+
prependSystemContext: SKILLS_SECURITY_GUIDANCE,
|
|
102
74
|
}));
|
|
103
75
|
|
|
104
76
|
api.logger.info("[skills-scanner] ✅ Security guidance injected into system prompt");
|
|
105
|
-
if (enablePromptInjectionGuard) {
|
|
106
|
-
api.logger.info("[skills-scanner] - Prompt injection guard enabled");
|
|
107
|
-
}
|
|
108
|
-
if (enableHighRiskOperationGuard) {
|
|
109
|
-
api.logger.info("[skills-scanner] - High-risk operation guard enabled");
|
|
110
|
-
}
|
|
111
77
|
} else {
|
|
112
78
|
api.logger.info("[skills-scanner] ⏭️ Security guidance injection disabled");
|
|
113
79
|
}
|
|
@@ -129,7 +95,9 @@ export default definePluginEntry({
|
|
|
129
95
|
api.logger.error("[skills-scanner] ❌ before_install hook error", {
|
|
130
96
|
error: err.message,
|
|
131
97
|
stack: err.stack,
|
|
132
|
-
|
|
98
|
+
sourcePath: event.sourcePath,
|
|
99
|
+
targetType: event.targetType,
|
|
100
|
+
targetName: event.targetName,
|
|
133
101
|
});
|
|
134
102
|
// Return safe default on error - allow installation but log the failure
|
|
135
103
|
return { block: false };
|
|
@@ -141,80 +109,20 @@ export default definePluginEntry({
|
|
|
141
109
|
api.logger.warn("[skills-scanner] ⚠️ before_install hook DISABLED - installations will NOT be intercepted!");
|
|
142
110
|
}
|
|
143
111
|
|
|
144
|
-
//
|
|
145
|
-
|
|
112
|
+
// 在插件启动时设置系统 crontab 清理任务
|
|
113
|
+
(async () => {
|
|
146
114
|
try {
|
|
147
|
-
|
|
148
|
-
await ensureCronJobViaGateway({
|
|
115
|
+
await ensureCronJob({
|
|
149
116
|
logger: api.logger,
|
|
150
|
-
|
|
151
|
-
return await api.callGateway(method, params);
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
api.logger.info("[skills-scanner] ✅ Cron job check completed");
|
|
155
|
-
} catch (err: any) {
|
|
156
|
-
api.logger.error("[skills-scanner] ❌ Cron job registration failed", {
|
|
157
|
-
error: err.message,
|
|
158
|
-
stack: err.stack,
|
|
117
|
+
config: api.config,
|
|
159
118
|
});
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Register plugin_uninstall hook for cleanup
|
|
165
|
-
api.on("plugin_uninstall", async () => {
|
|
166
|
-
api.logger.info("[skills-scanner] 🗑️ Plugin uninstalling, cleaning up...");
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
// 1. Stop file watcher
|
|
170
|
-
if (stopWatcher) {
|
|
171
|
-
api.logger.debug("[skills-scanner] Stopping file watcher...");
|
|
172
|
-
stopWatcher();
|
|
173
|
-
stopWatcher = null;
|
|
174
|
-
api.logger.debug("[skills-scanner] ✅ File watcher stopped");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// 2. Remove cron jobs
|
|
178
|
-
try {
|
|
179
|
-
const listResult = await api.callGateway("cron.list", {});
|
|
180
|
-
const jobs = listResult?.jobs || [];
|
|
181
|
-
const ourJobs = jobs.filter((j: any) => j.name === "skills-weekly-report");
|
|
182
|
-
|
|
183
|
-
for (const job of ourJobs) {
|
|
184
|
-
const jobId = job.jobId || job.id;
|
|
185
|
-
await api.callGateway("cron.remove", { jobId });
|
|
186
|
-
api.logger.info("[skills-scanner] Removed cron job", { jobId });
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (ourJobs.length > 0) {
|
|
190
|
-
api.logger.info(`[skills-scanner] ✅ Removed ${ourJobs.length} cron job(s)`);
|
|
191
|
-
}
|
|
192
|
-
} catch (err: any) {
|
|
193
|
-
api.logger.warn("[skills-scanner] Failed to remove cron jobs", {
|
|
194
|
-
error: err.message,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 3. Save final state
|
|
199
|
-
try {
|
|
200
|
-
const finalState = loadState(api.runtime);
|
|
201
|
-
finalState.lastUninstallAt = new Date().toISOString();
|
|
202
|
-
saveState(finalState, api.runtime);
|
|
203
|
-
api.logger.debug("[skills-scanner] ✅ Final state saved");
|
|
204
|
-
} catch (err: any) {
|
|
205
|
-
api.logger.warn("[skills-scanner] Failed to save final state", {
|
|
206
|
-
error: err.message,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
api.logger.info("[skills-scanner] ✅ Cleanup completed successfully");
|
|
119
|
+
api.logger.info("[skills-scanner] ✅ System crontab cleanup task ensured");
|
|
211
120
|
} catch (err: any) {
|
|
212
|
-
api.logger.
|
|
121
|
+
api.logger.warn("[skills-scanner] ⚠️ Failed to setup system crontab", {
|
|
213
122
|
error: err.message,
|
|
214
|
-
stack: err.stack,
|
|
215
123
|
});
|
|
216
124
|
}
|
|
217
|
-
});
|
|
125
|
+
})();
|
|
218
126
|
|
|
219
127
|
// Register config_changed hook for hot reload
|
|
220
128
|
api.on("config_changed", async (newConfig: any) => {
|
|
@@ -233,8 +141,6 @@ export default definePluginEntry({
|
|
|
233
141
|
|
|
234
142
|
// Check what changed
|
|
235
143
|
const apiUrlChanged = newCfg.apiUrl !== cfg.apiUrl;
|
|
236
|
-
const scanDirsChanged = JSON.stringify(newCfg.scanDirs) !== JSON.stringify(cfg.scanDirs);
|
|
237
|
-
const preInstallScanChanged = newCfg.preInstallScan !== cfg.preInstallScan;
|
|
238
144
|
|
|
239
145
|
if (apiUrlChanged) {
|
|
240
146
|
api.logger.info("[skills-scanner] API URL updated", {
|
|
@@ -245,42 +151,6 @@ export default definePluginEntry({
|
|
|
245
151
|
Object.assign(cfg, { apiUrl: newCfg.apiUrl });
|
|
246
152
|
}
|
|
247
153
|
|
|
248
|
-
if (scanDirsChanged || preInstallScanChanged) {
|
|
249
|
-
api.logger.info("[skills-scanner] Scan configuration updated, restarting watcher...");
|
|
250
|
-
|
|
251
|
-
// Stop old watcher
|
|
252
|
-
if (stopWatcher) {
|
|
253
|
-
stopWatcher();
|
|
254
|
-
stopWatcher = null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Start new watcher with updated config
|
|
258
|
-
const newScanDirs =
|
|
259
|
-
(newCfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
|
|
260
|
-
? newCfg.scanDirs!.map(expandPath)
|
|
261
|
-
: defaultScanDirs();
|
|
262
|
-
|
|
263
|
-
if (newCfg.preInstallScan === "on" && newScanDirs.length > 0) {
|
|
264
|
-
stopWatcher = startWatcher(
|
|
265
|
-
newScanDirs,
|
|
266
|
-
newCfg.onUnsafe ?? "warn",
|
|
267
|
-
newCfg.behavioral ?? false,
|
|
268
|
-
newCfg.apiUrl ?? apiUrl,
|
|
269
|
-
newCfg.useLLM ?? false,
|
|
270
|
-
newCfg.policy ?? "balanced",
|
|
271
|
-
persistWatcherAlert,
|
|
272
|
-
api.logger,
|
|
273
|
-
QUARANTINE_DIR
|
|
274
|
-
);
|
|
275
|
-
api.logger.info("[skills-scanner] ✅ Watcher restarted with new configuration");
|
|
276
|
-
} else {
|
|
277
|
-
api.logger.info("[skills-scanner] ⏭️ Watcher disabled by new configuration");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Update global config
|
|
281
|
-
Object.assign(cfg, newCfg);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
154
|
api.logger.info("[skills-scanner] ✅ Configuration reload completed");
|
|
285
155
|
} catch (err: any) {
|
|
286
156
|
api.logger.error("[skills-scanner] ❌ Configuration reload failed", {
|
|
@@ -297,93 +167,15 @@ export default definePluginEntry({
|
|
|
297
167
|
const configGuide = generateConfigGuide(
|
|
298
168
|
cfg,
|
|
299
169
|
apiUrl,
|
|
300
|
-
scanDirs,
|
|
301
170
|
behavioral,
|
|
302
171
|
useLLM,
|
|
303
172
|
policy,
|
|
304
|
-
preInstallScan,
|
|
305
173
|
onUnsafe
|
|
306
174
|
);
|
|
307
175
|
console.log(configGuide);
|
|
308
176
|
markConfigReviewed(api.runtime);
|
|
309
177
|
}
|
|
310
178
|
|
|
311
|
-
// Helper for watcher alerts
|
|
312
|
-
function persistWatcherAlert(msg: string): void {
|
|
313
|
-
const state = loadState(api.runtime);
|
|
314
|
-
const alerts: string[] = (state as any).pendingAlerts ?? [];
|
|
315
|
-
alerts.push(`[${new Date().toLocaleString("en-US")}] ${msg}`);
|
|
316
|
-
saveState({ ...state, pendingAlerts: alerts } as any, api.runtime);
|
|
317
|
-
api.logger.warn(`[skills-scanner] ${msg}`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Service: start watcher
|
|
321
|
-
let stopWatcher: (() => void) | null = null;
|
|
322
|
-
|
|
323
|
-
api.registerService({
|
|
324
|
-
id: "skills-scanner-setup",
|
|
325
|
-
start: async () => {
|
|
326
|
-
api.logger.info("[skills-scanner] 🚀 Service starting...");
|
|
327
|
-
|
|
328
|
-
if (preInstallScan === "on" && scanDirs.length > 0) {
|
|
329
|
-
api.logger.info(`[skills-scanner] 📁 Starting file monitoring: ${scanDirs.length} directories`);
|
|
330
|
-
stopWatcher = startWatcher(
|
|
331
|
-
scanDirs,
|
|
332
|
-
onUnsafe,
|
|
333
|
-
behavioral,
|
|
334
|
-
apiUrl,
|
|
335
|
-
useLLM,
|
|
336
|
-
policy,
|
|
337
|
-
persistWatcherAlert,
|
|
338
|
-
api.logger,
|
|
339
|
-
QUARANTINE_DIR
|
|
340
|
-
);
|
|
341
|
-
api.logger.info("[skills-scanner] ✅ File monitoring started");
|
|
342
|
-
} else {
|
|
343
|
-
api.logger.info("[skills-scanner] ⏭️ Pre-install scan disabled");
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
stop: async () => {
|
|
347
|
-
api.logger.info("[skills-scanner] 🛑 Service stopping...");
|
|
348
|
-
|
|
349
|
-
try {
|
|
350
|
-
// 1. Stop file watcher
|
|
351
|
-
if (stopWatcher) {
|
|
352
|
-
api.logger.debug("[skills-scanner] Stopping file watcher...");
|
|
353
|
-
stopWatcher();
|
|
354
|
-
stopWatcher = null;
|
|
355
|
-
api.logger.debug("[skills-scanner] ✅ File watcher stopped");
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// 2. Save final state
|
|
359
|
-
try {
|
|
360
|
-
const finalState = loadState(api.runtime);
|
|
361
|
-
finalState.lastShutdownAt = new Date().toISOString();
|
|
362
|
-
saveState(finalState, api.runtime);
|
|
363
|
-
api.logger.debug("[skills-scanner] ✅ Final state saved");
|
|
364
|
-
} catch (err: any) {
|
|
365
|
-
api.logger.warn("[skills-scanner] Failed to save final state", {
|
|
366
|
-
error: err.message,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// 3. Clear pending alerts (optional - keep for next run)
|
|
371
|
-
// This is intentionally commented out to preserve alerts
|
|
372
|
-
// const state = loadState(api.runtime);
|
|
373
|
-
// if ((state as any).pendingAlerts?.length > 0) {
|
|
374
|
-
// saveState({ ...state, pendingAlerts: [] } as any, api.runtime);
|
|
375
|
-
// }
|
|
376
|
-
|
|
377
|
-
api.logger.info("[skills-scanner] ✅ Service stopped cleanly");
|
|
378
|
-
} catch (err: any) {
|
|
379
|
-
api.logger.error("[skills-scanner] ❌ Error during shutdown", {
|
|
380
|
-
error: err.message,
|
|
381
|
-
stack: err.stack,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
},
|
|
385
|
-
});
|
|
386
|
-
|
|
387
179
|
// Health check endpoint
|
|
388
180
|
api.registerHttpRoute({
|
|
389
181
|
method: "GET",
|
|
@@ -417,11 +209,6 @@ export default definePluginEntry({
|
|
|
417
209
|
url: apiUrl,
|
|
418
210
|
status: apiStatus,
|
|
419
211
|
},
|
|
420
|
-
watcher: {
|
|
421
|
-
enabled: preInstallScan === "on",
|
|
422
|
-
running: stopWatcher !== null,
|
|
423
|
-
directories: scanDirs.length,
|
|
424
|
-
},
|
|
425
212
|
state: {
|
|
426
213
|
lastScanAt: state.lastScanAt || null,
|
|
427
214
|
lastShutdownAt: state.lastShutdownAt || null,
|
|
@@ -461,16 +248,12 @@ export default definePluginEntry({
|
|
|
461
248
|
const handlers = createCommandHandlers(
|
|
462
249
|
cfg,
|
|
463
250
|
apiUrl,
|
|
464
|
-
scanDirs,
|
|
465
251
|
behavioral,
|
|
466
252
|
useLLM,
|
|
467
253
|
policy,
|
|
468
|
-
preInstallScan,
|
|
469
254
|
onUnsafe,
|
|
470
255
|
api.logger,
|
|
471
|
-
|
|
472
|
-
return await api.callGateway(method, params);
|
|
473
|
-
}
|
|
256
|
+
api.config
|
|
474
257
|
);
|
|
475
258
|
|
|
476
259
|
// Chat command: /skills-scanner
|
|
@@ -492,13 +275,11 @@ export default definePluginEntry({
|
|
|
492
275
|
"• `/skills-scanner scan clawhub <URL> [选项]` - 扫描 ClawHub Skill",
|
|
493
276
|
"• `/skills-scanner health` - 健康检查",
|
|
494
277
|
"• `/skills-scanner config [操作]` - 配置管理",
|
|
495
|
-
"• `/skills-scanner cron [操作]` - 定时任务管理",
|
|
496
278
|
"",
|
|
497
279
|
"扫描选项:",
|
|
498
280
|
"• `--detailed` - 显示详细发现",
|
|
499
281
|
"• `--behavioral` - 启用行为分析",
|
|
500
282
|
"• `--recursive` - 递归扫描子目录",
|
|
501
|
-
"• `--report` - 生成日报格式",
|
|
502
283
|
"",
|
|
503
284
|
"示例:",
|
|
504
285
|
"```",
|
|
@@ -523,8 +304,6 @@ export default definePluginEntry({
|
|
|
523
304
|
return await handlers.handleHealthCommand();
|
|
524
305
|
} else if (subCommand === "config") {
|
|
525
306
|
return await handlers.handleConfigCommand(subArgs);
|
|
526
|
-
} else if (subCommand === "cron") {
|
|
527
|
-
return await handlers.handleCronCommand(subArgs);
|
|
528
307
|
} else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
|
|
529
308
|
return { text: handlers.getHelpText() };
|
|
530
309
|
} else {
|
|
@@ -560,23 +339,6 @@ export default definePluginEntry({
|
|
|
560
339
|
}
|
|
561
340
|
});
|
|
562
341
|
|
|
563
|
-
api.registerGatewayMethod("skillsScanner.report", async ({ respond }: any) => {
|
|
564
|
-
if (scanDirs.length === 0) return respond(false, { error: "No scan directories found" });
|
|
565
|
-
try {
|
|
566
|
-
const report = await buildDailyReport(
|
|
567
|
-
scanDirs,
|
|
568
|
-
behavioral,
|
|
569
|
-
apiUrl,
|
|
570
|
-
useLLM,
|
|
571
|
-
policy,
|
|
572
|
-
api.logger
|
|
573
|
-
);
|
|
574
|
-
respond(true, { report, state: loadState() });
|
|
575
|
-
} catch (err: any) {
|
|
576
|
-
respond(false, { error: err.message });
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
|
|
580
342
|
// CLI commands
|
|
581
343
|
api.registerCli(
|
|
582
344
|
({ program }: any) => {
|
|
@@ -615,21 +377,6 @@ export default definePluginEntry({
|
|
|
615
377
|
process.exit(res.exitCode);
|
|
616
378
|
});
|
|
617
379
|
|
|
618
|
-
cmd
|
|
619
|
-
.command("report")
|
|
620
|
-
.description("生成日报")
|
|
621
|
-
.action(async () => {
|
|
622
|
-
const report = await buildDailyReport(
|
|
623
|
-
scanDirs,
|
|
624
|
-
behavioral,
|
|
625
|
-
apiUrl,
|
|
626
|
-
useLLM,
|
|
627
|
-
policy,
|
|
628
|
-
console
|
|
629
|
-
);
|
|
630
|
-
console.log(report);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
380
|
cmd
|
|
634
381
|
.command("health")
|
|
635
382
|
.description("检查 API 服务健康状态")
|