@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/README.md +3 -509
- package/index.ts +210 -212
- package/openclaw.plugin.json +112 -70
- package/package.json +64 -49
- package/skills/skills-scanner/SKILL.md +245 -1062
- 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/commands.ts
CHANGED
|
@@ -2,57 +2,43 @@
|
|
|
2
2
|
* Command handlers module
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync
|
|
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 {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
logger: any
|
|
19
|
+
logger: PluginLogger,
|
|
20
|
+
apiConfig?: any
|
|
31
21
|
) {
|
|
32
|
-
async function handleScanCommand(args: string): Promise<
|
|
22
|
+
async function handleScanCommand(args: string): Promise<CommandResponse> {
|
|
33
23
|
if (!args) {
|
|
34
24
|
return {
|
|
35
|
-
text: "用法:`/skills-scanner scan <路径> [--detailed] [--behavioral] [--recursive]
|
|
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 (
|
|
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(
|
|
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: `⚠️
|
|
63
|
+
return { text: `⚠️ 路径不存在:${targetPath}` };
|
|
97
64
|
}
|
|
98
65
|
|
|
99
66
|
const isSingleSkill = existsSync(join(targetPath, "SKILL.md"));
|
|
100
67
|
|
|
101
68
|
if (isSingleSkill) {
|
|
102
|
-
|
|
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(
|
|
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<
|
|
126
|
-
const state = loadState()
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
}
|
|
171
|
-
lines.push(`API
|
|
172
|
-
lines.push(
|
|
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
|
-
|
|
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<
|
|
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: "
|
|
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
|
+
|