@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 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 { ensureCronJobViaGateway, checkCronJobStatus } from "./src/cron-manager.js";
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 gateway_start hook for automatic cron job registration
147
- api.on("gateway_start", async () => {
148
- try {
149
- api.logger.info("[skills-scanner] 🚀 Gateway started, checking cron job...");
150
- await ensureCronJobViaGateway({
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
- const listResult = await api.callGateway("cron.list", {});
182
- const jobs = listResult?.jobs || [];
183
- const ourJobs = jobs.filter((j: any) => j.name === "skills-weekly-report");
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.warn("[skills-scanner] Failed to save final state", {
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
- api.logger.info("[skills-scanner] ✅ Cleanup completed successfully");
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] ❌ Cleanup failed", {
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
- async (method: string, params: any) => {
474
- return await api.callGateway(method, params);
475
- }
448
+ api.config // 传递 api.config
476
449
  );
477
450
 
478
451
  // Chat command: /skills-scanner
@@ -2,7 +2,7 @@
2
2
  "id": "skills-scanner",
3
3
  "name": "Skills Scanner",
4
4
  "description": "Security scanner for OpenClaw Skills to detect potential threats",
5
- "version": "1.0.0-beta.3",
5
+ "version": "1.0.0-beta.4",
6
6
  "author": "pwddd",
7
7
  "skills": ["./skills"],
8
8
  "uiHints": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pwddd/skills-scanner",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.4",
4
4
  "description": "OpenClaw Skills security scanner plugin - detect malicious code, data exfiltration, and prompt injection",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
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 { ensureCronJobViaGateway, checkCronJobStatus } from "./cron-manager.js";
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
- callGateway: (method: string, params: any) => Promise<any>
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
- try {
215
- await ensureCronJobViaGateway({
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
- callGateway,
218
- });
219
- return { text: "✅ 定时任务注册完成\n请查看日志了解详情" };
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
- try {
259
- const statusText = await checkCronJobStatus({
261
+ return {
262
+ text: await getCronJobStatus({
260
263
  logger,
261
- callGateway,
262
- });
263
- return { text: statusText };
264
- } catch (err: any) {
265
- return { text: `⚠️ 查询状态失败:${err.message}` };
266
- }
264
+ config: apiConfig,
265
+ }),
266
+ };
267
267
  }
268
268
  }
269
269
 
@@ -1,278 +1,237 @@
1
1
  /**
2
- * Cron job management using Gateway API
2
+ * Cron job management for skills-scanner
3
3
  *
4
- * This module handles automatic cron job registration via gateway_start hook.
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"; // 每周一 12:05
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
- callGateway: (method: string, params: any) => Promise<any>;
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
- * 注册定时任务(通过 Gateway API)
28
- * - 检查是否已存在同名任务
29
- * - 如果有多个重复任务,仅保留第一个,删除其余
30
- * - 如果不存在,创建新任务
25
+ * 确保 cron 任务已注册(启动时调用)
31
26
  */
32
- export async function ensureCronJobViaGateway(
27
+ export async function ensureCronJob(
33
28
  options: CronManagerOptions
34
29
  ): Promise<void> {
35
- const { logger, callGateway } = options;
30
+ const { logger, config } = options;
36
31
 
37
32
  try {
38
- logger.info("[定时任务] 🔍 检查现有定时任务...");
39
-
40
- // 1. 列出所有任务
41
- logger.debug("[定时任务] 调用 cron.list API...");
42
- const listResult = await callGateway("cron.list", {});
43
-
44
- logger.debug("[定时任务] API 响应", {
45
- hasResult: !!listResult,
46
- resultType: typeof listResult,
47
- hasJobs: !!listResult?.jobs,
48
- jobsType: typeof listResult?.jobs,
49
- rawResult: listResult,
50
- });
51
-
52
- const jobs: CronJob[] = Array.isArray(listResult?.jobs) ? listResult.jobs : [];
53
-
54
- logger.debug("[定时任务] 查询结果", {
55
- totalJobs: jobs.length,
56
- targetName: CRON_JOB_NAME,
57
- });
58
-
59
- // 2. 查找所有同名任务
60
- const existingJobs = jobs.filter(
61
- (job) => job.name === CRON_JOB_NAME
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
- logger.info("[定时任务] ⏭️ 跳过注册");
146
- } catch (err: any) {
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: "/skills-scanner scan --report",
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
- enabled: true,
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
- logger.debug("[定时任务] 创建参数", { jobParams });
186
- logger.debug("[定时任务] 调用 cron.add API...");
90
+ store.jobs.push(newJob as any);
91
+ await saveCronStore(storePath, store);
187
92
 
188
- const result = await callGateway("cron.add", jobParams);
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
- logger.debug("[定时任务] API 响应", {
191
- hasResult: !!result,
192
- resultType: typeof result,
193
- hasJobId: !!result?.jobId,
194
- hasId: !!result?.id,
195
- rawResult: result,
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
- const jobId = result?.jobId || result?.id;
115
+ // 只删除名称为 skills-weekly-report 的任务
116
+ store.jobs = store.jobs.filter((j) => j.name !== CRON_JOB_NAME);
199
117
 
200
- if (jobId) {
201
- logger.info("[定时任务] ✅ 定时任务创建成功", {
202
- jobId,
203
- schedule: `${CRON_SCHEDULE} (${CRON_TIMEZONE})`,
204
- description: "每周一 12:05",
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.warn("[定时任务] ⚠️ 任务创建成功,但未返回任务 ID", {
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 checkCronJobStatus(
135
+ export async function getCronJobStatus(
227
136
  options: CronManagerOptions
228
137
  ): Promise<string> {
229
- const { logger, callGateway } = options;
138
+ const { logger, config } = options;
230
139
 
231
140
  try {
232
- logger.debug("[定时任务] 查询状态...");
233
- const listResult = await callGateway("cron.list", {});
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 existingJobs = jobs.filter((job) => job.name === CRON_JOB_NAME);
144
+ const job = store.jobs.find((j) => j.name === CRON_JOB_NAME);
242
145
 
243
- if (existingJobs.length === 0) {
146
+ if (!job) {
244
147
  return [
245
- " *定时任务状态*",
246
- "状态:❌ 未注册",
148
+ " *定时任务状态*",
149
+ "",
150
+ "任务未注册。",
247
151
  "",
248
- "ℹ️ 使用 `/skills-scanner cron setup` 注册",
152
+ "请重启插件或手动注册:",
153
+ "```bash",
154
+ "/skills-scanner cron setup",
155
+ "```",
249
156
  ].join("\n");
250
157
  }
251
158
 
252
- const lines = ["✅ *定时任务状态*"];
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
- if (existingJobs.length > 1) {
255
- lines.push(`⚠️ 检测到 ${existingJobs.length} 个重复任务`);
256
- lines.push("建议重启 Gateway 以自动清理重复任务");
257
- lines.push("");
258
- }
197
+ /**
198
+ * 手动触发任务(用于测试)
199
+ */
200
+ export async function triggerCronJob(
201
+ options: CronManagerOptions
202
+ ): Promise<string> {
203
+ const { logger, config } = options;
259
204
 
260
- for (const job of existingJobs) {
261
- const jobId = job.jobId || job.id;
262
- const status = job.enabled === false ? "❌ 已禁用" : "✅ 已启用";
263
- lines.push(`任务 ID: ${jobId}`);
264
- lines.push(`状态: ${status}`);
265
- lines.push(`执行时间:每周一 12:05 (${CRON_TIMEZONE})`);
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 lines.join("\n");
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
- const errorMsg = err?.message || String(err);
271
- logger.error("[定时任务] 查询状态失败", {
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
+