@pwddd/skills-scanner 3.0.23 → 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/README.md +3 -509
- package/index.ts +209 -211
- package/openclaw.plugin.json +112 -70
- package/package.json +64 -49
- package/skills/skills-scanner/SKILL.md +245 -1065
- package/src/api-client.ts +275 -0
- package/src/before-install-hook.ts +274 -0
- package/src/cache.ts +138 -0
- package/src/commands.ts +56 -152
- package/src/config-validator.ts +94 -0
- package/src/config.ts +187 -170
- package/src/cron-manager.ts +158 -0
- package/src/debug.ts +40 -0
- package/src/error-handler.ts +103 -0
- package/src/metrics.ts +140 -0
- package/src/prompt-guidance.ts +42 -250
- package/src/rate-limiter.ts +102 -0
- package/src/scanner.ts +230 -54
- package/src/state.ts +119 -71
- package/src/structured-logger.ts +97 -0
- package/src/types.ts +72 -50
- package/skills/skills-scanner/__pycache__/scan.cpython-314.pyc +0 -0
- package/skills/skills-scanner/scan.py +0 -446
- package/src/cron.ts +0 -292
- package/src/deps.ts +0 -77
- package/src/high-risk-operation-guard.ts +0 -62
- package/src/prompt-injection-guard.ts +0 -56
- package/src/report.ts +0 -100
- package/src/watcher.ts +0 -125
package/src/config.ts
CHANGED
|
@@ -1,170 +1,187 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 配置管理模块
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Type } from "@sinclair/typebox";
|
|
6
|
-
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
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
|
-
// 验证
|
|
28
|
-
if (config.
|
|
29
|
-
return {
|
|
30
|
-
success: false,
|
|
31
|
-
error: {
|
|
32
|
-
issues: [{
|
|
33
|
-
path: ["
|
|
34
|
-
message: "
|
|
35
|
-
}]
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
label: "
|
|
70
|
-
help: "
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
label: "
|
|
83
|
-
help: "
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"
|
|
131
|
-
"",
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"",
|
|
153
|
-
"
|
|
154
|
-
"",
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"",
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
// 验证 onUnsafe
|
|
28
|
+
if (config.onUnsafe && !["quarantine", "delete", "warn"].includes(config.onUnsafe)) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: {
|
|
32
|
+
issues: [{
|
|
33
|
+
path: ["onUnsafe"],
|
|
34
|
+
message: "onUnsafe 必须是 quarantine、delete 或 warn"
|
|
35
|
+
}]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { success: true, data: config };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: {
|
|
45
|
+
issues: [{
|
|
46
|
+
path: [],
|
|
47
|
+
message: String(err)
|
|
48
|
+
}]
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
uiHints: {
|
|
55
|
+
apiUrl: {
|
|
56
|
+
label: "API 服务地址",
|
|
57
|
+
help: "扫描 API 服务的 URL 地址",
|
|
58
|
+
placeholder: "https://110.vemic.com/skills-scanner",
|
|
59
|
+
type: "string",
|
|
60
|
+
format: "uri",
|
|
61
|
+
},
|
|
62
|
+
behavioral: {
|
|
63
|
+
label: "行为分析",
|
|
64
|
+
help: "启用深度行为分析(较慢但更准确)。检测运行时行为模式,如网络请求、文件操作等",
|
|
65
|
+
type: "boolean",
|
|
66
|
+
default: false,
|
|
67
|
+
},
|
|
68
|
+
useLLM: {
|
|
69
|
+
label: "LLM 分析",
|
|
70
|
+
help: "使用 LLM 进行语义分析。可以检测更复杂的恶意模式,但需要更多时间和资源",
|
|
71
|
+
type: "boolean",
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
policy: {
|
|
75
|
+
label: "扫描策略",
|
|
76
|
+
help: "strict=严格(零容忍)/ balanced=平衡(推荐,误报率低)/ permissive=宽松(仅检测明显威胁)",
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: ["strict", "balanced", "permissive"],
|
|
79
|
+
default: "balanced",
|
|
80
|
+
},
|
|
81
|
+
onUnsafe: {
|
|
82
|
+
label: "不安全处理",
|
|
83
|
+
help: "warn=仅警告(推荐,不影响使用)/ quarantine=隔离到隔离区 / delete=直接删除(危险)",
|
|
84
|
+
type: "string",
|
|
85
|
+
enum: ["warn", "quarantine", "delete"],
|
|
86
|
+
default: "warn",
|
|
87
|
+
},
|
|
88
|
+
enableBeforeInstallHook: {
|
|
89
|
+
label: "安装前拦截",
|
|
90
|
+
help: "启用 before_install hook,在安装前强制拦截不安全的 Skills(强烈推荐)。这是最后一道防线",
|
|
91
|
+
type: "boolean",
|
|
92
|
+
default: true,
|
|
93
|
+
},
|
|
94
|
+
injectSecurityGuidance: {
|
|
95
|
+
label: "注入安全指导",
|
|
96
|
+
help: "在系统提示中注入安全指导,提醒 AI 注意 Skills 安全问题",
|
|
97
|
+
type: "boolean",
|
|
98
|
+
default: true,
|
|
99
|
+
},
|
|
100
|
+
scanTimeoutMs: {
|
|
101
|
+
label: "扫描超时(毫秒)",
|
|
102
|
+
help: "单次扫描的最大时长,超时后自动取消。默认 180000ms (3分钟)",
|
|
103
|
+
type: "number",
|
|
104
|
+
minimum: 10000,
|
|
105
|
+
maximum: 600000,
|
|
106
|
+
default: 180000,
|
|
107
|
+
},
|
|
108
|
+
reportDir: {
|
|
109
|
+
label: "报告目录",
|
|
110
|
+
help: "扫描报告保存目录。留空则使用默认位置",
|
|
111
|
+
type: "string",
|
|
112
|
+
},
|
|
113
|
+
quarantineDir: {
|
|
114
|
+
label: "隔离目录",
|
|
115
|
+
help: "隔离文件保存目录。留空则使用默认位置",
|
|
116
|
+
type: "string",
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export function generateConfigGuide(
|
|
122
|
+
cfg: ScannerConfig,
|
|
123
|
+
apiUrl: string,
|
|
124
|
+
behavioral: boolean,
|
|
125
|
+
useLLM: boolean,
|
|
126
|
+
policy: string,
|
|
127
|
+
onUnsafe: string
|
|
128
|
+
): string {
|
|
129
|
+
return [
|
|
130
|
+
"",
|
|
131
|
+
"╔════════════════════════════════════════════════════════════════╗",
|
|
132
|
+
"║ 🎉 Skills Scanner 首次运行 - 配置向导 ║",
|
|
133
|
+
"╚════════════════════════════════════════════════════════════════╝",
|
|
134
|
+
"",
|
|
135
|
+
"当前使用默认配置。建议根据您的需求自定义配置:",
|
|
136
|
+
"",
|
|
137
|
+
"📋 当前配置:",
|
|
138
|
+
` • API 服务地址:${apiUrl}`,
|
|
139
|
+
` • 行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
|
|
140
|
+
` • LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
|
|
141
|
+
` • 扫描策略:${policy}`,
|
|
142
|
+
` • 目录监控:❌ 已禁用(需手动扫描)`,
|
|
143
|
+
` • 不安全处理:${onUnsafe}`,
|
|
144
|
+
"",
|
|
145
|
+
"🔧 配置文件位置:",
|
|
146
|
+
" ~/.openclaw/config.json",
|
|
147
|
+
"",
|
|
148
|
+
"📝 推荐配置示例:",
|
|
149
|
+
"",
|
|
150
|
+
"```json",
|
|
151
|
+
"{",
|
|
152
|
+
' "plugins": {',
|
|
153
|
+
' "entries": {',
|
|
154
|
+
' "skills-scanner": {',
|
|
155
|
+
' "enabled": true,',
|
|
156
|
+
' "config": {',
|
|
157
|
+
' "apiUrl": "https://110.vemic.com/skills-scanner",',
|
|
158
|
+
' "behavioral": false,',
|
|
159
|
+
' "useLLM": false,',
|
|
160
|
+
' "policy": "balanced",',
|
|
161
|
+
' "onUnsafe": "warn"',
|
|
162
|
+
' }',
|
|
163
|
+
' }',
|
|
164
|
+
' }',
|
|
165
|
+
' }',
|
|
166
|
+
"}",
|
|
167
|
+
"```",
|
|
168
|
+
"",
|
|
169
|
+
"💡 配置说明:",
|
|
170
|
+
"",
|
|
171
|
+
"1. apiUrl 默认 https://110.vemic.com/skills-scanner",
|
|
172
|
+
"2. behavioral false=快速扫描(推荐),true=深度分析",
|
|
173
|
+
"3. useLLM false=不使用 LLM(推荐),true=语义分析",
|
|
174
|
+
"4. policy strict / balanced(推荐)/ permissive",
|
|
175
|
+
"5. onUnsafe warn=仅警告(推荐),quarantine=隔离,delete=删除",
|
|
176
|
+
"",
|
|
177
|
+
"⚠️ 注意:目录监控功能已禁用,请使用以下命令手动扫描:",
|
|
178
|
+
" /skills-scanner scan <路径>",
|
|
179
|
+
"",
|
|
180
|
+
"🚀 快速开始:",
|
|
181
|
+
" 编辑配置文件后重启 Gateway",
|
|
182
|
+
" /skills-scanner health",
|
|
183
|
+
"",
|
|
184
|
+
"提示:此消息只在首次运行时显示。",
|
|
185
|
+
"════════════════════════════════════════════════════════════════",
|
|
186
|
+
].join("\n");
|
|
187
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { exec, execSync } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { Logger } from "@openclaw/plugin-sdk";
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
const CRON_JOB_NAME = "skills-weekly-report";
|
|
8
|
+
const CLEANUP_JOB_NAME = "skills-scanner-cleanup";
|
|
9
|
+
const CRON_SCHEDULE = "5 12 * * 1"; // Every Monday at 12:05
|
|
10
|
+
const CLEANUP_SCHEDULE = "0 3 * * *"; // Every day at 3:00 AM
|
|
11
|
+
const CRON_TIMEZONE = "Asia/Shanghai";
|
|
12
|
+
|
|
13
|
+
export interface CronManagerOptions {
|
|
14
|
+
logger: Logger;
|
|
15
|
+
config: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect the correct OpenClaw command (openclaw vs npx openclaw)
|
|
20
|
+
*/
|
|
21
|
+
function getOpenClawCommand(): string {
|
|
22
|
+
// 1. Check environment variable
|
|
23
|
+
if (process.env.OPENCLAW_CLI_PATH) {
|
|
24
|
+
return process.env.OPENCLAW_CLI_PATH;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Check if running via npx
|
|
28
|
+
const argv1 = process.argv[1];
|
|
29
|
+
if (argv1?.includes("npx") || argv1?.includes("_npx")) {
|
|
30
|
+
return "npx openclaw";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (process.env.npm_execpath?.includes("npx")) {
|
|
34
|
+
return "npx openclaw";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Try global openclaw command
|
|
38
|
+
try {
|
|
39
|
+
execSync("openclaw --version", {
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
timeout: 3000,
|
|
42
|
+
stdio: "pipe"
|
|
43
|
+
});
|
|
44
|
+
return "openclaw";
|
|
45
|
+
} catch {
|
|
46
|
+
// openclaw command not available
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 4. Try npx as fallback
|
|
50
|
+
try {
|
|
51
|
+
execSync("npx openclaw --version", {
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
timeout: 5000,
|
|
54
|
+
stdio: "pipe"
|
|
55
|
+
});
|
|
56
|
+
return "npx openclaw";
|
|
57
|
+
} catch {
|
|
58
|
+
// npx also not available
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 5. Default to openclaw (will fail with clear error)
|
|
62
|
+
return "openclaw";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ensure system crontab exists for cleanup
|
|
67
|
+
*/
|
|
68
|
+
export async function ensureCronJob(options: CronManagerOptions): Promise<void> {
|
|
69
|
+
const { logger } = options;
|
|
70
|
+
|
|
71
|
+
logger.info(`[skills-scanner] Setting up system crontab for cleanup...`);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Check if crontab entry already exists
|
|
75
|
+
const { stdout: currentCrontab } = await execAsync("crontab -l", {
|
|
76
|
+
timeout: 5000,
|
|
77
|
+
}).catch(() => ({ stdout: "" }));
|
|
78
|
+
|
|
79
|
+
const cleanupMarker = "# skills-scanner cleanup";
|
|
80
|
+
|
|
81
|
+
if (currentCrontab.includes(cleanupMarker)) {
|
|
82
|
+
logger.info(`[skills-scanner] System crontab already exists`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create cleanup command - remove ALL skills-weekly-report jobs
|
|
87
|
+
// Escape $(...) and $variables once: \\$ in JS → \$ in string → $ in crontab
|
|
88
|
+
const openclawCmd = getOpenClawCommand();
|
|
89
|
+
const cleanupCmd = `for id in \\$(${openclawCmd} cron list | grep skills-weekly-report | awk '{print \\$1}'); do ${openclawCmd} cron remove \\$id; done > /dev/null 2>&1`;
|
|
90
|
+
|
|
91
|
+
// Add new crontab entry (comment on separate line) - runs at 2:00 AM daily
|
|
92
|
+
const newCrontabEntry = `${cleanupMarker}\n0 4 * * * ${cleanupCmd}`;
|
|
93
|
+
const newCrontab = currentCrontab ? `${currentCrontab}\n${newCrontabEntry}` : newCrontabEntry;
|
|
94
|
+
|
|
95
|
+
// Install new crontab
|
|
96
|
+
await execAsync(`echo "${newCrontab.replace(/"/g, '\\"')}" | crontab -`, {
|
|
97
|
+
timeout: 5000,
|
|
98
|
+
shell: "/bin/bash",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
logger.info(`[skills-scanner] ✅ System crontab created: runs daily at 3:00 AM`);
|
|
102
|
+
} catch (err: any) {
|
|
103
|
+
logger.warn(`[skills-scanner] ⚠️ Failed to setup system crontab: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Cleanup system crontab (called on plugin uninstall)
|
|
109
|
+
*/
|
|
110
|
+
export async function cleanupCronJob(options: CronManagerOptions): Promise<void> {
|
|
111
|
+
const { logger } = options;
|
|
112
|
+
|
|
113
|
+
logger.info(`[skills-scanner] Removing system crontab...`);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Get current crontab
|
|
117
|
+
const { stdout: currentCrontab } = await execAsync("crontab -l", {
|
|
118
|
+
timeout: 5000,
|
|
119
|
+
}).catch(() => ({ stdout: "" }));
|
|
120
|
+
|
|
121
|
+
const cleanupMarker = "# skills-scanner cleanup";
|
|
122
|
+
|
|
123
|
+
if (!currentCrontab.includes(cleanupMarker)) {
|
|
124
|
+
logger.info(`[skills-scanner] No system crontab to remove`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Remove the cleanup entry (marker line + cron line)
|
|
129
|
+
const lines = currentCrontab.split("\n");
|
|
130
|
+
const newLines: string[] = [];
|
|
131
|
+
let skipNext = false;
|
|
132
|
+
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
if (line.includes(cleanupMarker)) {
|
|
135
|
+
skipNext = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (skipNext) {
|
|
139
|
+
skipNext = false;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
newLines.push(line);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const newCrontab = newLines.join("\n").trim();
|
|
146
|
+
|
|
147
|
+
// Always update crontab, even if empty (don't use crontab -r)
|
|
148
|
+
await execAsync(`echo "${newCrontab.replace(/"/g, '\\"')}" | crontab -`, {
|
|
149
|
+
timeout: 5000,
|
|
150
|
+
shell: "/bin/bash",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
logger.info(`[skills-scanner] ✅ System crontab removed`);
|
|
154
|
+
} catch (err: any) {
|
|
155
|
+
logger.warn(`[skills-scanner] ⚠️ Failed to remove system crontab: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug utilities module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PluginLogger } from "./types.js";
|
|
6
|
+
|
|
7
|
+
// Check if debug mode is enabled via environment variable
|
|
8
|
+
const DEBUG_MODE = process.env.SKILLS_SCANNER_DEBUG === "1" ||
|
|
9
|
+
process.env.SKILLS_SCANNER_DEBUG === "true";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Log debug message if debug mode is enabled
|
|
13
|
+
*/
|
|
14
|
+
export function debugLog(logger: PluginLogger, message: string, data?: any): void {
|
|
15
|
+
if (DEBUG_MODE) {
|
|
16
|
+
if (data) {
|
|
17
|
+
logger.debug(`[skills-scanner:debug] ${message}`, data);
|
|
18
|
+
} else {
|
|
19
|
+
logger.debug(`[skills-scanner:debug] ${message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if debug mode is enabled
|
|
26
|
+
*/
|
|
27
|
+
export function isDebugMode(): boolean {
|
|
28
|
+
return DEBUG_MODE;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format debug data for logging
|
|
33
|
+
*/
|
|
34
|
+
export function formatDebugData(data: any): string {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(data, null, 2);
|
|
37
|
+
} catch {
|
|
38
|
+
return String(data);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a user-friendly error message with actionable suggestions
|
|
7
|
+
*/
|
|
8
|
+
export function createActionableError(
|
|
9
|
+
operation: string,
|
|
10
|
+
error: Error,
|
|
11
|
+
context?: {
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
): string {
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
|
|
19
|
+
// Main error message
|
|
20
|
+
lines.push(`❌ ${operation} 失败: ${error.message}`);
|
|
21
|
+
lines.push("");
|
|
22
|
+
|
|
23
|
+
// Context information
|
|
24
|
+
if (context) {
|
|
25
|
+
if (context.path) {
|
|
26
|
+
lines.push(`📁 文件路径: ${context.path}`);
|
|
27
|
+
}
|
|
28
|
+
if (context.apiUrl) {
|
|
29
|
+
lines.push(`🌐 API 地址: ${context.apiUrl}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Possible causes and solutions
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push("🔍 可能的原因:");
|
|
36
|
+
|
|
37
|
+
if (error.message.includes("ECONNREFUSED") || error.message.includes("fetch failed")) {
|
|
38
|
+
lines.push(" • API 服务不可用或无法连接");
|
|
39
|
+
lines.push(" • 网络连接问题");
|
|
40
|
+
lines.push(" • 防火墙阻止了连接");
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push("💡 解决方案:");
|
|
43
|
+
lines.push(" 1. 检查 API 服务状态: /skills-scanner health");
|
|
44
|
+
lines.push(" 2. 验证网络连接");
|
|
45
|
+
lines.push(" 3. 检查防火墙设置");
|
|
46
|
+
if (context?.apiUrl) {
|
|
47
|
+
lines.push(` 4. 尝试访问: ${context.apiUrl}/health`);
|
|
48
|
+
}
|
|
49
|
+
} else if (error.message.includes("timeout") || error.message.includes("ETIMEDOUT")) {
|
|
50
|
+
lines.push(" • 扫描操作超时");
|
|
51
|
+
lines.push(" • API 响应过慢");
|
|
52
|
+
lines.push(" • 网络延迟过高");
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push("💡 解决方案:");
|
|
55
|
+
lines.push(" 1. 增加超时时间配置 (scanTimeoutMs)");
|
|
56
|
+
lines.push(" 2. 检查网络质量");
|
|
57
|
+
lines.push(" 3. 稍后重试");
|
|
58
|
+
} else if (error.message.includes("ENOENT") || error.message.includes("not found")) {
|
|
59
|
+
lines.push(" • 文件或目录不存在");
|
|
60
|
+
lines.push(" • 路径配置错误");
|
|
61
|
+
lines.push(" • 文件已被移动或删除");
|
|
62
|
+
lines.push("");
|
|
63
|
+
lines.push("💡 解决方案:");
|
|
64
|
+
lines.push(" 1. 验证文件路径是否正确");
|
|
65
|
+
lines.push(" 2. 检查文件权限");
|
|
66
|
+
lines.push(" 3. 确认文件未被其他程序占用");
|
|
67
|
+
} else if (error.message.includes("EACCES") || error.message.includes("permission denied")) {
|
|
68
|
+
lines.push(" • 文件权限不足");
|
|
69
|
+
lines.push(" • 目录访问被拒绝");
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push("💡 解决方案:");
|
|
72
|
+
lines.push(" 1. 检查文件/目录权限");
|
|
73
|
+
lines.push(" 2. 确保当前用户有读写权限");
|
|
74
|
+
lines.push(" 3. 尝试使用管理员权限运行");
|
|
75
|
+
} else {
|
|
76
|
+
lines.push(" • 未知错误");
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push("💡 解决方案:");
|
|
79
|
+
lines.push(" 1. 查看完整错误日志");
|
|
80
|
+
lines.push(" 2. 启用调试模式: SKILLS_SCANNER_DEBUG=1");
|
|
81
|
+
lines.push(" 3. 联系技术支持");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Wrap an async operation with error handling
|
|
89
|
+
*/
|
|
90
|
+
export async function withErrorHandling<T>(
|
|
91
|
+
operation: string,
|
|
92
|
+
fn: () => Promise<T>,
|
|
93
|
+
context?: any
|
|
94
|
+
): Promise<T> {
|
|
95
|
+
try {
|
|
96
|
+
return await fn();
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
const message = createActionableError(operation, error, context);
|
|
99
|
+
const enhancedError = new Error(message);
|
|
100
|
+
enhancedError.stack = error.stack;
|
|
101
|
+
throw enhancedError;
|
|
102
|
+
}
|
|
103
|
+
}
|