@pwddd/skills-scanner 3.0.22 → 4.0.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.
package/src/commands.ts CHANGED
@@ -2,57 +2,43 @@
2
2
  * Command handlers module
3
3
  */
4
4
 
5
- import { existsSync, execSync } from "node:fs";
5
+ import { existsSync } from "node:fs";
6
6
  import { join, basename } from "node:path";
7
- import { promisify } from "node:util";
8
- import { exec } from "node:child_process";
9
7
  import { runScan } from "./scanner.js";
10
- import { buildDailyReport } from "./report.js";
11
8
  import { loadState, saveState, expandPath } from "./state.js";
12
- import { isPythonReady } from "./deps.js";
13
9
  import { generateConfigGuide } from "./config.js";
14
- import { ensureCronJob, getOpenClawCommand } from "./cron.js";
15
- import type { ScannerConfig } from "./types.js";
16
-
17
- const execAsync = promisify(exec);
10
+ import type { ScannerConfig, CommandResponse, PluginLogger } from "./types.js";
18
11
 
19
12
  export function createCommandHandlers(
20
13
  cfg: ScannerConfig,
21
14
  apiUrl: string,
22
- scanDirs: string[],
23
15
  behavioral: boolean,
24
16
  useLLM: boolean,
25
17
  policy: string,
26
- preInstallScan: string,
27
18
  onUnsafe: string,
28
- pythonCmd: string | null,
29
- scanScript: string,
30
- logger: any
19
+ logger: PluginLogger,
20
+ apiConfig?: any
31
21
  ) {
32
- async function handleScanCommand(args: string): Promise<any> {
22
+ async function handleScanCommand(args: string): Promise<CommandResponse> {
33
23
  if (!args) {
34
24
  return {
35
- text: "用法:`/skills-scanner scan <路径> [--detailed] [--behavioral] [--recursive] [--report]`\n或:`/skills-scanner scan clawhub <URL> [--detailed] [--behavioral]`",
25
+ text: "用法:`/skills-scanner scan <路径> [--detailed] [--behavioral] [--recursive]`\n或:`/skills-scanner scan clawhub <URL> [--detailed] [--behavioral]`",
36
26
  };
37
27
  }
38
28
 
39
- if (!isPythonReady(pythonCmd)) {
40
- return { text: "⚠️ Python 依赖尚未就绪,请稍后重试或查看日志" };
41
- }
42
-
43
29
  const parts = args.split(/\s+/);
44
30
 
45
31
  // Check if this is a ClawHub scan
46
32
  if (parts[0] === "clawhub") {
47
33
  const clawhubUrl = parts.find((p) => p.startsWith("https://clawhub.ai/"));
48
34
  if (!clawhubUrl) {
49
- return { text: "⚠️ 请提供有效的 ClawHub URL (例如: https://clawhub.ai/username/project)" };
35
+ return { text: "⚠️ 请提供有效的 ClawHub URL (例如:https://clawhub.ai/username/project)" };
50
36
  }
51
37
 
52
38
  const detailed = parts.includes("--detailed");
53
39
  const useBehav = parts.includes("--behavioral") || behavioral;
54
40
 
55
- const res = await runScan(pythonCmd, scanScript, "clawhub", clawhubUrl, {
41
+ const res = await runScan("clawhub", clawhubUrl, {
56
42
  detailed,
57
43
  behavioral: useBehav,
58
44
  apiUrl,
@@ -67,25 +53,6 @@ export function createCommandHandlers(
67
53
  const detailed = parts.includes("--detailed");
68
54
  const useBehav = parts.includes("--behavioral") || behavioral;
69
55
  const recursive = parts.includes("--recursive");
70
- const isReport = parts.includes("--report");
71
-
72
- // Report mode: use configured scanDirs
73
- if (isReport) {
74
- if (scanDirs.length === 0) {
75
- return { text: "⚠️ 未找到可扫描目录,请检查配置" };
76
- }
77
- const report = await buildDailyReport(
78
- scanDirs,
79
- useBehav,
80
- apiUrl,
81
- useLLM,
82
- policy,
83
- logger,
84
- pythonCmd,
85
- scanScript
86
- );
87
- return { text: report };
88
- }
89
56
 
90
57
  // Regular scan mode: require path
91
58
  if (!targetPath) {
@@ -93,23 +60,41 @@ export function createCommandHandlers(
93
60
  }
94
61
 
95
62
  if (!existsSync(targetPath)) {
96
- return { text: `⚠️ 路径不存在: ${targetPath}` };
63
+ return { text: `⚠️ 路径不存在:${targetPath}` };
97
64
  }
98
65
 
99
66
  const isSingleSkill = existsSync(join(targetPath, "SKILL.md"));
100
67
 
101
68
  if (isSingleSkill) {
102
- const res = await runScan(pythonCmd, scanScript, "scan", targetPath, {
69
+ logger.info(`[命令] 开始扫描单个 Skill: ${targetPath}`);
70
+ logger.debug(`[命令] 扫描参数`, {
71
+ mode: "scan",
72
+ targetPath,
103
73
  detailed,
104
74
  behavioral: useBehav,
105
75
  apiUrl,
106
76
  useLLM,
107
77
  policy,
108
78
  });
79
+
80
+ const res = await runScan("scan", targetPath, {
81
+ detailed,
82
+ behavioral: useBehav,
83
+ apiUrl,
84
+ useLLM,
85
+ policy,
86
+ timeoutMs: 180000, // 3 minutes
87
+ });
88
+
89
+ logger.info(`[命令] 扫描完成`, {
90
+ exitCode: res.exitCode,
91
+ hasData: !!res.data,
92
+ });
93
+
109
94
  const icon = res.exitCode === 0 ? "✅" : "❌";
110
95
  return { text: `${icon} 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
111
96
  } else {
112
- const res = await runScan(pythonCmd, scanScript, "batch", targetPath, {
97
+ const res = await runScan("batch", targetPath, {
113
98
  recursive,
114
99
  detailed,
115
100
  behavioral: useBehav,
@@ -122,55 +107,38 @@ export function createCommandHandlers(
122
107
  }
123
108
  }
124
109
 
125
- async function handleHealthCommand(): Promise<any> {
126
- const state = loadState() as any;
127
- const alerts: string[] = state.pendingAlerts ?? [];
110
+ async function handleHealthCommand(): Promise<CommandResponse> {
111
+ const state = loadState();
112
+ const alerts: string[] = (state as any).pendingAlerts ?? [];
128
113
 
129
114
  const lines = [
130
115
  "✅ *Skills Scanner 状态*",
131
- `API 地址: ${apiUrl}`,
132
- `Python 依赖: ${isPythonReady(pythonCmd) ? "✅ 就绪" : "❌ 未就绪"}`,
133
- `安装前扫描: ${preInstallScan === "on" ? `✅ 监听中 (${onUnsafe})` : "⏭️ 已禁用"}`,
134
- `扫描策略: ${policy}`,
135
- `LLM 分析: ${useLLM ? "✅ 启用" : "❌ 禁用"}`,
136
- `行为分析: ${behavioral ? "✅ 启用" : "❌ 禁用"}`,
137
- `上次扫描: ${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
138
- `扫描目录:\n${scanDirs.map((d) => ` 📁 ${d}`).join("\n")}`,
116
+ `API 地址:${apiUrl}`,
117
+ `扫描策略:${policy}`,
118
+ `LLM 分析:${useLLM ? "✅ 启用" : " 禁用"}`,
119
+ `行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
120
+ `上次扫描:${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
139
121
  ];
140
122
 
141
- if (isPythonReady(pythonCmd)) {
142
- lines.push("", "✅ *API 服务检查*");
143
- try {
144
- const cmd = `"${pythonCmd}" "${scanScript}" --api-url "${apiUrl}" health`;
145
- const env = { ...process.env };
146
- delete env.http_proxy;
147
- delete env.https_proxy;
148
- delete env.HTTP_PROXY;
149
- delete env.HTTPS_PROXY;
150
- delete env.all_proxy;
151
- delete env.ALL_PROXY;
152
-
153
- const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
154
- const output = (stdout + stderr).trim();
155
-
156
- if (output.includes("✅") || output.includes("✓") || output.includes("OK")) {
157
- lines.push(`API 服务: ✅ 正常`);
158
- const jsonMatch = output.match(/\{[\s\S]*\}/);
159
- if (jsonMatch) {
160
- try {
161
- const healthData = JSON.parse(jsonMatch[0]);
162
- if (healthData.analyzers_available) {
163
- lines.push(`可用分析器: ${healthData.analyzers_available.join(", ")}`);
164
- }
165
- } catch {}
123
+ // API health check
124
+ lines.push("", "✅ *API 服务检查*");
125
+ try {
126
+ const res = await runScan("health", "", { apiUrl });
127
+ if (res.exitCode === 0) {
128
+ lines.push(`API 服务:✅ 正常`);
129
+ if (res.data) {
130
+ const healthData = res.data as any;
131
+ if (healthData.analyzers_available) {
132
+ lines.push(`可用分析器:${healthData.analyzers_available.join(", ")}`);
166
133
  }
167
- } else {
168
- lines.push(`API 服务: ⚠️ 不可用`);
169
134
  }
170
- } catch (err: any) {
171
- lines.push(`API 服务: ⚠️ 连接失败`);
172
- lines.push(`错误: ${err.message}`);
135
+ } else {
136
+ lines.push(`API 服务:⚠️ 不可用`);
137
+ lines.push(`错误:${res.output}`);
173
138
  }
139
+ } catch (err: any) {
140
+ lines.push(`API 服务:⚠️ 连接失败`);
141
+ lines.push(`错误:${err.message}`);
174
142
  }
175
143
 
176
144
  if (alerts.length > 0) {
@@ -179,29 +147,21 @@ export function createCommandHandlers(
179
147
  saveState({ ...state, pendingAlerts: [] });
180
148
  }
181
149
 
182
- lines.push("", "✅ *定时任务*");
183
- if (state.cronJobId && state.cronJobId !== "manual-created") {
184
- lines.push(`状态: ✅ 已注册 (${state.cronJobId})`);
185
- } else {
186
- lines.push("状态: ❌ 未注册");
187
- lines.push("ℹ️ 使用 `/skills-scanner cron register` 注册");
188
- }
150
+ // 定时任务功能已移除
189
151
 
190
152
  return { text: lines.join("\n") };
191
153
  }
192
154
 
193
- async function handleConfigCommand(args: string): Promise<any> {
155
+ async function handleConfigCommand(args: string): Promise<CommandResponse> {
194
156
  const action = args.trim().toLowerCase() || "show";
195
157
 
196
158
  if (action === "show" || action === "") {
197
159
  const configGuide = generateConfigGuide(
198
160
  cfg,
199
161
  apiUrl,
200
- scanDirs,
201
162
  behavioral,
202
163
  useLLM,
203
164
  policy,
204
- preInstallScan,
205
165
  onUnsafe
206
166
  );
207
167
  return { text: "```\n" + configGuide + "\n```" };
@@ -212,59 +172,7 @@ export function createCommandHandlers(
212
172
  text: "✅ 配置审查标记已重置\n下次重启 Gateway 时将再次显示配置向导",
213
173
  };
214
174
  } else {
215
- return { text: "用法: `/skills-scanner config [show|reset]`" };
216
- }
217
- }
218
-
219
- async function handleCronCommand(args: string): Promise<any> {
220
- const action = args.trim().toLowerCase() || "status";
221
- const state = loadState() as any;
222
-
223
- if (action === "setup" || action === "register") {
224
- const oldJobId = state.cronJobId;
225
- if (oldJobId && oldJobId !== "manual-created") {
226
- const openclawCmd = getOpenClawCommand();
227
- try {
228
- execSync(`${openclawCmd} cron remove ${oldJobId}`, { encoding: "utf-8", timeout: 5000 });
229
- } catch {}
230
- }
231
-
232
- saveState({ ...state, cronJobId: undefined });
233
- await ensureCronJob(logger);
234
-
235
- const newState = loadState() as any;
236
- if (newState.cronJobId) {
237
- return { text: `✅ 定时任务注册成功\n任务 ID: ${newState.cronJobId}` };
238
- } else {
239
- return { text: "⚠️ 定时任务注册失败,请查看日志" };
240
- }
241
- } else if (action === "unregister") {
242
- if (!state.cronJobId) {
243
- return { text: "ℹ️ 未找到已注册的定时任务" };
244
- }
245
-
246
- const openclawCmd = getOpenClawCommand();
247
- try {
248
- execSync(`${openclawCmd} cron remove ${state.cronJobId}`, {
249
- encoding: "utf-8",
250
- timeout: 5000,
251
- });
252
- saveState({ ...state, cronJobId: undefined });
253
- return { text: `✅ 定时任务已删除: ${state.cronJobId}` };
254
- } catch (err: any) {
255
- return { text: `⚠️ 删除失败: ${err.message}` };
256
- }
257
- } else {
258
- const lines = ["✅ *定时任务状态*"];
259
- if (state.cronJobId && state.cronJobId !== "manual-created") {
260
- lines.push(`任务 ID: ${state.cronJobId}`);
261
- lines.push(`执行时间: 每天 08:00 (Asia/Shanghai)`);
262
- lines.push("状态: ✅ 已注册");
263
- } else {
264
- lines.push("状态: ❌ 未注册");
265
- lines.push("", "ℹ️ 使用 `/skills-scanner cron setup` 注册");
266
- }
267
- return { text: lines.join("\n") };
175
+ return { text: "用法:`/skills-scanner config [show|reset]`" };
268
176
  }
269
177
  }
270
178
 
@@ -280,13 +188,11 @@ export function createCommandHandlers(
280
188
  "• `--detailed` - 显示详细发现",
281
189
  "• `--behavioral` - 启用行为分析",
282
190
  "• `--recursive` - 递归扫描子目录",
283
- "• `--report` - 生成日报格式",
284
191
  "",
285
192
  "示例:",
286
193
  "```",
287
194
  "/skills-scanner scan ~/.openclaw/skills/my-skill",
288
195
  "/skills-scanner scan ~/.openclaw/skills --recursive",
289
- "/skills-scanner scan ~/.openclaw/skills --report",
290
196
  "/skills-scanner scan clawhub https://clawhub.ai/username/project",
291
197
  "/skills-scanner scan clawhub https://clawhub.ai/username/project --detailed",
292
198
  "```",
@@ -294,7 +200,6 @@ export function createCommandHandlers(
294
200
  "═══ 其他命令 ═══",
295
201
  "• `/skills-scanner health` - 健康检查",
296
202
  "• `/skills-scanner config [show|reset]` - 配置管理",
297
- "• `/skills-scanner cron [register|unregister|status]` - 定时任务管理",
298
203
  ].join("\n");
299
204
  }
300
205
 
@@ -302,7 +207,6 @@ export function createCommandHandlers(
302
207
  handleScanCommand,
303
208
  handleHealthCommand,
304
209
  handleConfigCommand,
305
- handleCronCommand,
306
210
  getHelpText,
307
211
  };
308
212
  }
@@ -0,0 +1,94 @@
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 policy
35
+ if (cfg.policy && !["strict", "balanced", "permissive"].includes(cfg.policy)) {
36
+ errors.push(
37
+ `Invalid policy: "${cfg.policy}". Must be one of: strict, balanced, permissive`
38
+ );
39
+ }
40
+
41
+ // Validate onUnsafe
42
+ if (cfg.onUnsafe && !["warn", "delete", "quarantine"].includes(cfg.onUnsafe)) {
43
+ errors.push(
44
+ `Invalid onUnsafe: "${cfg.onUnsafe}". Must be one of: warn, delete, quarantine`
45
+ );
46
+ }
47
+
48
+ // Validate reportDir if specified
49
+ if (cfg.reportDir && !existsSync(cfg.reportDir)) {
50
+ warnings.push(`Report directory does not exist: ${cfg.reportDir}`);
51
+ }
52
+
53
+ // Validate quarantineDir if specified
54
+ if (cfg.quarantineDir && !existsSync(cfg.quarantineDir)) {
55
+ warnings.push(`Quarantine directory does not exist: ${cfg.quarantineDir}`);
56
+ }
57
+
58
+ // Validate scanTimeoutMs
59
+ if (cfg.scanTimeoutMs !== undefined) {
60
+ if (typeof cfg.scanTimeoutMs !== "number" || cfg.scanTimeoutMs <= 0) {
61
+ errors.push(
62
+ `Invalid scanTimeoutMs: must be a positive number (milliseconds)`
63
+ );
64
+ } else if (cfg.scanTimeoutMs < 10000) {
65
+ warnings.push(
66
+ `scanTimeoutMs is very low (${cfg.scanTimeoutMs}ms). Scans may timeout prematurely.`
67
+ );
68
+ } else if (cfg.scanTimeoutMs > 600000) {
69
+ warnings.push(
70
+ `scanTimeoutMs is very high (${cfg.scanTimeoutMs}ms). Consider reducing for better responsiveness.`
71
+ );
72
+ }
73
+ }
74
+
75
+ // Log validation results
76
+ if (errors.length > 0) {
77
+ logger.error("[skills-scanner] Configuration validation failed", {
78
+ errors,
79
+ });
80
+ }
81
+
82
+ if (warnings.length > 0) {
83
+ logger.warn("[skills-scanner] Configuration warnings", {
84
+ warnings,
85
+ });
86
+ }
87
+
88
+ return {
89
+ valid: errors.length === 0,
90
+ errors,
91
+ warnings,
92
+ };
93
+ }
94
+