@pwddd/skills-scanner 1.0.0-beta.3 → 1.0.0-beta.4
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/index.ts +37 -64
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands.ts +51 -51
- package/src/cron-manager.ts +173 -214
package/index.ts
CHANGED
|
@@ -21,7 +21,12 @@ import {
|
|
|
21
21
|
} from "./src/state.js";
|
|
22
22
|
import { runScan } from "./src/scanner.js";
|
|
23
23
|
import { buildDailyReport } from "./src/report.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
ensureCronJob,
|
|
26
|
+
cleanupCronJob,
|
|
27
|
+
getCronJobStatus,
|
|
28
|
+
triggerCronJob,
|
|
29
|
+
} from "./src/cron-manager.js";
|
|
25
30
|
import { startWatcher } from "./src/watcher.js";
|
|
26
31
|
import { createCommandHandlers } from "./src/commands.js";
|
|
27
32
|
import { SKILLS_SECURITY_GUIDANCE } from "./src/prompt-guidance.js";
|
|
@@ -143,75 +148,45 @@ export default definePluginEntry({
|
|
|
143
148
|
api.logger.warn("[skills-scanner] ⚠️ before_install hook DISABLED - installations will NOT be intercepted!");
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
// Register
|
|
147
|
-
api.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
logger: api.logger,
|
|
152
|
-
callGateway: async (method: string, params: any) => {
|
|
153
|
-
return await api.callGateway(method, params);
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
api.logger.info("[skills-scanner] ✅ Cron job check completed");
|
|
157
|
-
} catch (err: any) {
|
|
158
|
-
api.logger.error("[skills-scanner] ❌ Cron job registration failed", {
|
|
159
|
-
error: err.message,
|
|
160
|
-
stack: err.stack,
|
|
161
|
-
});
|
|
162
|
-
// Don't throw - avoid blocking gateway startup
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Register plugin_uninstall hook for cleanup
|
|
167
|
-
api.on("plugin_uninstall", async () => {
|
|
168
|
-
api.logger.info("[skills-scanner] 🗑️ Plugin uninstalling, cleaning up...");
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
// 1. Stop file watcher
|
|
172
|
-
if (stopWatcher) {
|
|
173
|
-
api.logger.debug("[skills-scanner] Stopping file watcher...");
|
|
174
|
-
stopWatcher();
|
|
175
|
-
stopWatcher = null;
|
|
176
|
-
api.logger.debug("[skills-scanner] ✅ File watcher stopped");
|
|
177
|
-
}
|
|
151
|
+
// Register service for initialization and cleanup
|
|
152
|
+
api.registerService({
|
|
153
|
+
id: "skills-scanner-cron-manager",
|
|
154
|
+
start: async () => {
|
|
155
|
+
api.logger.info("[skills-scanner] 🚀 Cron manager starting...");
|
|
178
156
|
|
|
179
|
-
// 2. Remove cron jobs
|
|
180
157
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
for (const job of ourJobs) {
|
|
186
|
-
const jobId = job.jobId || job.id;
|
|
187
|
-
await api.callGateway("cron.remove", { jobId });
|
|
188
|
-
api.logger.info("[skills-scanner] Removed cron job", { jobId });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (ourJobs.length > 0) {
|
|
192
|
-
api.logger.info(`[skills-scanner] ✅ Removed ${ourJobs.length} cron job(s)`);
|
|
193
|
-
}
|
|
194
|
-
} catch (err: any) {
|
|
195
|
-
api.logger.warn("[skills-scanner] Failed to remove cron jobs", {
|
|
196
|
-
error: err.message,
|
|
158
|
+
// 注册定时任务
|
|
159
|
+
await ensureCronJob({
|
|
160
|
+
logger: api.logger,
|
|
161
|
+
config: api.config,
|
|
197
162
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// 3. Save final state
|
|
201
|
-
try {
|
|
202
|
-
const finalState = loadState(api.runtime);
|
|
203
|
-
finalState.lastUninstallAt = new Date().toISOString();
|
|
204
|
-
saveState(finalState, api.runtime);
|
|
205
|
-
api.logger.debug("[skills-scanner] ✅ Final state saved");
|
|
163
|
+
api.logger.info("[skills-scanner] ✅ Cron job registered successfully");
|
|
206
164
|
} catch (err: any) {
|
|
207
|
-
api.logger.
|
|
165
|
+
api.logger.error("[skills-scanner] ❌ Failed to register cron job", {
|
|
208
166
|
error: err.message,
|
|
167
|
+
stack: err.stack,
|
|
209
168
|
});
|
|
169
|
+
// Don't throw - avoid blocking service startup
|
|
210
170
|
}
|
|
171
|
+
},
|
|
172
|
+
stop: async () => {
|
|
173
|
+
api.logger.info("[skills-scanner] 🛑 Cron manager stopping...");
|
|
174
|
+
// Cron 任务在卸载时清理,这里不需要操作
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Register plugin_uninstall hook for cleanup
|
|
179
|
+
api.on("plugin_uninstall", async () => {
|
|
180
|
+
api.logger.info("[skills-scanner] 🗑️ Plugin uninstalling, cleaning up cron jobs...");
|
|
211
181
|
|
|
212
|
-
|
|
182
|
+
try {
|
|
183
|
+
await cleanupCronJob({
|
|
184
|
+
logger: api.logger,
|
|
185
|
+
config: api.config,
|
|
186
|
+
});
|
|
187
|
+
api.logger.info("[skills-scanner] ✅ Cron jobs cleaned up");
|
|
213
188
|
} catch (err: any) {
|
|
214
|
-
api.logger.error("[skills-scanner] ❌
|
|
189
|
+
api.logger.error("[skills-scanner] ❌ Failed to cleanup cron jobs", {
|
|
215
190
|
error: err.message,
|
|
216
191
|
stack: err.stack,
|
|
217
192
|
});
|
|
@@ -470,9 +445,7 @@ export default definePluginEntry({
|
|
|
470
445
|
preInstallScan,
|
|
471
446
|
onUnsafe,
|
|
472
447
|
api.logger,
|
|
473
|
-
|
|
474
|
-
return await api.callGateway(method, params);
|
|
475
|
-
}
|
|
448
|
+
api.config // 传递 api.config
|
|
476
449
|
);
|
|
477
450
|
|
|
478
451
|
// Chat command: /skills-scanner
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -8,7 +8,10 @@ import { runScan } from "./scanner.js";
|
|
|
8
8
|
import { buildDailyReport } from "./report.js";
|
|
9
9
|
import { loadState, saveState, expandPath } from "./state.js";
|
|
10
10
|
import { generateConfigGuide } from "./config.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getCronJobStatus,
|
|
13
|
+
triggerCronJob,
|
|
14
|
+
} from "./cron-manager.js";
|
|
12
15
|
import type { ScannerConfig, CommandResponse, PluginLogger } from "./types.js";
|
|
13
16
|
|
|
14
17
|
export function createCommandHandlers(
|
|
@@ -21,7 +24,7 @@ export function createCommandHandlers(
|
|
|
21
24
|
preInstallScan: string,
|
|
22
25
|
onUnsafe: string,
|
|
23
26
|
logger: PluginLogger,
|
|
24
|
-
|
|
27
|
+
apiConfig?: any // 添加 api.config 参数
|
|
25
28
|
) {
|
|
26
29
|
async function handleScanCommand(args: string): Promise<CommandResponse> {
|
|
27
30
|
if (!args) {
|
|
@@ -211,59 +214,56 @@ export function createCommandHandlers(
|
|
|
211
214
|
const action = args.trim().toLowerCase() || "status";
|
|
212
215
|
|
|
213
216
|
if (action === "setup" || action === "register") {
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
return {
|
|
218
|
+
text: [
|
|
219
|
+
"✅ *定时任务注册*",
|
|
220
|
+
"",
|
|
221
|
+
"定时任务已在插件启动时自动注册。",
|
|
222
|
+
"",
|
|
223
|
+
"如需查看状态,请使用:",
|
|
224
|
+
"```bash",
|
|
225
|
+
"/skills-scanner cron status",
|
|
226
|
+
"```",
|
|
227
|
+
"",
|
|
228
|
+
"或使用 CLI 命令:",
|
|
229
|
+
"```bash",
|
|
230
|
+
"openclaw cron list | grep skills-scanner",
|
|
231
|
+
"```",
|
|
232
|
+
].join("\n"),
|
|
233
|
+
};
|
|
234
|
+
} else if (action === "unregister" || action === "remove") {
|
|
235
|
+
return {
|
|
236
|
+
text: [
|
|
237
|
+
"✅ *删除定时任务*",
|
|
238
|
+
"",
|
|
239
|
+
"定时任务会在插件卸载时自动清理。",
|
|
240
|
+
"",
|
|
241
|
+
"如需手动删除,请使用 CLI 命令:",
|
|
242
|
+
"",
|
|
243
|
+
"```bash",
|
|
244
|
+
"# 1. 查找任务 ID",
|
|
245
|
+
"openclaw cron list | grep skills-scanner",
|
|
246
|
+
"",
|
|
247
|
+
"# 2. 删除任务",
|
|
248
|
+
"openclaw cron remove skills-scanner:weekly-report",
|
|
249
|
+
"```",
|
|
250
|
+
].join("\n"),
|
|
251
|
+
};
|
|
252
|
+
} else if (action === "trigger" || action === "run") {
|
|
253
|
+
return {
|
|
254
|
+
text: await triggerCronJob({
|
|
216
255
|
logger,
|
|
217
|
-
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
} catch (err: any) {
|
|
221
|
-
return { text: `⚠️ 定时任务注册失败:${err.message}` };
|
|
222
|
-
}
|
|
223
|
-
} else if (action === "unregister") {
|
|
224
|
-
try {
|
|
225
|
-
// 列出所有任务
|
|
226
|
-
const listResult = await callGateway("cron.list", {});
|
|
227
|
-
const jobs = Array.isArray(listResult?.jobs) ? listResult.jobs : [];
|
|
228
|
-
const existingJobs = jobs.filter((job: any) => job.name === "skills-weekly-report");
|
|
229
|
-
|
|
230
|
-
if (existingJobs.length === 0) {
|
|
231
|
-
return { text: "ℹ️ 未找到已注册的定时任务" };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 删除所有同名任务
|
|
235
|
-
const deletedIds: string[] = [];
|
|
236
|
-
for (const job of existingJobs) {
|
|
237
|
-
const jobId = job.jobId || job.id;
|
|
238
|
-
try {
|
|
239
|
-
await callGateway("cron.remove", { jobId });
|
|
240
|
-
deletedIds.push(jobId);
|
|
241
|
-
} catch (err: any) {
|
|
242
|
-
logger.warn(`删除任务 ${jobId} 失败: ${err.message}`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (deletedIds.length > 0) {
|
|
247
|
-
return {
|
|
248
|
-
text: `✅ 已删除 ${deletedIds.length} 个定时任务\n${deletedIds.map(id => `- ${id}`).join("\n")}`,
|
|
249
|
-
};
|
|
250
|
-
} else {
|
|
251
|
-
return { text: "⚠️ 删除失败,请查看日志" };
|
|
252
|
-
}
|
|
253
|
-
} catch (err: any) {
|
|
254
|
-
return { text: `⚠️ 删除失败:${err.message}` };
|
|
255
|
-
}
|
|
256
|
+
config: apiConfig,
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
256
259
|
} else {
|
|
257
260
|
// status
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
return {
|
|
262
|
+
text: await getCronJobStatus({
|
|
260
263
|
logger,
|
|
261
|
-
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
} catch (err: any) {
|
|
265
|
-
return { text: `⚠️ 查询状态失败:${err.message}` };
|
|
266
|
-
}
|
|
264
|
+
config: apiConfig,
|
|
265
|
+
}),
|
|
266
|
+
};
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
package/src/cron-manager.ts
CHANGED
|
@@ -1,278 +1,237 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cron job management
|
|
2
|
+
* Cron job management for skills-scanner
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* It ensures only one cron job exists and cleans up duplicates.
|
|
4
|
+
* 使用 Plugin SDK 提供的 cron store API 来管理定时任务
|
|
6
5
|
*/
|
|
7
6
|
|
|
7
|
+
import {
|
|
8
|
+
loadCronStore,
|
|
9
|
+
resolveCronStorePath,
|
|
10
|
+
saveCronStore,
|
|
11
|
+
} from "openclaw/plugin-sdk/config-runtime";
|
|
8
12
|
import type { PluginLogger } from "./types.js";
|
|
9
13
|
|
|
14
|
+
// Cron job configuration
|
|
10
15
|
const CRON_JOB_NAME = "skills-weekly-report";
|
|
11
|
-
const CRON_SCHEDULE = "5 12 * * 1";
|
|
16
|
+
const CRON_SCHEDULE = "5 12 * * 1"; // 每周一 12:05
|
|
12
17
|
const CRON_TIMEZONE = "Asia/Shanghai";
|
|
13
18
|
|
|
14
19
|
export interface CronManagerOptions {
|
|
15
20
|
logger: PluginLogger;
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface CronJob {
|
|
20
|
-
id: string;
|
|
21
|
-
jobId?: string;
|
|
22
|
-
name: string;
|
|
23
|
-
enabled?: boolean;
|
|
21
|
+
config?: any;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
|
-
*
|
|
28
|
-
* - 检查是否已存在同名任务
|
|
29
|
-
* - 如果有多个重复任务,仅保留第一个,删除其余
|
|
30
|
-
* - 如果不存在,创建新任务
|
|
25
|
+
* 确保 cron 任务已注册(启动时调用)
|
|
31
26
|
*/
|
|
32
|
-
export async function
|
|
27
|
+
export async function ensureCronJob(
|
|
33
28
|
options: CronManagerOptions
|
|
34
29
|
): Promise<void> {
|
|
35
|
-
const { logger,
|
|
30
|
+
const { logger, config } = options;
|
|
36
31
|
|
|
37
32
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (existingJobs.length === 0) {
|
|
65
|
-
// 不存在,创建新任务
|
|
66
|
-
logger.info("[定时任务] 📝 未找到现有任务,正在创建...");
|
|
67
|
-
await createCronJob(options);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 3. 如果存在多个重复任务,仅保留第一个
|
|
72
|
-
if (existingJobs.length > 1) {
|
|
73
|
-
logger.warn("[定时任务] ⚠️ 检测到重复任务,正在清理...", {
|
|
74
|
-
duplicateCount: existingJobs.length,
|
|
75
|
-
jobs: existingJobs.map(j => ({
|
|
76
|
-
jobId: j.jobId,
|
|
77
|
-
id: j.id,
|
|
78
|
-
name: j.name,
|
|
79
|
-
enabled: j.enabled,
|
|
80
|
-
})),
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const keepJob = existingJobs[0];
|
|
84
|
-
const duplicates = existingJobs.slice(1);
|
|
85
|
-
|
|
86
|
-
logger.info("[定时任务] 📌 保留第一个任务", {
|
|
87
|
-
jobId: keepJob.jobId,
|
|
88
|
-
id: keepJob.id,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// 删除重复任务
|
|
92
|
-
let successCount = 0;
|
|
93
|
-
let failCount = 0;
|
|
94
|
-
|
|
95
|
-
for (const job of duplicates) {
|
|
96
|
-
const jobId = job.jobId || job.id;
|
|
97
|
-
logger.debug("[定时任务] 尝试删除重复任务", {
|
|
98
|
-
jobId,
|
|
99
|
-
hasJobId: !!job.jobId,
|
|
100
|
-
hasId: !!job.id,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const removeResult = await callGateway("cron.remove", { jobId });
|
|
105
|
-
logger.info("[定时任务] 🗑️ 已删除重复任务", {
|
|
106
|
-
jobId,
|
|
107
|
-
removeResult,
|
|
108
|
-
});
|
|
109
|
-
successCount++;
|
|
110
|
-
} catch (err: any) {
|
|
111
|
-
logger.error("[定时任务] ❌ 删除任务失败", {
|
|
112
|
-
jobId,
|
|
113
|
-
errorMessage: err?.message || String(err),
|
|
114
|
-
errorCode: err?.code,
|
|
115
|
-
errorStack: err?.stack,
|
|
116
|
-
fullError: err,
|
|
117
|
-
});
|
|
118
|
-
failCount++;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
logger.info("[定时任务] 清理完成", {
|
|
123
|
-
total: duplicates.length,
|
|
124
|
-
success: successCount,
|
|
125
|
-
failed: failCount,
|
|
126
|
-
});
|
|
127
|
-
} else {
|
|
128
|
-
// 只有一个任务,检查状态
|
|
129
|
-
const job = existingJobs[0];
|
|
130
|
-
const jobId = job.jobId || job.id;
|
|
131
|
-
logger.info("[定时任务] ✅ 检测到已存在的定时任务", {
|
|
132
|
-
jobId,
|
|
133
|
-
enabled: job.enabled !== false,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// 如果任务被禁用,提示用户
|
|
137
|
-
if (job.enabled === false) {
|
|
138
|
-
logger.warn("[定时任务] ⚠️ 任务已禁用", {
|
|
139
|
-
jobId,
|
|
140
|
-
hint: `openclaw cron edit ${jobId} --enabled`,
|
|
141
|
-
});
|
|
33
|
+
const storePath = resolveCronStorePath(config?.cron?.store);
|
|
34
|
+
const store = await loadCronStore(storePath);
|
|
35
|
+
|
|
36
|
+
// 只通过名称检查是否存在任务(ID 是系统自动生成的 UUID)
|
|
37
|
+
const existingJob = store.jobs.find((j) => j.name === CRON_JOB_NAME);
|
|
38
|
+
|
|
39
|
+
if (existingJob) {
|
|
40
|
+
// 任务已存在,检查是否需要更新配置
|
|
41
|
+
const needsUpdate =
|
|
42
|
+
existingJob.schedule.kind !== "cron" ||
|
|
43
|
+
(existingJob.schedule as any).expr !== CRON_SCHEDULE ||
|
|
44
|
+
(existingJob.schedule as any).tz !== CRON_TIMEZONE;
|
|
45
|
+
|
|
46
|
+
if (needsUpdate) {
|
|
47
|
+
logger.info("[Cron] 检测到任务配置变更,正在更新...");
|
|
48
|
+
existingJob.schedule = {
|
|
49
|
+
kind: "cron",
|
|
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}`);
|
|
142
58
|
}
|
|
59
|
+
return;
|
|
143
60
|
}
|
|
144
61
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
logger.error("[定时任务] ❌ 检查任务失败", {
|
|
148
|
-
errorMessage: err?.message || String(err),
|
|
149
|
-
errorName: err?.name,
|
|
150
|
-
errorCode: err?.code,
|
|
151
|
-
errorStack: err?.stack,
|
|
152
|
-
errorType: typeof err,
|
|
153
|
-
fullError: err,
|
|
154
|
-
});
|
|
155
|
-
logger.info("[定时任务] 💡 请手动注册定时任务: /skills-scanner cron setup");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* 创建新的定时任务
|
|
161
|
-
*/
|
|
162
|
-
async function createCronJob(options: CronManagerOptions): Promise<void> {
|
|
163
|
-
const { logger, callGateway } = options;
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
const jobParams = {
|
|
62
|
+
// 创建新任务(不设置 id,让系统自动生成)
|
|
63
|
+
const newJob = {
|
|
167
64
|
name: CRON_JOB_NAME,
|
|
65
|
+
enabled: true,
|
|
168
66
|
schedule: {
|
|
169
|
-
kind: "cron",
|
|
67
|
+
kind: "cron" as const,
|
|
170
68
|
expr: CRON_SCHEDULE,
|
|
171
69
|
tz: CRON_TIMEZONE,
|
|
172
70
|
},
|
|
173
|
-
sessionTarget: "isolated",
|
|
71
|
+
sessionTarget: "isolated" as const,
|
|
174
72
|
payload: {
|
|
175
|
-
kind: "agentTurn",
|
|
176
|
-
message: "
|
|
73
|
+
kind: "agentTurn" as const,
|
|
74
|
+
message: "生成 Skills 安全周报。请扫描所有已配置的 Skills 目录,汇总本周的安全发现,并生成详细报告。",
|
|
75
|
+
timeoutSeconds: 600, // 10 分钟超时
|
|
177
76
|
},
|
|
178
77
|
delivery: {
|
|
179
|
-
mode: "announce",
|
|
180
|
-
channel: "last",
|
|
78
|
+
mode: "announce" as const,
|
|
79
|
+
channel: "last" as const,
|
|
181
80
|
},
|
|
182
|
-
|
|
81
|
+
failureAlert: {
|
|
82
|
+
after: 2,
|
|
83
|
+
channel: "last" as const,
|
|
84
|
+
mode: "announce" as const,
|
|
85
|
+
},
|
|
86
|
+
createdAtMs: Date.now(),
|
|
87
|
+
state: {},
|
|
183
88
|
};
|
|
184
89
|
|
|
185
|
-
|
|
186
|
-
|
|
90
|
+
store.jobs.push(newJob as any);
|
|
91
|
+
await saveCronStore(storePath, store);
|
|
187
92
|
|
|
188
|
-
|
|
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;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
189
100
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
101
|
+
/**
|
|
102
|
+
* 清理 cron 任务(卸载时调用)
|
|
103
|
+
*/
|
|
104
|
+
export async function cleanupCronJob(
|
|
105
|
+
options: CronManagerOptions
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const { logger, config } = options;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const storePath = resolveCronStorePath(config?.cron?.store);
|
|
111
|
+
const store = await loadCronStore(storePath);
|
|
112
|
+
|
|
113
|
+
const before = store.jobs.length;
|
|
197
114
|
|
|
198
|
-
|
|
115
|
+
// 只删除名称为 skills-weekly-report 的任务
|
|
116
|
+
store.jobs = store.jobs.filter((j) => j.name !== CRON_JOB_NAME);
|
|
199
117
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
118
|
+
const removed = before - store.jobs.length;
|
|
119
|
+
|
|
120
|
+
if (removed > 0) {
|
|
121
|
+
await saveCronStore(storePath, store);
|
|
122
|
+
logger.info(`[Cron] ✅ 已清理 ${removed} 个定时任务`);
|
|
206
123
|
} else {
|
|
207
|
-
logger.
|
|
208
|
-
result,
|
|
209
|
-
});
|
|
124
|
+
logger.info("[Cron] 没有需要清理的定时任务");
|
|
210
125
|
}
|
|
211
126
|
} catch (err: any) {
|
|
212
|
-
logger.error(
|
|
213
|
-
errorMessage: err?.message || String(err),
|
|
214
|
-
errorName: err?.name,
|
|
215
|
-
errorCode: err?.code,
|
|
216
|
-
errorStack: err?.stack,
|
|
217
|
-
fullError: err,
|
|
218
|
-
});
|
|
127
|
+
logger.error(`[Cron] ❌ 清理定时任务失败: ${err.message}`);
|
|
219
128
|
throw err;
|
|
220
129
|
}
|
|
221
130
|
}
|
|
222
131
|
|
|
223
132
|
/**
|
|
224
|
-
*
|
|
133
|
+
* 获取 cron 任务状态
|
|
225
134
|
*/
|
|
226
|
-
export async function
|
|
135
|
+
export async function getCronJobStatus(
|
|
227
136
|
options: CronManagerOptions
|
|
228
137
|
): Promise<string> {
|
|
229
|
-
const { logger,
|
|
138
|
+
const { logger, config } = options;
|
|
230
139
|
|
|
231
140
|
try {
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
const jobs: CronJob[] = Array.isArray(listResult?.jobs) ? listResult.jobs : [];
|
|
235
|
-
|
|
236
|
-
logger.debug("[定时任务] 状态查询结果", {
|
|
237
|
-
totalJobs: jobs.length,
|
|
238
|
-
targetName: CRON_JOB_NAME,
|
|
239
|
-
});
|
|
141
|
+
const storePath = resolveCronStorePath(config?.cron?.store);
|
|
142
|
+
const store = await loadCronStore(storePath);
|
|
240
143
|
|
|
241
|
-
const
|
|
144
|
+
const job = store.jobs.find((j) => j.name === CRON_JOB_NAME);
|
|
242
145
|
|
|
243
|
-
if (
|
|
146
|
+
if (!job) {
|
|
244
147
|
return [
|
|
245
|
-
"
|
|
246
|
-
"
|
|
148
|
+
"❌ *定时任务状态*",
|
|
149
|
+
"",
|
|
150
|
+
"任务未注册。",
|
|
247
151
|
"",
|
|
248
|
-
"
|
|
152
|
+
"请重启插件或手动注册:",
|
|
153
|
+
"```bash",
|
|
154
|
+
"/skills-scanner cron setup",
|
|
155
|
+
"```",
|
|
249
156
|
].join("\n");
|
|
250
157
|
}
|
|
251
158
|
|
|
252
|
-
const
|
|
159
|
+
const nextRun = job.state?.nextRunAtMs
|
|
160
|
+
? new Date(job.state.nextRunAtMs).toLocaleString("zh-CN", {
|
|
161
|
+
timeZone: CRON_TIMEZONE,
|
|
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
|
+
}
|
|
253
196
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
197
|
+
/**
|
|
198
|
+
* 手动触发任务(用于测试)
|
|
199
|
+
*/
|
|
200
|
+
export async function triggerCronJob(
|
|
201
|
+
options: CronManagerOptions
|
|
202
|
+
): Promise<string> {
|
|
203
|
+
const { logger, config } = options;
|
|
259
204
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
205
|
+
try {
|
|
206
|
+
const storePath = resolveCronStorePath(config?.cron?.store);
|
|
207
|
+
const store = await loadCronStore(storePath);
|
|
208
|
+
|
|
209
|
+
const job = store.jobs.find((j) => j.name === CRON_JOB_NAME);
|
|
210
|
+
|
|
211
|
+
if (!job) {
|
|
212
|
+
return [
|
|
213
|
+
"❌ *手动触发任务*",
|
|
214
|
+
"",
|
|
215
|
+
"任务未找到,请先注册任务。",
|
|
216
|
+
].join("\n");
|
|
266
217
|
}
|
|
267
218
|
|
|
268
|
-
return
|
|
219
|
+
return [
|
|
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");
|
|
269
232
|
} catch (err: any) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
errorMessage: errorMsg,
|
|
273
|
-
errorCode: err?.code,
|
|
274
|
-
errorStack: err?.stack,
|
|
275
|
-
});
|
|
276
|
-
return `❌ 查询失败: ${errorMsg}`;
|
|
233
|
+
logger.error(`[Cron] ❌ 获取任务信息失败: ${err.message}`);
|
|
234
|
+
return `❌ 获取任务信息失败: ${err.message}`;
|
|
277
235
|
}
|
|
278
236
|
}
|
|
237
|
+
|