Skip to content

Commit 37c05cb

Browse files
committed
添加 soloncode /loop cron 表达式支持
1 parent 88c764f commit 37c05cb

3 files changed

Lines changed: 82 additions & 18 deletions

File tree

soloncode-cli/src/main/java/org/noear/solon/codecli/command/builtin/LoopCommand.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* /loop 5m check if deployment finished → fixed interval (5m)
3232
* /loop 30s check ci status → fixed interval (30s)
3333
* /loop check ci status → auto interval (5m default)
34+
* /loop cron:'0 */5 * * * ?' check status → cron expression
3435
* /loop ls → list active tasks
3536
* /loop stop <id> → stop a task
3637
* /loop stop-all → stop all tasks
@@ -62,7 +63,7 @@ public String name() {
6263

6364
@Override
6465
public String description() {
65-
return "循环任务管理 (ls, stop, stop-all, <interval> <prompt>)";
66+
return "循环任务管理 (ls, stop, stop-all, <interval> <prompt>, cron:<expr> <prompt>)";
6667
}
6768

6869
@Override
@@ -98,14 +99,31 @@ public boolean execute(CommandContext ctx) throws Exception {
9899
scheduler.stopAll(sessionId, workspace, harnessSessions);
99100
ctx.println(ctx.color(GREEN + "All loop tasks stopped." + RESET));
100101
} else {
101-
// Schedule a new task: /loop [interval] <prompt>
102+
// Schedule a new task: /loop [interval|cron:<expr>] <prompt>
102103
int intervalMinutes = 5; // default
103104
int promptStartIndex = 0;
104-
105-
Integer parsed = parseInterval(sub);
106-
if (parsed != null) {
107-
intervalMinutes = parsed;
105+
String cronExpr = null;
106+
107+
// 检查是否为 cron 模式
108+
if (sub.startsWith("cron:")) {
109+
cronExpr = sub.substring(5).trim();
110+
// 去除可能被引号包裹的 cron 表达式
111+
if ((cronExpr.startsWith("\"") && cronExpr.endsWith("\"")) ||
112+
(cronExpr.startsWith("'") && cronExpr.endsWith("'"))) {
113+
cronExpr = cronExpr.substring(1, cronExpr.length() - 1);
114+
}
115+
if (cronExpr.isEmpty()) {
116+
ctx.println(ctx.color(RED + "Usage: /loop cron:<expr> <prompt>" + RESET));
117+
ctx.println(ctx.color(DIM + " /loop cron:\"0 */5 * * * ?\" check status" + RESET));
118+
return true;
119+
}
108120
promptStartIndex = 1;
121+
} else {
122+
Integer parsed = parseInterval(sub);
123+
if (parsed != null) {
124+
intervalMinutes = parsed;
125+
promptStartIndex = 1;
126+
}
109127
}
110128

111129
// Build prompt from remaining args
@@ -119,20 +137,27 @@ public boolean execute(CommandContext ctx) throws Exception {
119137

120138
String prompt = promptBuilder.toString().trim();
121139
if (prompt.isEmpty()) {
122-
ctx.println(ctx.color(RED + "Usage: /loop [interval] <prompt>" + RESET));
140+
ctx.println(ctx.color(RED + "Usage: /loop [interval|cron:<expr>] <prompt>" + RESET));
123141
ctx.println(ctx.color(DIM + " /loop 5m check deployment" + RESET));
124142
ctx.println(ctx.color(DIM + " /loop 30s check CI status" + RESET));
125143
ctx.println(ctx.color(DIM + " /loop check CI status (auto 5m)" + RESET));
144+
ctx.println(ctx.color(DIM + " /loop cron:\"0 */5 * * * ?\" check status" + RESET));
126145
return true;
127146
}
128147

129148
// Create and schedule
130-
LoopTask task = new LoopTask(prompt, intervalMinutes);
149+
LoopTask task = cronExpr != null
150+
? new LoopTask(prompt, cronExpr)
151+
: new LoopTask(prompt, intervalMinutes);
131152
scheduler.schedule(sessionId, workspace, harnessSessions, task);
132153

133154
ctx.println(ctx.color(GREEN + "Loop task registered:" + RESET));
134155
ctx.println(ctx.color(" " + BOLD + "ID:" + RESET + " " + task.getId()));
135-
ctx.println(ctx.color(" " + BOLD + "Interval:" + RESET + " " + formatInterval(intervalMinutes)));
156+
if (task.isCronMode()) {
157+
ctx.println(ctx.color(" " + BOLD + "Cron:" + RESET + " " + task.getCron()));
158+
} else {
159+
ctx.println(ctx.color(" " + BOLD + "Interval:" + RESET + " " + formatInterval(intervalMinutes)));
160+
}
136161
ctx.println(ctx.color(" " + BOLD + "Prompt:" + RESET + " " + prompt));
137162
ctx.println(ctx.color(DIM + " Expires: " + task.getExpireAt() + RESET));
138163
}
@@ -150,10 +175,13 @@ private void doList(CommandContext ctx, String sessionId, String workspace, Stri
150175
ctx.println(ctx.color(BOLD + "Active Loop Tasks:" + RESET));
151176
for (LoopTask t : tasks) {
152177
String status = t.isRunning() ? YELLOW + "running" + RESET : GREEN + "idle" + RESET;
178+
String scheduleInfo = t.isCronMode()
179+
? CYAN + "cron:" + t.getCron() + RESET
180+
: formatInterval(t.getIntervalMinutes());
153181
String lastInfo = t.getLastExecutedAt() != null
154182
? DIM + " (last: " + formatAgo(t.getLastExecutedAt()) + ": " + (t.getLastResult() != null ? t.getLastResult() : "-") + ")" + RESET
155183
: "";
156-
ctx.println(ctx.color(" " + CYAN + t.getId() + RESET + " " + formatInterval(t.getIntervalMinutes()) + " " + status + " " + DIM + t.getPrompt() + RESET + lastInfo));
184+
ctx.println(ctx.color(" " + CYAN + t.getId() + RESET + " " + scheduleInfo + " " + status + " " + DIM + t.getPrompt() + RESET + lastInfo));
157185
}
158186
ctx.println(ctx.color(DIM + "\nUsage: /loop stop <id> | /loop stop-all" + RESET));
159187
}

soloncode-cli/src/main/java/org/noear/solon/codecli/command/builtin/LoopScheduler.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,20 @@ public void restore(String sessionId, String workspace, String harnessSessions)
232232
// ==================== IJobManager 注册 ====================
233233

234234
/**
235-
* 注册任务到 IJobManager(使用 fixedDelay 串行策略)
235+
* 注册任务到 IJobManager(cron 模式使用 cron 表达式,否则使用 fixedDelay 串行策略)
236236
*/
237237
private void registerJob(String sessionId, LoopTask task) {
238238
String jobName = task.getJobName();
239-
long intervalMs = (long) task.getIntervalMinutes() * 60_000L;
240239

241-
ScheduledAnno scheduled = new ScheduledAnno()
242-
.fixedDelay(intervalMs)
243-
.initialDelay(intervalMs);
240+
ScheduledAnno scheduled;
241+
if (task.isCronMode()) {
242+
scheduled = new ScheduledAnno().cron(task.getCron());
243+
} else {
244+
long intervalMs = (long) task.getIntervalMinutes() * 60_000L;
245+
scheduled = new ScheduledAnno()
246+
.fixedDelay(intervalMs)
247+
.initialDelay(intervalMs);
248+
}
244249

245250
jobManager.jobAdd(jobName, scheduled, ctx -> {
246251
onTrigger(sessionId, task);

soloncode-cli/src/main/java/org/noear/solon/codecli/command/builtin/LoopTask.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class LoopTask {
3838
private final String id;
3939
private final String prompt;
4040
private final int intervalMinutes;
41+
private final String cron;
4142
private final Instant createdAt;
4243
private final Instant expireAt;
4344
private final boolean autoInterval;
@@ -54,6 +55,20 @@ public LoopTask(String prompt, int intervalMinutes) {
5455
this.id = UUID.randomUUID().toString().substring(0, 8);
5556
this.prompt = prompt;
5657
this.intervalMinutes = Math.max(MIN_INTERVAL, Math.min(MAX_INTERVAL, intervalMinutes));
58+
this.cron = null;
59+
this.createdAt = Instant.now();
60+
this.expireAt = createdAt.plus(EXPIRE_DAYS, ChronoUnit.DAYS);
61+
this.autoInterval = false;
62+
}
63+
64+
/**
65+
* cron 表达式构造
66+
*/
67+
public LoopTask(String prompt, String cron) {
68+
this.id = UUID.randomUUID().toString().substring(0, 8);
69+
this.prompt = prompt;
70+
this.intervalMinutes = 0;
71+
this.cron = cron;
5772
this.createdAt = Instant.now();
5873
this.expireAt = createdAt.plus(EXPIRE_DAYS, ChronoUnit.DAYS);
5974
this.autoInterval = false;
@@ -66,6 +81,7 @@ public LoopTask(String prompt, boolean autoInterval) {
6681
this.id = UUID.randomUUID().toString().substring(0, 8);
6782
this.prompt = prompt;
6883
this.intervalMinutes = DEFAULT_AUTO_INTERVAL;
84+
this.cron = null;
6985
this.createdAt = Instant.now();
7086
this.expireAt = createdAt.plus(EXPIRE_DAYS, ChronoUnit.DAYS);
7187
this.autoInterval = autoInterval;
@@ -74,12 +90,13 @@ public LoopTask(String prompt, boolean autoInterval) {
7490
/**
7591
* 内部构造,用于反序列化
7692
*/
77-
private LoopTask(String id, String prompt, int intervalMinutes, Instant createdAt,
78-
Instant expireAt, boolean autoInterval, boolean cancelled,
79-
String lastResult, Instant lastExecutedAt) {
93+
private LoopTask(String id, String prompt, int intervalMinutes, String cron,
94+
Instant createdAt, Instant expireAt, boolean autoInterval,
95+
boolean cancelled, String lastResult, Instant lastExecutedAt) {
8096
this.id = id;
8197
this.prompt = prompt;
8298
this.intervalMinutes = intervalMinutes;
99+
this.cron = cron;
83100
this.createdAt = createdAt;
84101
this.expireAt = expireAt;
85102
this.autoInterval = autoInterval;
@@ -95,6 +112,13 @@ public boolean isExpired() {
95112
return Instant.now().isAfter(expireAt);
96113
}
97114

115+
/**
116+
* 是否为 cron 模式
117+
*/
118+
public boolean isCronMode() {
119+
return cron != null && !cron.isEmpty();
120+
}
121+
98122
/**
99123
* 是否仍处于活跃状态(未取消且未过期)
100124
*/
@@ -152,6 +176,9 @@ public ONode toONode() {
152176
node.set("id", id);
153177
node.set("prompt", prompt);
154178
node.set("intervalMinutes", intervalMinutes);
179+
if (cron != null) {
180+
node.set("cron", cron);
181+
}
155182
node.set("createdAt", createdAt.toString());
156183
node.set("expireAt", expireAt.toString());
157184
node.set("autoInterval", autoInterval);
@@ -178,11 +205,15 @@ public static LoopTask fromONode(ONode node) {
178205
Instant lastExecutedAtVal = node.getOrNull("lastExecutedAt") != null
179206
? Instant.parse(node.get("lastExecutedAt").getString())
180207
: null;
208+
String cronVal = node.getOrNull("cron") != null
209+
? node.get("cron").getString()
210+
: null;
181211

182212
return new LoopTask(
183213
node.get("id").getString(),
184214
node.get("prompt").getString(),
185215
node.get("intervalMinutes").getInt(),
216+
cronVal,
186217
Instant.parse(node.get("createdAt").getString()),
187218
Instant.parse(node.get("expireAt").getString()),
188219
node.get("autoInterval").getBoolean(),

0 commit comments

Comments
 (0)