@pwddd/skills-scanner 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -0
- package/index.ts +647 -0
- package/openclaw.plugin.json +122 -0
- package/package.json +64 -0
- package/skills/skills-scanner/SKILL.md +281 -0
- package/src/api-client.ts +245 -0
- package/src/before-install-hook.ts +241 -0
- package/src/cache.ts +138 -0
- package/src/commands.ts +289 -0
- package/src/config-validator.ts +110 -0
- package/src/config.ts +230 -0
- package/src/cron-manager.ts +210 -0
- package/src/debug.ts +40 -0
- package/src/error-handler.ts +103 -0
- package/src/high-risk-operation-guard.ts +62 -0
- package/src/metrics.ts +140 -0
- package/src/prompt-guidance.ts +80 -0
- package/src/prompt-injection-guard.ts +56 -0
- package/src/rate-limiter.ts +102 -0
- package/src/report.ts +128 -0
- package/src/scanner.ts +230 -0
- package/src/state.ts +136 -0
- package/src/structured-logger.ts +97 -0
- package/src/types.ts +76 -0
- package/src/watcher.ts +178 -0
package/src/commands.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command handlers module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join, basename } from "node:path";
|
|
7
|
+
import { runScan } from "./scanner.js";
|
|
8
|
+
import { buildDailyReport } from "./report.js";
|
|
9
|
+
import { loadState, saveState, expandPath } from "./state.js";
|
|
10
|
+
import { generateConfigGuide } from "./config.js";
|
|
11
|
+
import { ensureCronJobViaGateway, checkCronJobStatus } from "./cron-manager.js";
|
|
12
|
+
import type { ScannerConfig, CommandResponse, PluginLogger } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export function createCommandHandlers(
|
|
15
|
+
cfg: ScannerConfig,
|
|
16
|
+
apiUrl: string,
|
|
17
|
+
scanDirs: string[],
|
|
18
|
+
behavioral: boolean,
|
|
19
|
+
useLLM: boolean,
|
|
20
|
+
policy: string,
|
|
21
|
+
preInstallScan: string,
|
|
22
|
+
onUnsafe: string,
|
|
23
|
+
logger: PluginLogger,
|
|
24
|
+
callGateway: (method: string, params: any) => Promise<any>
|
|
25
|
+
) {
|
|
26
|
+
async function handleScanCommand(args: string): Promise<CommandResponse> {
|
|
27
|
+
if (!args) {
|
|
28
|
+
return {
|
|
29
|
+
text: "用法:`/skills-scanner scan <路径> [--detailed] [--behavioral] [--recursive] [--report]`\n或:`/skills-scanner scan clawhub <URL> [--detailed] [--behavioral]`",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parts = args.split(/\s+/);
|
|
34
|
+
|
|
35
|
+
// Check if this is a ClawHub scan
|
|
36
|
+
if (parts[0] === "clawhub") {
|
|
37
|
+
const clawhubUrl = parts.find((p) => p.startsWith("https://clawhub.ai/"));
|
|
38
|
+
if (!clawhubUrl) {
|
|
39
|
+
return { text: "⚠️ 请提供有效的 ClawHub URL (例如:https://clawhub.ai/username/project)" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const detailed = parts.includes("--detailed");
|
|
43
|
+
const useBehav = parts.includes("--behavioral") || behavioral;
|
|
44
|
+
|
|
45
|
+
const res = await runScan("clawhub", clawhubUrl, {
|
|
46
|
+
detailed,
|
|
47
|
+
behavioral: useBehav,
|
|
48
|
+
apiUrl,
|
|
49
|
+
useLLM,
|
|
50
|
+
policy,
|
|
51
|
+
});
|
|
52
|
+
const icon = res.exitCode === 0 ? "✅" : "❌";
|
|
53
|
+
return { text: `${icon} ClawHub 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const targetPath = expandPath(parts.find((p) => !p.startsWith("--")) ?? "");
|
|
57
|
+
const detailed = parts.includes("--detailed");
|
|
58
|
+
const useBehav = parts.includes("--behavioral") || behavioral;
|
|
59
|
+
const recursive = parts.includes("--recursive");
|
|
60
|
+
const isReport = parts.includes("--report");
|
|
61
|
+
|
|
62
|
+
// Report mode: use configured scanDirs
|
|
63
|
+
if (isReport) {
|
|
64
|
+
if (scanDirs.length === 0) {
|
|
65
|
+
return { text: "⚠️ 未找到可扫描目录,请检查配置" };
|
|
66
|
+
}
|
|
67
|
+
const report = await buildDailyReport(
|
|
68
|
+
scanDirs,
|
|
69
|
+
useBehav,
|
|
70
|
+
apiUrl,
|
|
71
|
+
useLLM,
|
|
72
|
+
policy,
|
|
73
|
+
logger
|
|
74
|
+
);
|
|
75
|
+
return { text: report };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Regular scan mode: require path
|
|
79
|
+
if (!targetPath) {
|
|
80
|
+
return { text: "⚠️ 请指定扫描路径" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!existsSync(targetPath)) {
|
|
84
|
+
return { text: `⚠️ 路径不存在:${targetPath}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isSingleSkill = existsSync(join(targetPath, "SKILL.md"));
|
|
88
|
+
|
|
89
|
+
if (isSingleSkill) {
|
|
90
|
+
const res = await runScan("scan", targetPath, {
|
|
91
|
+
detailed,
|
|
92
|
+
behavioral: useBehav,
|
|
93
|
+
apiUrl,
|
|
94
|
+
useLLM,
|
|
95
|
+
policy,
|
|
96
|
+
});
|
|
97
|
+
const icon = res.exitCode === 0 ? "✅" : "❌";
|
|
98
|
+
return { text: `${icon} 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
|
|
99
|
+
} else {
|
|
100
|
+
const res = await runScan("batch", targetPath, {
|
|
101
|
+
recursive,
|
|
102
|
+
detailed,
|
|
103
|
+
behavioral: useBehav,
|
|
104
|
+
apiUrl,
|
|
105
|
+
useLLM,
|
|
106
|
+
policy,
|
|
107
|
+
});
|
|
108
|
+
const icon = res.exitCode === 0 ? "✅" : "❌";
|
|
109
|
+
return { text: `${icon} 批量扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function handleHealthCommand(): Promise<CommandResponse> {
|
|
114
|
+
const state = loadState();
|
|
115
|
+
const alerts: string[] = (state as any).pendingAlerts ?? [];
|
|
116
|
+
|
|
117
|
+
const lines = [
|
|
118
|
+
"✅ *Skills Scanner 状态*",
|
|
119
|
+
`API 地址:${apiUrl}`,
|
|
120
|
+
`安装前扫描:${preInstallScan === "on" ? `✅ 监听中 (${onUnsafe})` : "⏭️ 已禁用"}`,
|
|
121
|
+
`扫描策略:${policy}`,
|
|
122
|
+
`LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
|
|
123
|
+
`行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
|
|
124
|
+
`上次扫描:${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
|
|
125
|
+
`扫描目录:\n${scanDirs.map((d) => ` 📁 ${d}`).join("\n")}`,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// API health check
|
|
129
|
+
lines.push("", "✅ *API 服务检查*");
|
|
130
|
+
try {
|
|
131
|
+
const res = await runScan("health", "", { apiUrl });
|
|
132
|
+
if (res.exitCode === 0) {
|
|
133
|
+
lines.push(`API 服务:✅ 正常`);
|
|
134
|
+
if (res.data) {
|
|
135
|
+
const healthData = res.data as any;
|
|
136
|
+
if (healthData.analyzers_available) {
|
|
137
|
+
lines.push(`可用分析器:${healthData.analyzers_available.join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
lines.push(`API 服务:⚠️ 不可用`);
|
|
142
|
+
lines.push(`错误:${res.output}`);
|
|
143
|
+
}
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
lines.push(`API 服务:⚠️ 连接失败`);
|
|
146
|
+
lines.push(`错误:${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (alerts.length > 0) {
|
|
150
|
+
lines.push("", `⚠️ *待查告警 (${alerts.length} 条):*`);
|
|
151
|
+
alerts.slice(-5).forEach((a) => lines.push(` ${a}`));
|
|
152
|
+
saveState({ ...state, pendingAlerts: [] });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lines.push("", "✅ *定时任务*");
|
|
156
|
+
if (state.cronJobId && state.cronJobId !== "manual-created") {
|
|
157
|
+
lines.push(`状态:✅ 已注册 (${state.cronJobId})`);
|
|
158
|
+
} else {
|
|
159
|
+
lines.push("状态:❌ 未注册");
|
|
160
|
+
lines.push("ℹ️ 使用 `/skills-scanner cron register` 注册");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { text: lines.join("\n") };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleConfigCommand(args: string): Promise<CommandResponse> {
|
|
167
|
+
const action = args.trim().toLowerCase() || "show";
|
|
168
|
+
|
|
169
|
+
if (action === "show" || action === "") {
|
|
170
|
+
const configGuide = generateConfigGuide(
|
|
171
|
+
cfg,
|
|
172
|
+
apiUrl,
|
|
173
|
+
scanDirs,
|
|
174
|
+
behavioral,
|
|
175
|
+
useLLM,
|
|
176
|
+
policy,
|
|
177
|
+
preInstallScan,
|
|
178
|
+
onUnsafe
|
|
179
|
+
);
|
|
180
|
+
return { text: "```\n" + configGuide + "\n```" };
|
|
181
|
+
} else if (action === "reset") {
|
|
182
|
+
const state = loadState() as any;
|
|
183
|
+
saveState({ ...state, configReviewed: false });
|
|
184
|
+
return {
|
|
185
|
+
text: "✅ 配置审查标记已重置\n下次重启 Gateway 时将再次显示配置向导",
|
|
186
|
+
};
|
|
187
|
+
} else {
|
|
188
|
+
return { text: "用法:`/skills-scanner config [show|reset]`" };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function handleCronCommand(args: string): Promise<CommandResponse> {
|
|
193
|
+
const action = args.trim().toLowerCase() || "status";
|
|
194
|
+
|
|
195
|
+
if (action === "setup" || action === "register") {
|
|
196
|
+
try {
|
|
197
|
+
await ensureCronJobViaGateway({
|
|
198
|
+
logger,
|
|
199
|
+
callGateway,
|
|
200
|
+
});
|
|
201
|
+
return { text: "✅ 定时任务注册完成\n请查看日志了解详情" };
|
|
202
|
+
} catch (err: any) {
|
|
203
|
+
return { text: `⚠️ 定时任务注册失败:${err.message}` };
|
|
204
|
+
}
|
|
205
|
+
} else if (action === "unregister") {
|
|
206
|
+
try {
|
|
207
|
+
// 列出所有任务
|
|
208
|
+
const listResult = await callGateway("cron.list", {});
|
|
209
|
+
const jobs = Array.isArray(listResult?.jobs) ? listResult.jobs : [];
|
|
210
|
+
const existingJobs = jobs.filter((job: any) => job.name === "skills-weekly-report");
|
|
211
|
+
|
|
212
|
+
if (existingJobs.length === 0) {
|
|
213
|
+
return { text: "ℹ️ 未找到已注册的定时任务" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 删除所有同名任务
|
|
217
|
+
const deletedIds: string[] = [];
|
|
218
|
+
for (const job of existingJobs) {
|
|
219
|
+
const jobId = job.jobId || job.id;
|
|
220
|
+
try {
|
|
221
|
+
await callGateway("cron.remove", { jobId });
|
|
222
|
+
deletedIds.push(jobId);
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
logger.warn(`删除任务 ${jobId} 失败: ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (deletedIds.length > 0) {
|
|
229
|
+
return {
|
|
230
|
+
text: `✅ 已删除 ${deletedIds.length} 个定时任务\n${deletedIds.map(id => `- ${id}`).join("\n")}`,
|
|
231
|
+
};
|
|
232
|
+
} else {
|
|
233
|
+
return { text: "⚠️ 删除失败,请查看日志" };
|
|
234
|
+
}
|
|
235
|
+
} catch (err: any) {
|
|
236
|
+
return { text: `⚠️ 删除失败:${err.message}` };
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// status
|
|
240
|
+
try {
|
|
241
|
+
const statusText = await checkCronJobStatus({
|
|
242
|
+
logger,
|
|
243
|
+
callGateway,
|
|
244
|
+
});
|
|
245
|
+
return { text: statusText };
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
return { text: `⚠️ 查询状态失败:${err.message}` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getHelpText(): string {
|
|
253
|
+
return [
|
|
254
|
+
"✅ *Skills Scanner - 帮助*",
|
|
255
|
+
"",
|
|
256
|
+
"═══ 扫描命令 ═══",
|
|
257
|
+
"`/skills-scanner scan <路径> [选项]`",
|
|
258
|
+
"`/skills-scanner scan clawhub <URL> [选项]`",
|
|
259
|
+
"",
|
|
260
|
+
"选项:",
|
|
261
|
+
"• `--detailed` - 显示详细发现",
|
|
262
|
+
"• `--behavioral` - 启用行为分析",
|
|
263
|
+
"• `--recursive` - 递归扫描子目录",
|
|
264
|
+
"• `--report` - 生成日报格式",
|
|
265
|
+
"",
|
|
266
|
+
"示例:",
|
|
267
|
+
"```",
|
|
268
|
+
"/skills-scanner scan ~/.openclaw/skills/my-skill",
|
|
269
|
+
"/skills-scanner scan ~/.openclaw/skills --recursive",
|
|
270
|
+
"/skills-scanner scan ~/.openclaw/skills --report",
|
|
271
|
+
"/skills-scanner scan clawhub https://clawhub.ai/username/project",
|
|
272
|
+
"/skills-scanner scan clawhub https://clawhub.ai/username/project --detailed",
|
|
273
|
+
"```",
|
|
274
|
+
"",
|
|
275
|
+
"═══ 其他命令 ═══",
|
|
276
|
+
"• `/skills-scanner health` - 健康检查",
|
|
277
|
+
"• `/skills-scanner config [show|reset]` - 配置管理",
|
|
278
|
+
"• `/skills-scanner cron [register|unregister|status]` - 定时任务管理",
|
|
279
|
+
].join("\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
handleScanCommand,
|
|
284
|
+
handleHealthCommand,
|
|
285
|
+
handleConfigCommand,
|
|
286
|
+
handleCronCommand,
|
|
287
|
+
getHelpText,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import type { ScannerConfig } from "./types.js";
|
|
7
|
+
import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
|
|
8
|
+
|
|
9
|
+
export interface ValidationResult {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
errors: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate plugin configuration
|
|
17
|
+
*/
|
|
18
|
+
export function validateConfig(
|
|
19
|
+
cfg: ScannerConfig,
|
|
20
|
+
logger: PluginLogger
|
|
21
|
+
): ValidationResult {
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
const warnings: string[] = [];
|
|
24
|
+
|
|
25
|
+
// Validate API URL
|
|
26
|
+
if (cfg.apiUrl) {
|
|
27
|
+
try {
|
|
28
|
+
new URL(cfg.apiUrl);
|
|
29
|
+
} catch {
|
|
30
|
+
errors.push(`Invalid apiUrl: "${cfg.apiUrl}" is not a valid URL`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate scan directories
|
|
35
|
+
if (cfg.scanDirs && cfg.scanDirs.length > 0) {
|
|
36
|
+
for (const dir of cfg.scanDirs) {
|
|
37
|
+
if (!existsSync(dir)) {
|
|
38
|
+
warnings.push(`Scan directory does not exist: ${dir}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validate policy
|
|
44
|
+
if (cfg.policy && !["strict", "balanced", "permissive"].includes(cfg.policy)) {
|
|
45
|
+
errors.push(
|
|
46
|
+
`Invalid policy: "${cfg.policy}". Must be one of: strict, balanced, permissive`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate preInstallScan
|
|
51
|
+
if (cfg.preInstallScan && !["on", "off"].includes(cfg.preInstallScan)) {
|
|
52
|
+
errors.push(
|
|
53
|
+
`Invalid preInstallScan: "${cfg.preInstallScan}". Must be "on" or "off"`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate onUnsafe
|
|
58
|
+
if (cfg.onUnsafe && !["warn", "delete", "quarantine"].includes(cfg.onUnsafe)) {
|
|
59
|
+
errors.push(
|
|
60
|
+
`Invalid onUnsafe: "${cfg.onUnsafe}". Must be one of: warn, delete, quarantine`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate reportDir if specified
|
|
65
|
+
if (cfg.reportDir && !existsSync(cfg.reportDir)) {
|
|
66
|
+
warnings.push(`Report directory does not exist: ${cfg.reportDir}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate quarantineDir if specified
|
|
70
|
+
if (cfg.quarantineDir && !existsSync(cfg.quarantineDir)) {
|
|
71
|
+
warnings.push(`Quarantine directory does not exist: ${cfg.quarantineDir}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate scanTimeoutMs
|
|
75
|
+
if (cfg.scanTimeoutMs !== undefined) {
|
|
76
|
+
if (typeof cfg.scanTimeoutMs !== "number" || cfg.scanTimeoutMs <= 0) {
|
|
77
|
+
errors.push(
|
|
78
|
+
`Invalid scanTimeoutMs: must be a positive number (milliseconds)`
|
|
79
|
+
);
|
|
80
|
+
} else if (cfg.scanTimeoutMs < 10000) {
|
|
81
|
+
warnings.push(
|
|
82
|
+
`scanTimeoutMs is very low (${cfg.scanTimeoutMs}ms). Scans may timeout prematurely.`
|
|
83
|
+
);
|
|
84
|
+
} else if (cfg.scanTimeoutMs > 600000) {
|
|
85
|
+
warnings.push(
|
|
86
|
+
`scanTimeoutMs is very high (${cfg.scanTimeoutMs}ms). Consider reducing for better responsiveness.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Log validation results
|
|
92
|
+
if (errors.length > 0) {
|
|
93
|
+
logger.error("[skills-scanner] Configuration validation failed", {
|
|
94
|
+
errors,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (warnings.length > 0) {
|
|
99
|
+
logger.warn("[skills-scanner] Configuration warnings", {
|
|
100
|
+
warnings,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
valid: errors.length === 0,
|
|
106
|
+
errors,
|
|
107
|
+
warnings,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置管理模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry";
|
|
7
|
+
import type { ScannerConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const skillsScannerConfigSchema: OpenClawPluginConfigSchema = {
|
|
10
|
+
safeParse: (value: unknown) => {
|
|
11
|
+
try {
|
|
12
|
+
const config = value as ScannerConfig;
|
|
13
|
+
|
|
14
|
+
// 验证 policy
|
|
15
|
+
if (config.policy && !["strict", "balanced", "permissive"].includes(config.policy)) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: {
|
|
19
|
+
issues: [{
|
|
20
|
+
path: ["policy"],
|
|
21
|
+
message: "policy 必须是 strict、balanced 或 permissive"
|
|
22
|
+
}]
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 验证 preInstallScan
|
|
28
|
+
if (config.preInstallScan && !["on", "off"].includes(config.preInstallScan)) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: {
|
|
32
|
+
issues: [{
|
|
33
|
+
path: ["preInstallScan"],
|
|
34
|
+
message: "preInstallScan 必须是 on 或 off"
|
|
35
|
+
}]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 验证 onUnsafe
|
|
41
|
+
if (config.onUnsafe && !["quarantine", "delete", "warn"].includes(config.onUnsafe)) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: {
|
|
45
|
+
issues: [{
|
|
46
|
+
path: ["onUnsafe"],
|
|
47
|
+
message: "onUnsafe 必须是 quarantine、delete 或 warn"
|
|
48
|
+
}]
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { success: true, data: config };
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: {
|
|
58
|
+
issues: [{
|
|
59
|
+
path: [],
|
|
60
|
+
message: String(err)
|
|
61
|
+
}]
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
uiHints: {
|
|
68
|
+
apiUrl: {
|
|
69
|
+
label: "API 服务地址",
|
|
70
|
+
help: "扫描 API 服务的 URL 地址",
|
|
71
|
+
placeholder: "https://110.vemic.com/skills-scanner",
|
|
72
|
+
type: "string",
|
|
73
|
+
format: "uri",
|
|
74
|
+
},
|
|
75
|
+
scanDirs: {
|
|
76
|
+
label: "扫描目录",
|
|
77
|
+
help: "要监控的 Skills 目录列表,支持 ~ 路径。默认监控 ~/.openclaw/skills 和 ~/.openclaw/workspace/skills",
|
|
78
|
+
type: "array",
|
|
79
|
+
items: { type: "string" },
|
|
80
|
+
},
|
|
81
|
+
behavioral: {
|
|
82
|
+
label: "行为分析",
|
|
83
|
+
help: "启用深度行为分析(较慢但更准确)。检测运行时行为模式,如网络请求、文件操作等",
|
|
84
|
+
type: "boolean",
|
|
85
|
+
default: false,
|
|
86
|
+
},
|
|
87
|
+
useLLM: {
|
|
88
|
+
label: "LLM 分析",
|
|
89
|
+
help: "使用 LLM 进行语义分析。可以检测更复杂的恶意模式,但需要更多时间和资源",
|
|
90
|
+
type: "boolean",
|
|
91
|
+
default: false,
|
|
92
|
+
},
|
|
93
|
+
policy: {
|
|
94
|
+
label: "扫描策略",
|
|
95
|
+
help: "strict=严格(零容忍)/ balanced=平衡(推荐,误报率低)/ permissive=宽松(仅检测明显威胁)",
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: ["strict", "balanced", "permissive"],
|
|
98
|
+
default: "balanced",
|
|
99
|
+
},
|
|
100
|
+
preInstallScan: {
|
|
101
|
+
label: "安装前扫描",
|
|
102
|
+
help: "监听新 Skill 并自动扫描。启用后会实时监控 scanDirs 中的文件变化",
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: ["on", "off"],
|
|
105
|
+
default: "on",
|
|
106
|
+
},
|
|
107
|
+
onUnsafe: {
|
|
108
|
+
label: "不安全处理",
|
|
109
|
+
help: "warn=仅警告(推荐,不影响使用)/ quarantine=隔离到隔离区 / delete=直接删除(危险)",
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: ["warn", "quarantine", "delete"],
|
|
112
|
+
default: "warn",
|
|
113
|
+
},
|
|
114
|
+
enableBeforeInstallHook: {
|
|
115
|
+
label: "安装前拦截",
|
|
116
|
+
help: "启用 before_install hook,在安装前强制拦截不安全的 Skills(强烈推荐)。这是最后一道防线",
|
|
117
|
+
type: "boolean",
|
|
118
|
+
default: true,
|
|
119
|
+
},
|
|
120
|
+
injectSecurityGuidance: {
|
|
121
|
+
label: "注入安全指导",
|
|
122
|
+
help: "在系统提示中注入安全指导,提醒 AI 注意 Skills 安全问题",
|
|
123
|
+
type: "boolean",
|
|
124
|
+
default: true,
|
|
125
|
+
},
|
|
126
|
+
enablePromptInjectionGuard: {
|
|
127
|
+
label: "提示注入防护",
|
|
128
|
+
help: "启用提示注入攻击防护,检测并阻止恶意提示",
|
|
129
|
+
type: "boolean",
|
|
130
|
+
default: false,
|
|
131
|
+
},
|
|
132
|
+
enableHighRiskOperationGuard: {
|
|
133
|
+
label: "高风险操作防护",
|
|
134
|
+
help: "启用高风险操作防护,对敏感操作进行额外检查",
|
|
135
|
+
type: "boolean",
|
|
136
|
+
default: false,
|
|
137
|
+
},
|
|
138
|
+
scanTimeoutMs: {
|
|
139
|
+
label: "扫描超时(毫秒)",
|
|
140
|
+
help: "单次扫描的最大时长,超时后自动取消。默认 180000ms (3分钟)",
|
|
141
|
+
type: "number",
|
|
142
|
+
minimum: 10000,
|
|
143
|
+
maximum: 600000,
|
|
144
|
+
default: 180000,
|
|
145
|
+
},
|
|
146
|
+
reportDir: {
|
|
147
|
+
label: "报告目录",
|
|
148
|
+
help: "扫描报告保存目录。留空则使用默认位置",
|
|
149
|
+
type: "string",
|
|
150
|
+
},
|
|
151
|
+
quarantineDir: {
|
|
152
|
+
label: "隔离目录",
|
|
153
|
+
help: "隔离文件保存目录。留空则使用默认位置",
|
|
154
|
+
type: "string",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export function generateConfigGuide(
|
|
161
|
+
cfg: ScannerConfig,
|
|
162
|
+
apiUrl: string,
|
|
163
|
+
scanDirs: string[],
|
|
164
|
+
behavioral: boolean,
|
|
165
|
+
useLLM: boolean,
|
|
166
|
+
policy: string,
|
|
167
|
+
preInstallScan: string,
|
|
168
|
+
onUnsafe: string
|
|
169
|
+
): string {
|
|
170
|
+
return [
|
|
171
|
+
"",
|
|
172
|
+
"╔════════════════════════════════════════════════════════════════╗",
|
|
173
|
+
"║ 🎉 Skills Scanner 首次运行 - 配置向导 ║",
|
|
174
|
+
"╚════════════════════════════════════════════════════════════════╝",
|
|
175
|
+
"",
|
|
176
|
+
"当前使用默认配置。建议根据您的需求自定义配置:",
|
|
177
|
+
"",
|
|
178
|
+
"📋 当前配置:",
|
|
179
|
+
` • API 服务地址:${apiUrl}`,
|
|
180
|
+
` • 扫描目录:${scanDirs.length} 个(自动检测)`,
|
|
181
|
+
` • 行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
|
|
182
|
+
` • LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
|
|
183
|
+
` • 扫描策略:${policy}`,
|
|
184
|
+
` • 安装前扫描:${preInstallScan === "on" ? "✅ 启用" : "❌ 禁用"}`,
|
|
185
|
+
` • 不安全处理:${onUnsafe}`,
|
|
186
|
+
"",
|
|
187
|
+
"🔧 配置文件位置:",
|
|
188
|
+
" ~/.openclaw/config.json",
|
|
189
|
+
"",
|
|
190
|
+
"📝 推荐配置示例:",
|
|
191
|
+
"",
|
|
192
|
+
"```json",
|
|
193
|
+
"{",
|
|
194
|
+
' "plugins": {',
|
|
195
|
+
' "entries": {',
|
|
196
|
+
' "skills-scanner": {',
|
|
197
|
+
' "enabled": true,',
|
|
198
|
+
' "config": {',
|
|
199
|
+
' "apiUrl": "https://110.vemic.com/skills-scanner",',
|
|
200
|
+
' "scanDirs": ["~/.openclaw/skills"],',
|
|
201
|
+
' "behavioral": false,',
|
|
202
|
+
' "useLLM": false,',
|
|
203
|
+
' "policy": "balanced",',
|
|
204
|
+
' "preInstallScan": "on",',
|
|
205
|
+
' "onUnsafe": "warn"',
|
|
206
|
+
' }',
|
|
207
|
+
' }',
|
|
208
|
+
' }',
|
|
209
|
+
' }',
|
|
210
|
+
"}",
|
|
211
|
+
"```",
|
|
212
|
+
"",
|
|
213
|
+
"💡 配置说明:",
|
|
214
|
+
"",
|
|
215
|
+
"1. apiUrl 默认 https://110.vemic.com/skills-scanner",
|
|
216
|
+
"2. scanDirs 可添加多个目录(默认自动检测 ~/.openclaw/skills)",
|
|
217
|
+
"3. behavioral false=快速扫描(推荐),true=深度分析",
|
|
218
|
+
"4. useLLM false=不使用 LLM(推荐),true=语义分析",
|
|
219
|
+
"5. policy strict / balanced(推荐)/ permissive",
|
|
220
|
+
"6. preInstallScan on=监听新 Skill 并自动扫描(推荐),off=禁用",
|
|
221
|
+
"7. onUnsafe warn=仅警告(推荐),quarantine=隔离,delete=删除",
|
|
222
|
+
"",
|
|
223
|
+
"🚀 快速开始:",
|
|
224
|
+
" 编辑配置文件后重启 Gateway",
|
|
225
|
+
" /skills-scanner health",
|
|
226
|
+
"",
|
|
227
|
+
"提示:此消息只在首次运行时显示。",
|
|
228
|
+
"════════════════════════════════════════════════════════════════",
|
|
229
|
+
].join("\n");
|
|
230
|
+
}
|