@pwddd/skills-scanner 1.0.0-beta.4 → 1.0.0-beta.6
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cron-manager.ts +216 -191
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/cron-manager.ts
CHANGED
|
@@ -1,237 +1,262 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* 使用 Plugin SDK 提供的 cron store API 来管理定时任务
|
|
5
|
-
*/
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import type { Logger } from "@openclaw/plugin-sdk";
|
|
6
3
|
|
|
7
|
-
import {
|
|
8
|
-
loadCronStore,
|
|
9
|
-
resolveCronStorePath,
|
|
10
|
-
saveCronStore,
|
|
11
|
-
} from "openclaw/plugin-sdk/config-runtime";
|
|
12
|
-
import type { PluginLogger } from "./types.js";
|
|
13
|
-
|
|
14
|
-
// Cron job configuration
|
|
15
4
|
const CRON_JOB_NAME = "skills-weekly-report";
|
|
16
|
-
const CRON_SCHEDULE = "5 12 * * 1"; //
|
|
5
|
+
const CRON_SCHEDULE = "5 12 * * 1"; // Every Monday at 12:05
|
|
17
6
|
const CRON_TIMEZONE = "Asia/Shanghai";
|
|
18
7
|
|
|
19
8
|
export interface CronManagerOptions {
|
|
20
|
-
logger:
|
|
21
|
-
config
|
|
9
|
+
logger: Logger;
|
|
10
|
+
config: any;
|
|
22
11
|
}
|
|
23
12
|
|
|
24
13
|
/**
|
|
25
|
-
*
|
|
14
|
+
* Detect the correct OpenClaw command (openclaw vs npx openclaw)
|
|
26
15
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
16
|
+
function getOpenClawCommand(): string {
|
|
17
|
+
// 1. Check environment variable
|
|
18
|
+
if (process.env.OPENCLAW_CLI_PATH) {
|
|
19
|
+
return process.env.OPENCLAW_CLI_PATH;
|
|
20
|
+
}
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
// 2. Check if running via npx
|
|
23
|
+
const argv1 = process.argv[1];
|
|
24
|
+
if (argv1?.includes("npx") || argv1?.includes("_npx")) {
|
|
25
|
+
return "npx openclaw";
|
|
26
|
+
}
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
if (process.env.npm_execpath?.includes("npx")) {
|
|
29
|
+
return "npx openclaw";
|
|
30
|
+
}
|
|
38
31
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
expr: CRON_SCHEDULE,
|
|
51
|
-
tz: CRON_TIMEZONE,
|
|
52
|
-
};
|
|
53
|
-
existingJob.updatedAtMs = Date.now();
|
|
54
|
-
await saveCronStore(storePath, store);
|
|
55
|
-
logger.info("[Cron] ✅ 任务配置已更新");
|
|
56
|
-
} else {
|
|
57
|
-
logger.info(`[Cron] ✅ 任务已存在: ${CRON_JOB_NAME}`);
|
|
58
|
-
}
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
32
|
+
// 3. Try global openclaw command
|
|
33
|
+
try {
|
|
34
|
+
execSync("openclaw --version", {
|
|
35
|
+
encoding: "utf-8",
|
|
36
|
+
timeout: 3000,
|
|
37
|
+
stdio: "pipe"
|
|
38
|
+
});
|
|
39
|
+
return "openclaw";
|
|
40
|
+
} catch {
|
|
41
|
+
// openclaw command not available
|
|
42
|
+
}
|
|
61
43
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
payload: {
|
|
73
|
-
kind: "agentTurn" as const,
|
|
74
|
-
message: "生成 Skills 安全周报。请扫描所有已配置的 Skills 目录,汇总本周的安全发现,并生成详细报告。",
|
|
75
|
-
timeoutSeconds: 600, // 10 分钟超时
|
|
76
|
-
},
|
|
77
|
-
delivery: {
|
|
78
|
-
mode: "announce" as const,
|
|
79
|
-
channel: "last" as const,
|
|
80
|
-
},
|
|
81
|
-
failureAlert: {
|
|
82
|
-
after: 2,
|
|
83
|
-
channel: "last" as const,
|
|
84
|
-
mode: "announce" as const,
|
|
85
|
-
},
|
|
86
|
-
createdAtMs: Date.now(),
|
|
87
|
-
state: {},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
store.jobs.push(newJob as any);
|
|
91
|
-
await saveCronStore(storePath, store);
|
|
92
|
-
|
|
93
|
-
logger.info(`[Cron] ✅ 定时任务已注册: ${CRON_JOB_NAME}`);
|
|
94
|
-
logger.info(`[Cron] 调度: ${CRON_SCHEDULE} (${CRON_TIMEZONE})`);
|
|
95
|
-
} catch (err: any) {
|
|
96
|
-
logger.error(`[Cron] ❌ 注册定时任务失败: ${err.message}`);
|
|
97
|
-
throw err;
|
|
44
|
+
// 4. Try npx as fallback
|
|
45
|
+
try {
|
|
46
|
+
execSync("npx openclaw --version", {
|
|
47
|
+
encoding: "utf-8",
|
|
48
|
+
timeout: 5000,
|
|
49
|
+
stdio: "pipe"
|
|
50
|
+
});
|
|
51
|
+
return "npx openclaw";
|
|
52
|
+
} catch {
|
|
53
|
+
// npx also not available
|
|
98
54
|
}
|
|
55
|
+
|
|
56
|
+
// 5. Default to openclaw (will fail with clear error)
|
|
57
|
+
return "openclaw";
|
|
99
58
|
}
|
|
100
59
|
|
|
101
60
|
/**
|
|
102
|
-
*
|
|
61
|
+
* Ensure cron job exists (create if not exists, update if config changed)
|
|
103
62
|
*/
|
|
104
|
-
export async function
|
|
105
|
-
options
|
|
106
|
-
|
|
107
|
-
const { logger, config } = options;
|
|
63
|
+
export async function ensureCronJob(options: CronManagerOptions): Promise<void> {
|
|
64
|
+
const { logger } = options;
|
|
65
|
+
const openclawCmd = getOpenClawCommand();
|
|
108
66
|
|
|
67
|
+
logger.info(`[skills-scanner] Using CLI command: ${openclawCmd}`);
|
|
68
|
+
|
|
69
|
+
// Test if command is available
|
|
109
70
|
try {
|
|
110
|
-
const
|
|
111
|
-
|
|
71
|
+
const testResult = execSync(`${openclawCmd} --version`, {
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
stdio: "pipe"
|
|
75
|
+
});
|
|
76
|
+
logger.info(`[skills-scanner] Command test successful: ${testResult.trim()}`);
|
|
77
|
+
} catch (testErr: any) {
|
|
78
|
+
logger.error(`[skills-scanner] ❌ Command not available: ${testErr.message}`);
|
|
79
|
+
logger.info(`[skills-scanner] 💡 Please ensure OpenClaw is installed and accessible`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Get cron list output (text format)
|
|
85
|
+
const listResult = execSync(`${openclawCmd} cron list`, {
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
timeout: 5000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Parse text output to find existing jobs
|
|
91
|
+
const lines = listResult.split("\n");
|
|
92
|
+
const existingJobs: Array<{ id: string; name: string }> = [];
|
|
112
93
|
|
|
113
|
-
const
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (line.includes(CRON_JOB_NAME)) {
|
|
96
|
+
const uuidMatch = line.match(/^([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/);
|
|
97
|
+
if (uuidMatch) {
|
|
98
|
+
existingJobs.push({ id: uuidMatch[1], name: CRON_JOB_NAME });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
114
102
|
|
|
115
|
-
//
|
|
116
|
-
|
|
103
|
+
// If multiple jobs exist with the same name, remove duplicates
|
|
104
|
+
if (existingJobs.length > 1) {
|
|
105
|
+
logger.warn(`[skills-scanner] ⚠️ Found ${existingJobs.length} duplicate jobs, cleaning up...`);
|
|
117
106
|
|
|
118
|
-
|
|
107
|
+
// Keep the first one, remove the rest
|
|
108
|
+
for (let i = 1; i < existingJobs.length; i++) {
|
|
109
|
+
const jobId = existingJobs[i].id;
|
|
110
|
+
try {
|
|
111
|
+
execSync(`${openclawCmd} cron remove ${jobId}`, {
|
|
112
|
+
encoding: "utf-8",
|
|
113
|
+
timeout: 5000,
|
|
114
|
+
});
|
|
115
|
+
logger.info(`[skills-scanner] ✅ Removed duplicate job: ${jobId}`);
|
|
116
|
+
} catch (removeErr: any) {
|
|
117
|
+
logger.warn(`[skills-scanner] ⚠️ Failed to remove duplicate job ${jobId}: ${removeErr.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
119
121
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
// Check if we have an existing job (after cleanup)
|
|
123
|
+
const existingJob = existingJobs.length > 0 ? existingJobs[0] : null;
|
|
124
|
+
|
|
125
|
+
if (existingJob) {
|
|
126
|
+
const jobId = existingJob.id;
|
|
127
|
+
logger.info(`[skills-scanner] ✅ Job already exists: ${jobId}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.info("[skills-scanner] 📝 Creating cron job via CLI...");
|
|
132
|
+
|
|
133
|
+
// Create cron job with --announce and --channel last
|
|
134
|
+
const cronCmd = [
|
|
135
|
+
`${openclawCmd} cron add`,
|
|
136
|
+
`--name "${CRON_JOB_NAME}"`,
|
|
137
|
+
`--cron "${CRON_SCHEDULE}"`,
|
|
138
|
+
`--tz "${CRON_TIMEZONE}"`,
|
|
139
|
+
"--session isolated",
|
|
140
|
+
'--message "请执行 /skills-scanner scan --report 并将结果发送到本频道"',
|
|
141
|
+
"--announce",
|
|
142
|
+
"--channel last",
|
|
143
|
+
].join(" ");
|
|
144
|
+
|
|
145
|
+
logger.info(`[skills-scanner] Executing: ${cronCmd}`);
|
|
146
|
+
|
|
147
|
+
const result = execSync(cronCmd, { encoding: "utf-8", timeout: 10000 });
|
|
148
|
+
|
|
149
|
+
const jobIdMatch =
|
|
150
|
+
result.match(/Job ID[:\s]+([a-zA-Z0-9-]+)/i) ||
|
|
151
|
+
result.match(/jobId[:\s]+([a-zA-Z0-9-]+)/i) ||
|
|
152
|
+
result.match(/id[:\s]+([a-zA-Z0-9-]+)/i);
|
|
153
|
+
|
|
154
|
+
if (jobIdMatch) {
|
|
155
|
+
const cronJobId = jobIdMatch[1];
|
|
156
|
+
logger.info(`[skills-scanner] ✅ Job created successfully: ${cronJobId}`);
|
|
157
|
+
logger.info(`[skills-scanner] 📅 Schedule: Every Monday at 12:05 (${CRON_TIMEZONE})`);
|
|
158
|
+
logger.info("[skills-scanner] 📬 Reports will be delivered to the last active channel");
|
|
123
159
|
} else {
|
|
124
|
-
logger.info("[
|
|
160
|
+
logger.info("[skills-scanner] ✅ Job creation command executed");
|
|
161
|
+
logger.debug(`[skills-scanner] Output: ${result.trim()}`);
|
|
125
162
|
}
|
|
126
163
|
} catch (err: any) {
|
|
127
|
-
logger.
|
|
128
|
-
|
|
164
|
+
logger.warn("[skills-scanner] ⚠️ Auto-registration failed");
|
|
165
|
+
logger.warn(`[skills-scanner] Error: ${err.message || err}`);
|
|
166
|
+
|
|
167
|
+
if (err.stderr) {
|
|
168
|
+
logger.warn(`[skills-scanner] stderr: ${err.stderr}`);
|
|
169
|
+
}
|
|
170
|
+
if (err.stdout) {
|
|
171
|
+
logger.warn(`[skills-scanner] stdout: ${err.stdout}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (err.message.includes("permission") || err.message.includes("EACCES")) {
|
|
175
|
+
logger.error("[skills-scanner] ❌ Permission denied, please run with admin privileges");
|
|
176
|
+
} else if (
|
|
177
|
+
err.message.includes("command not found") ||
|
|
178
|
+
err.message.includes("ENOENT")
|
|
179
|
+
) {
|
|
180
|
+
logger.error(`[skills-scanner] ❌ ${openclawCmd} command not found, please check installation`);
|
|
181
|
+
} else {
|
|
182
|
+
logger.info("[skills-scanner] 💡 Please manually register cron job:");
|
|
183
|
+
logger.info("[skills-scanner]");
|
|
184
|
+
logger.info(`[skills-scanner] ${openclawCmd} cron add \\`);
|
|
185
|
+
logger.info(`[skills-scanner] --name "${CRON_JOB_NAME}" \\`);
|
|
186
|
+
logger.info(`[skills-scanner] --cron "${CRON_SCHEDULE}" \\`);
|
|
187
|
+
logger.info(`[skills-scanner] --tz "${CRON_TIMEZONE}" \\`);
|
|
188
|
+
logger.info("[skills-scanner] --session isolated \\");
|
|
189
|
+
logger.info('[skills-scanner] --message "请执行 /skills-scanner scan --report 并将结果发送到本频道" \\');
|
|
190
|
+
logger.info("[skills-scanner] --announce \\");
|
|
191
|
+
logger.info("[skills-scanner] --channel last");
|
|
192
|
+
logger.info("[skills-scanner]");
|
|
193
|
+
}
|
|
129
194
|
}
|
|
130
195
|
}
|
|
131
196
|
|
|
132
197
|
/**
|
|
133
|
-
*
|
|
198
|
+
* Cleanup cron job (called on plugin uninstall)
|
|
134
199
|
*/
|
|
135
|
-
export async function
|
|
136
|
-
options
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
export async function cleanupCronJob(options: CronManagerOptions): Promise<void> {
|
|
201
|
+
const { logger } = options;
|
|
202
|
+
const openclawCmd = getOpenClawCommand();
|
|
203
|
+
|
|
204
|
+
logger.info(`[skills-scanner] 🗑️ Cleaning up cron job: ${CRON_JOB_NAME}`);
|
|
139
205
|
|
|
140
206
|
try {
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"任务未注册。",
|
|
151
|
-
"",
|
|
152
|
-
"请重启插件或手动注册:",
|
|
153
|
-
"```bash",
|
|
154
|
-
"/skills-scanner cron setup",
|
|
155
|
-
"```",
|
|
156
|
-
].join("\n");
|
|
207
|
+
// Get cron list output (text format)
|
|
208
|
+
const listResult = execSync(`${openclawCmd} cron list`, {
|
|
209
|
+
encoding: "utf-8",
|
|
210
|
+
timeout: 5000,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!listResult.includes(CRON_JOB_NAME)) {
|
|
214
|
+
logger.info(`[skills-scanner] No cron job to remove: ${CRON_JOB_NAME}`);
|
|
215
|
+
return;
|
|
157
216
|
}
|
|
158
217
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
: "未知";
|
|
164
|
-
|
|
165
|
-
const lastRun = job.state?.lastRunAtMs
|
|
166
|
-
? new Date(job.state.lastRunAtMs).toLocaleString("zh-CN", {
|
|
167
|
-
timeZone: CRON_TIMEZONE,
|
|
168
|
-
})
|
|
169
|
-
: "从未运行";
|
|
170
|
-
|
|
171
|
-
const lastStatus = job.state?.lastRunStatus || "未知";
|
|
172
|
-
|
|
173
|
-
return [
|
|
174
|
-
"✅ *定时任务状态*",
|
|
175
|
-
"",
|
|
176
|
-
`• **任务 ID**: \`${(job as any).id}\``,
|
|
177
|
-
`• **任务名称**: ${job.name}`,
|
|
178
|
-
`• **状态**: ${job.enabled ? "✅ 已启用" : "❌ 已禁用"}`,
|
|
179
|
-
`• **调度**: \`${CRON_SCHEDULE}\` (${CRON_TIMEZONE})`,
|
|
180
|
-
`• **下次运行**: ${nextRun}`,
|
|
181
|
-
`• **上次运行**: ${lastRun}`,
|
|
182
|
-
`• **上次状态**: ${lastStatus}`,
|
|
183
|
-
"",
|
|
184
|
-
"管理命令:",
|
|
185
|
-
"```bash",
|
|
186
|
-
`openclaw cron list | grep ${CRON_JOB_NAME}`,
|
|
187
|
-
`openclaw cron run ${(job as any).id}`,
|
|
188
|
-
`openclaw cron update ${(job as any).id} --enabled false`,
|
|
189
|
-
"```",
|
|
190
|
-
].join("\n");
|
|
191
|
-
} catch (err: any) {
|
|
192
|
-
logger.error(`[Cron] ❌ 获取任务状态失败: ${err.message}`);
|
|
193
|
-
return `❌ 获取任务状态失败: ${err.message}`;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
218
|
+
// Parse text output to extract job IDs
|
|
219
|
+
// Format: ID (UUID) followed by Name
|
|
220
|
+
const lines = listResult.split("\n");
|
|
221
|
+
const jobIdsToRemove: string[] = [];
|
|
196
222
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
// Check if line contains the job name
|
|
225
|
+
if (line.includes(CRON_JOB_NAME)) {
|
|
226
|
+
// Extract UUID from the beginning of the line
|
|
227
|
+
// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
228
|
+
const uuidMatch = line.match(/^([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/);
|
|
229
|
+
if (uuidMatch) {
|
|
230
|
+
jobIdsToRemove.push(uuidMatch[1]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
204
234
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
if (jobIdsToRemove.length === 0) {
|
|
236
|
+
logger.info(`[skills-scanner] No cron job to remove: ${CRON_JOB_NAME}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
208
239
|
|
|
209
|
-
|
|
240
|
+
logger.info(`[skills-scanner] Found ${jobIdsToRemove.length} job(s) to remove`);
|
|
210
241
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
242
|
+
// Remove all matching jobs
|
|
243
|
+
let successCount = 0;
|
|
244
|
+
for (const jobId of jobIdsToRemove) {
|
|
245
|
+
try {
|
|
246
|
+
execSync(`${openclawCmd} cron remove ${jobId}`, {
|
|
247
|
+
encoding: "utf-8",
|
|
248
|
+
timeout: 5000,
|
|
249
|
+
});
|
|
250
|
+
logger.info(`[skills-scanner] ✅ Removed cron job: ${jobId}`);
|
|
251
|
+
successCount++;
|
|
252
|
+
} catch (removeErr: any) {
|
|
253
|
+
logger.warn(`[skills-scanner] ⚠️ Failed to remove job ${jobId}: ${removeErr.message}`);
|
|
254
|
+
}
|
|
217
255
|
}
|
|
218
256
|
|
|
219
|
-
|
|
220
|
-
"💡 *手动触发任务*",
|
|
221
|
-
"",
|
|
222
|
-
"请使用 CLI 命令手动触发:",
|
|
223
|
-
"```bash",
|
|
224
|
-
`openclaw cron run ${(job as any).id} --force`,
|
|
225
|
-
"```",
|
|
226
|
-
"",
|
|
227
|
-
"或者直接运行报告生成:",
|
|
228
|
-
"```bash",
|
|
229
|
-
"/skills-scanner scan --report",
|
|
230
|
-
"```",
|
|
231
|
-
].join("\n");
|
|
257
|
+
logger.info(`[skills-scanner] ✅ Cleanup complete, removed ${successCount}/${jobIdsToRemove.length} job(s)`);
|
|
232
258
|
} catch (err: any) {
|
|
233
|
-
logger.error(`[
|
|
234
|
-
return `❌ 获取任务信息失败: ${err.message}`;
|
|
259
|
+
logger.error(`[skills-scanner] ❌ Failed to cleanup cron job: ${err.message}`);
|
|
235
260
|
}
|
|
236
261
|
}
|
|
237
262
|
|