u-foo 1.2.12 → 1.2.14
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/bin/ufoo.js +149 -0
- package/modules/online/SKILLS/ufoo-online/SKILL.md +2 -2
- package/package.json +1 -1
- package/scripts/postinstall.js +32 -14
- package/src/agent/notifier.js +5 -1
- package/src/agent/ufooAgent.js +1 -1
- package/src/bus/index.js +10 -3
- package/src/bus/inject.js +6 -3
- package/src/bus/subscriber.js +15 -2
- package/src/chat/commandExecutor.js +138 -10
- package/src/chat/commands.js +2 -1
- package/src/chat/daemonMessageRouter.js +40 -0
- package/src/chat/index.js +10 -29
- package/src/cli/onlineCoreCommands.js +11 -11
- package/src/cli.js +43 -4
- package/src/code/tui.js +53 -29
- package/src/daemon/cronOps.js +362 -29
- package/src/daemon/index.js +64 -0
- package/src/daemon/status.js +3 -0
- package/src/online/bridge.js +1 -1
- package/src/online/client.js +1 -1
- package/src/shared/eventContract.js +1 -0
package/src/daemon/cronOps.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
1
4
|
const {
|
|
2
|
-
createCronScheduler,
|
|
3
5
|
parseIntervalMs,
|
|
4
6
|
formatIntervalMs,
|
|
5
7
|
} = require("../chat/cronScheduler");
|
|
@@ -49,6 +51,68 @@ function resolveCronIntervalMs(op = {}) {
|
|
|
49
51
|
return parseIntervalMs(everyRaw);
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function parseCronAtMs(value = "") {
|
|
55
|
+
const text = String(value || "").trim();
|
|
56
|
+
if (!text) return 0;
|
|
57
|
+
|
|
58
|
+
if (/^\d+$/.test(text)) {
|
|
59
|
+
const parsed = Number.parseInt(text, 10);
|
|
60
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
|
|
61
|
+
return text.length <= 10 ? parsed * 1000 : parsed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalized = text.replace(/\//g, "-");
|
|
65
|
+
const direct = normalized.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})(?::(\d{2}))?$/);
|
|
66
|
+
if (direct) {
|
|
67
|
+
const seconds = direct[3] || "00";
|
|
68
|
+
const parsed = Date.parse(`${direct[1]}T${direct[2]}:${seconds}`);
|
|
69
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parsed = Date.parse(normalized);
|
|
73
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatCronAtMs(value = 0) {
|
|
77
|
+
const ts = Number(value) || 0;
|
|
78
|
+
if (ts <= 0) return "";
|
|
79
|
+
const d = new Date(ts);
|
|
80
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
81
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveCronOnceAtMs(op = {}) {
|
|
85
|
+
const numeric = Number(
|
|
86
|
+
op.once_at_ms
|
|
87
|
+
?? op.onceAtMs
|
|
88
|
+
?? op.at_ms
|
|
89
|
+
?? op.atMs
|
|
90
|
+
?? op.run_at_ms
|
|
91
|
+
?? op.runAtMs
|
|
92
|
+
);
|
|
93
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
94
|
+
return Math.floor(numeric);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const combined = (op.date && op.time)
|
|
98
|
+
? `${String(op.date).trim()} ${String(op.time).trim()}`
|
|
99
|
+
: "";
|
|
100
|
+
|
|
101
|
+
const raw = String(
|
|
102
|
+
op.at
|
|
103
|
+
|| op.once
|
|
104
|
+
|| op.run_at
|
|
105
|
+
|| op.runAt
|
|
106
|
+
|| op.datetime
|
|
107
|
+
|| op.date_time
|
|
108
|
+
|| combined
|
|
109
|
+
|| ""
|
|
110
|
+
).trim();
|
|
111
|
+
|
|
112
|
+
if (!raw) return 0;
|
|
113
|
+
return parseCronAtMs(raw);
|
|
114
|
+
}
|
|
115
|
+
|
|
52
116
|
function resolveCronPrompt(op = {}) {
|
|
53
117
|
return String(op.prompt || op.message || op.msg || "").trim();
|
|
54
118
|
}
|
|
@@ -57,61 +121,302 @@ function resolveCronTaskId(op = {}) {
|
|
|
57
121
|
return String(op.id || op.task_id || op.taskId || "").trim();
|
|
58
122
|
}
|
|
59
123
|
|
|
124
|
+
function sanitizeSummaryText(value = "") {
|
|
125
|
+
return String(value || "")
|
|
126
|
+
.replace(/[{}]/g, "")
|
|
127
|
+
.replace(/\s+/g, " ")
|
|
128
|
+
.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function summarizeCronTask(task = {}) {
|
|
132
|
+
const id = String(task.id || "");
|
|
133
|
+
const targets = Array.isArray(task.targets) ? task.targets.join("+") : "";
|
|
134
|
+
const promptRaw = sanitizeSummaryText(task.prompt || "");
|
|
135
|
+
const prompt = promptRaw.length > 24 ? `${promptRaw.slice(0, 24)}...` : promptRaw;
|
|
136
|
+
|
|
137
|
+
if (Number(task.onceAtMs) > 0) {
|
|
138
|
+
return `${id}@once(${formatCronAtMs(task.onceAtMs)})->${targets}: ${prompt || "(empty)"}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const interval = formatIntervalMs(task.intervalMs || 0);
|
|
142
|
+
return `${id}@${interval}->${targets}: ${prompt || "(empty)"}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
60
145
|
function formatCronTask(task = {}) {
|
|
146
|
+
const onceAtMs = Number(task.onceAtMs) || 0;
|
|
61
147
|
return {
|
|
62
148
|
id: String(task.id || ""),
|
|
149
|
+
mode: onceAtMs > 0 ? "once" : "interval",
|
|
63
150
|
intervalMs: Number(task.intervalMs) || 0,
|
|
64
|
-
interval:
|
|
151
|
+
interval: Number(task.intervalMs) > 0 ? formatIntervalMs(task.intervalMs) : "",
|
|
152
|
+
onceAtMs,
|
|
153
|
+
onceAt: onceAtMs > 0 ? formatCronAtMs(onceAtMs) : "",
|
|
65
154
|
targets: Array.isArray(task.targets) ? task.targets.slice() : [],
|
|
66
155
|
prompt: String(task.prompt || ""),
|
|
67
156
|
createdAt: Number(task.createdAt) || 0,
|
|
68
157
|
lastRunAt: Number(task.lastRunAt) || 0,
|
|
69
158
|
tickCount: Number(task.tickCount) || 0,
|
|
70
|
-
summary:
|
|
159
|
+
summary: summarizeCronTask(task),
|
|
71
160
|
};
|
|
72
161
|
}
|
|
73
162
|
|
|
74
163
|
function createDaemonCronController(options = {}) {
|
|
75
164
|
const {
|
|
165
|
+
projectRoot = "",
|
|
76
166
|
dispatch = async () => {},
|
|
77
167
|
log = () => {},
|
|
78
|
-
setIntervalFn,
|
|
79
|
-
clearIntervalFn,
|
|
80
|
-
|
|
168
|
+
setIntervalFn = setInterval,
|
|
169
|
+
clearIntervalFn = clearInterval,
|
|
170
|
+
setTimeoutFn = setTimeout,
|
|
171
|
+
clearTimeoutFn = clearTimeout,
|
|
172
|
+
nowFn = () => Date.now(),
|
|
173
|
+
fsModule = fs,
|
|
174
|
+
pathModule = path,
|
|
175
|
+
getUfooPathsImpl = getUfooPaths,
|
|
176
|
+
storageFile = "",
|
|
81
177
|
} = options;
|
|
82
178
|
|
|
83
|
-
|
|
84
|
-
|
|
179
|
+
let seq = 0;
|
|
180
|
+
const tasks = [];
|
|
181
|
+
|
|
182
|
+
const persistedFile = storageFile
|
|
183
|
+
|| (projectRoot ? pathModule.join(getUfooPathsImpl(projectRoot).runDir, "cron.tasks.json") : "");
|
|
184
|
+
|
|
185
|
+
function nextTaskId() {
|
|
186
|
+
seq += 1;
|
|
187
|
+
return `c${seq}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function persistState() {
|
|
191
|
+
if (!persistedFile) return;
|
|
192
|
+
const state = {
|
|
193
|
+
version: 1,
|
|
194
|
+
seq,
|
|
195
|
+
tasks: tasks.map((task) => ({
|
|
196
|
+
id: task.id,
|
|
197
|
+
intervalMs: task.intervalMs,
|
|
198
|
+
onceAtMs: task.onceAtMs,
|
|
199
|
+
targets: task.targets.slice(),
|
|
200
|
+
prompt: task.prompt,
|
|
201
|
+
createdAt: task.createdAt,
|
|
202
|
+
lastRunAt: task.lastRunAt,
|
|
203
|
+
tickCount: task.tickCount,
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
fsModule.mkdirSync(pathModule.dirname(persistedFile), { recursive: true });
|
|
209
|
+
const tmpFile = `${persistedFile}.tmp`;
|
|
210
|
+
fsModule.writeFileSync(tmpFile, JSON.stringify(state, null, 2), "utf8");
|
|
211
|
+
fsModule.renameSync(tmpFile, persistedFile);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const detail = err && err.message ? err.message : String(err || "persist failed");
|
|
214
|
+
log(`cron persist failed: ${detail}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function clearTaskTimer(task) {
|
|
219
|
+
if (!task || !task.timer) return;
|
|
220
|
+
if (task.onceAtMs > 0) {
|
|
221
|
+
clearTimeoutFn(task.timer);
|
|
222
|
+
} else {
|
|
223
|
+
clearIntervalFn(task.timer);
|
|
224
|
+
}
|
|
225
|
+
task.timer = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function runTask(task) {
|
|
229
|
+
task.lastRunAt = nowFn();
|
|
230
|
+
task.tickCount += 1;
|
|
231
|
+
|
|
232
|
+
for (const target of task.targets) {
|
|
85
233
|
try {
|
|
86
|
-
Promise.resolve(dispatch({
|
|
234
|
+
Promise.resolve(dispatch({
|
|
235
|
+
taskId: task.id,
|
|
236
|
+
target,
|
|
237
|
+
message: task.prompt,
|
|
238
|
+
})).catch((err) => {
|
|
87
239
|
const detail = err && err.message ? err.message : String(err || "dispatch failed");
|
|
88
|
-
log(`cron dispatch failed task=${
|
|
240
|
+
log(`cron dispatch failed task=${task.id} target=${target}: ${detail}`);
|
|
89
241
|
});
|
|
90
242
|
} catch (err) {
|
|
91
243
|
const detail = err && err.message ? err.message : String(err || "dispatch failed");
|
|
92
|
-
log(`cron dispatch failed task=${
|
|
244
|
+
log(`cron dispatch failed task=${task.id} target=${target}: ${detail}`);
|
|
93
245
|
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function stopTask(taskId = "") {
|
|
250
|
+
const id = String(taskId || "").trim();
|
|
251
|
+
if (!id) return false;
|
|
252
|
+
const idx = tasks.findIndex((task) => task.id === id);
|
|
253
|
+
if (idx < 0) return false;
|
|
254
|
+
|
|
255
|
+
const task = tasks[idx];
|
|
256
|
+
clearTaskTimer(task);
|
|
257
|
+
tasks.splice(idx, 1);
|
|
258
|
+
persistState();
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function attachTaskTimer(task) {
|
|
263
|
+
if (task.onceAtMs > 0) {
|
|
264
|
+
const delay = Math.max(0, task.onceAtMs - nowFn());
|
|
265
|
+
task.timer = setTimeoutFn(() => {
|
|
266
|
+
runTask(task);
|
|
267
|
+
stopTask(task.id);
|
|
268
|
+
}, delay);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
task.timer = setIntervalFn(() => {
|
|
273
|
+
runTask(task);
|
|
274
|
+
persistState();
|
|
275
|
+
}, task.intervalMs);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function addTask({ intervalMs = 0, onceAtMs = 0, targets = [], prompt = "" } = {}) {
|
|
279
|
+
const safeInterval = Number.parseInt(intervalMs, 10);
|
|
280
|
+
const safeOnceAt = Number.parseInt(onceAtMs, 10);
|
|
281
|
+
const safeTargets = Array.isArray(targets)
|
|
282
|
+
? targets.map((item) => String(item || "").trim()).filter(Boolean)
|
|
283
|
+
: [];
|
|
284
|
+
const safePrompt = String(prompt || "").trim();
|
|
285
|
+
|
|
286
|
+
if (!safePrompt || safeTargets.length === 0) return null;
|
|
287
|
+
|
|
288
|
+
const useOnce = Number.isFinite(safeOnceAt) && safeOnceAt > 0;
|
|
289
|
+
if (!useOnce) {
|
|
290
|
+
if (!Number.isFinite(safeInterval) || safeInterval < 1000) return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const task = {
|
|
294
|
+
id: nextTaskId(),
|
|
295
|
+
intervalMs: useOnce ? 0 : safeInterval,
|
|
296
|
+
onceAtMs: useOnce ? safeOnceAt : 0,
|
|
297
|
+
targets: Array.from(new Set(safeTargets)),
|
|
298
|
+
prompt: safePrompt,
|
|
299
|
+
createdAt: nowFn(),
|
|
300
|
+
lastRunAt: 0,
|
|
301
|
+
tickCount: 0,
|
|
302
|
+
timer: null,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
attachTaskTimer(task);
|
|
306
|
+
tasks.push(task);
|
|
307
|
+
persistState();
|
|
308
|
+
|
|
309
|
+
return formatCronTask(task);
|
|
310
|
+
}
|
|
99
311
|
|
|
100
312
|
function listTasks() {
|
|
101
|
-
return
|
|
313
|
+
return tasks.map((task) => formatCronTask(task));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function stopAll() {
|
|
317
|
+
if (tasks.length === 0) return 0;
|
|
318
|
+
const count = tasks.length;
|
|
319
|
+
while (tasks.length > 0) {
|
|
320
|
+
const task = tasks.pop();
|
|
321
|
+
clearTaskTimer(task);
|
|
322
|
+
}
|
|
323
|
+
persistState();
|
|
324
|
+
return count;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function recoverPersistedTasks() {
|
|
328
|
+
if (!persistedFile) return;
|
|
329
|
+
if (!fsModule.existsSync(persistedFile)) return;
|
|
330
|
+
|
|
331
|
+
let payload = null;
|
|
332
|
+
try {
|
|
333
|
+
payload = JSON.parse(fsModule.readFileSync(persistedFile, "utf8"));
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const detail = err && err.message ? err.message : String(err || "read failed");
|
|
336
|
+
log(`cron load failed: ${detail}`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const persistedSeq = Number(payload && payload.seq);
|
|
341
|
+
if (Number.isFinite(persistedSeq) && persistedSeq > 0) {
|
|
342
|
+
seq = Math.floor(persistedSeq);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const rawTasks = Array.isArray(payload && payload.tasks) ? payload.tasks : [];
|
|
346
|
+
if (rawTasks.length === 0) {
|
|
347
|
+
persistState();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const now = nowFn();
|
|
352
|
+
let changed = false;
|
|
353
|
+
|
|
354
|
+
for (const item of rawTasks) {
|
|
355
|
+
const rawId = String(item && item.id ? item.id : "").trim();
|
|
356
|
+
const parsedId = rawId.match(/^c(\d+)$/i);
|
|
357
|
+
if (parsedId) {
|
|
358
|
+
const numericId = Number.parseInt(parsedId[1], 10);
|
|
359
|
+
if (Number.isFinite(numericId) && numericId > seq) {
|
|
360
|
+
seq = numericId;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const intervalMs = Number(item && item.intervalMs);
|
|
365
|
+
const onceAtMs = Number(item && item.onceAtMs);
|
|
366
|
+
const targets = Array.isArray(item && item.targets)
|
|
367
|
+
? item.targets.map((v) => String(v || "").trim()).filter(Boolean)
|
|
368
|
+
: [];
|
|
369
|
+
const prompt = String(item && item.prompt ? item.prompt : "").trim();
|
|
370
|
+
|
|
371
|
+
if (!prompt || targets.length === 0) {
|
|
372
|
+
changed = true;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (Number.isFinite(onceAtMs) && onceAtMs > 0) {
|
|
377
|
+
if (onceAtMs <= now) {
|
|
378
|
+
changed = true;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
} else if (!Number.isFinite(intervalMs) || intervalMs < 1000) {
|
|
382
|
+
changed = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const task = {
|
|
387
|
+
id: rawId || nextTaskId(),
|
|
388
|
+
intervalMs: Number.isFinite(intervalMs) ? Math.floor(intervalMs) : 0,
|
|
389
|
+
onceAtMs: Number.isFinite(onceAtMs) ? Math.floor(onceAtMs) : 0,
|
|
390
|
+
targets: Array.from(new Set(targets)),
|
|
391
|
+
prompt,
|
|
392
|
+
createdAt: Number(item && item.createdAt) || now,
|
|
393
|
+
lastRunAt: Number(item && item.lastRunAt) || 0,
|
|
394
|
+
tickCount: Number(item && item.tickCount) || 0,
|
|
395
|
+
timer: null,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
attachTaskTimer(task);
|
|
399
|
+
tasks.push(task);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (changed) {
|
|
403
|
+
persistState();
|
|
404
|
+
}
|
|
102
405
|
}
|
|
103
406
|
|
|
407
|
+
recoverPersistedTasks();
|
|
408
|
+
|
|
104
409
|
function handleCronOp(op = {}) {
|
|
105
410
|
const operation = resolveCronOperation(op);
|
|
106
411
|
|
|
107
412
|
if (operation === "list" || operation === "ls") {
|
|
108
|
-
const
|
|
413
|
+
const listed = listTasks();
|
|
109
414
|
return {
|
|
110
415
|
action: "cron",
|
|
111
416
|
operation: "list",
|
|
112
417
|
ok: true,
|
|
113
|
-
count:
|
|
114
|
-
tasks,
|
|
418
|
+
count: listed.length,
|
|
419
|
+
tasks: listed,
|
|
115
420
|
};
|
|
116
421
|
}
|
|
117
422
|
|
|
@@ -127,7 +432,7 @@ function createDaemonCronController(options = {}) {
|
|
|
127
432
|
}
|
|
128
433
|
|
|
129
434
|
if (id === "all") {
|
|
130
|
-
const stopped =
|
|
435
|
+
const stopped = stopAll();
|
|
131
436
|
return {
|
|
132
437
|
action: "cron",
|
|
133
438
|
operation: "stop",
|
|
@@ -137,7 +442,7 @@ function createDaemonCronController(options = {}) {
|
|
|
137
442
|
};
|
|
138
443
|
}
|
|
139
444
|
|
|
140
|
-
const ok =
|
|
445
|
+
const ok = stopTask(id);
|
|
141
446
|
if (!ok) {
|
|
142
447
|
return {
|
|
143
448
|
action: "cron",
|
|
@@ -167,7 +472,36 @@ function createDaemonCronController(options = {}) {
|
|
|
167
472
|
}
|
|
168
473
|
|
|
169
474
|
const intervalMs = resolveCronIntervalMs(op);
|
|
170
|
-
|
|
475
|
+
const onceAtMs = resolveCronOnceAtMs(op);
|
|
476
|
+
|
|
477
|
+
if (intervalMs > 0 && onceAtMs > 0) {
|
|
478
|
+
return {
|
|
479
|
+
action: "cron",
|
|
480
|
+
operation: "start",
|
|
481
|
+
ok: false,
|
|
482
|
+
error: "cron start accepts either every or at/once, not both",
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (onceAtMs > 0 && onceAtMs <= nowFn()) {
|
|
487
|
+
return {
|
|
488
|
+
action: "cron",
|
|
489
|
+
operation: "start",
|
|
490
|
+
ok: false,
|
|
491
|
+
error: "one-time cron time must be in the future",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (intervalMs <= 0 && onceAtMs <= 0) {
|
|
496
|
+
return {
|
|
497
|
+
action: "cron",
|
|
498
|
+
operation: "start",
|
|
499
|
+
ok: false,
|
|
500
|
+
error: "cron start requires every or at/once",
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (intervalMs > 0 && intervalMs < 1000) {
|
|
171
505
|
return {
|
|
172
506
|
action: "cron",
|
|
173
507
|
operation: "start",
|
|
@@ -196,8 +530,9 @@ function createDaemonCronController(options = {}) {
|
|
|
196
530
|
};
|
|
197
531
|
}
|
|
198
532
|
|
|
199
|
-
const task =
|
|
533
|
+
const task = addTask({
|
|
200
534
|
intervalMs,
|
|
535
|
+
onceAtMs,
|
|
201
536
|
targets,
|
|
202
537
|
prompt,
|
|
203
538
|
});
|
|
@@ -215,14 +550,10 @@ function createDaemonCronController(options = {}) {
|
|
|
215
550
|
action: "cron",
|
|
216
551
|
operation: "start",
|
|
217
552
|
ok: true,
|
|
218
|
-
task
|
|
553
|
+
task,
|
|
219
554
|
};
|
|
220
555
|
}
|
|
221
556
|
|
|
222
|
-
function stopAll() {
|
|
223
|
-
return scheduler.stopAll();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
557
|
return {
|
|
227
558
|
handleCronOp,
|
|
228
559
|
listTasks,
|
|
@@ -235,7 +566,9 @@ module.exports = {
|
|
|
235
566
|
normalizeCronTargets,
|
|
236
567
|
resolveCronOperation,
|
|
237
568
|
resolveCronIntervalMs,
|
|
569
|
+
resolveCronOnceAtMs,
|
|
238
570
|
resolveCronPrompt,
|
|
239
571
|
resolveCronTaskId,
|
|
572
|
+
parseCronAtMs,
|
|
240
573
|
formatCronTask,
|
|
241
574
|
};
|
package/src/daemon/index.js
CHANGED
|
@@ -673,6 +673,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
673
673
|
providerSessions = loadProviderSessionCache(projectRoot);
|
|
674
674
|
probeHandles = new Map();
|
|
675
675
|
daemonCronController = createDaemonCronController({
|
|
676
|
+
projectRoot,
|
|
676
677
|
dispatch: async ({ taskId, target, message }) => {
|
|
677
678
|
await dispatchMessages(projectRoot, [{ target, message }]);
|
|
678
679
|
log(`cron:${taskId} -> ${target}`);
|
|
@@ -827,6 +828,69 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
827
828
|
type: IPC_RESPONSE_TYPES.ERROR,
|
|
828
829
|
error: err.message || "bus_send failed",
|
|
829
830
|
})}
|
|
831
|
+
`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (req.type === IPC_REQUEST_TYPES.CRON) {
|
|
837
|
+
if (!daemonCronController) {
|
|
838
|
+
socket.write(
|
|
839
|
+
`${JSON.stringify({
|
|
840
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
841
|
+
error: "cron controller unavailable",
|
|
842
|
+
})}
|
|
843
|
+
`,
|
|
844
|
+
);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
try {
|
|
849
|
+
const result = daemonCronController.handleCronOp(req);
|
|
850
|
+
let reply = "";
|
|
851
|
+
if (!result.ok) {
|
|
852
|
+
reply = `Cron failed: ${result.error || "unknown error"}`;
|
|
853
|
+
} else if (result.operation === "list") {
|
|
854
|
+
reply = result.count > 0
|
|
855
|
+
? `Cron ${result.count} task(s)`
|
|
856
|
+
: "Cron none";
|
|
857
|
+
} else if (result.operation === "stop") {
|
|
858
|
+
if (result.id === "all") {
|
|
859
|
+
reply = `Stopped ${result.stopped || 0} cron task(s)`;
|
|
860
|
+
} else {
|
|
861
|
+
reply = `Stopped cron task ${result.id}`;
|
|
862
|
+
}
|
|
863
|
+
} else if (result.operation === "start" && result.task) {
|
|
864
|
+
if (result.task.mode === "once") {
|
|
865
|
+
reply = `Cron scheduled ${result.task.id} at ${result.task.onceAt || result.task.onceAtMs}`;
|
|
866
|
+
} else {
|
|
867
|
+
reply = `Cron started ${result.task.id}: every ${result.task.interval || result.task.intervalMs}`;
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
870
|
+
reply = "Cron updated";
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
socket.write(
|
|
874
|
+
`${JSON.stringify({
|
|
875
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
876
|
+
data: {
|
|
877
|
+
reply,
|
|
878
|
+
cron: result,
|
|
879
|
+
ops: [{ action: "cron", operation: result.operation || String(req.operation || "") }],
|
|
880
|
+
},
|
|
881
|
+
})}
|
|
882
|
+
`,
|
|
883
|
+
);
|
|
884
|
+
ipcServer.sendToSockets({
|
|
885
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
886
|
+
data: buildRuntimeStatus(),
|
|
887
|
+
});
|
|
888
|
+
} catch (err) {
|
|
889
|
+
socket.write(
|
|
890
|
+
`${JSON.stringify({
|
|
891
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
892
|
+
error: err.message || "cron request failed",
|
|
893
|
+
})}
|
|
830
894
|
`,
|
|
831
895
|
);
|
|
832
896
|
}
|
package/src/daemon/status.js
CHANGED
|
@@ -65,8 +65,11 @@ function normalizeCronTasks(raw = []) {
|
|
|
65
65
|
const items = Array.isArray(raw) ? raw : [];
|
|
66
66
|
return items.map((task) => ({
|
|
67
67
|
id: String(task && task.id ? task.id : ""),
|
|
68
|
+
mode: String(task && task.mode ? task.mode : ((task && task.onceAtMs) ? "once" : "interval")),
|
|
68
69
|
intervalMs: Number(task && task.intervalMs ? task.intervalMs : 0) || 0,
|
|
69
70
|
interval: String(task && task.interval ? task.interval : ""),
|
|
71
|
+
onceAtMs: Number(task && task.onceAtMs ? task.onceAtMs : 0) || 0,
|
|
72
|
+
onceAt: String(task && task.onceAt ? task.onceAt : ""),
|
|
70
73
|
targets: Array.isArray(task && task.targets) ? task.targets.slice() : [],
|
|
71
74
|
prompt: String(task && task.prompt ? task.prompt : ""),
|
|
72
75
|
summary: String(task && task.summary ? task.summary : ""),
|
package/src/online/bridge.js
CHANGED
|
@@ -151,7 +151,7 @@ class OnlineConnect {
|
|
|
151
151
|
this.projectRoot = options.projectRoot || process.cwd();
|
|
152
152
|
this.nickname = options.nickname || "";
|
|
153
153
|
this.subscriberId = options.subscriberId || autoSubscriberId(this.nickname);
|
|
154
|
-
this.url = options.url || "
|
|
154
|
+
this.url = options.url || "wss://online.ufoo.dev/ufoo/online";
|
|
155
155
|
this.world = options.world || "default";
|
|
156
156
|
this.agentType = options.agentType || "ufoo";
|
|
157
157
|
this.tokenFile = options.tokenFile || "";
|
package/src/online/client.js
CHANGED
|
@@ -20,7 +20,7 @@ function waitForOpen(ws, timeoutMs = 5000) {
|
|
|
20
20
|
class OnlineClient extends EventEmitter {
|
|
21
21
|
constructor(options = {}) {
|
|
22
22
|
super();
|
|
23
|
-
this.url = options.url || "
|
|
23
|
+
this.url = options.url || "wss://online.ufoo.dev/ufoo/online";
|
|
24
24
|
this.subscriberId = options.subscriberId || "";
|
|
25
25
|
this.nickname = options.nickname || "";
|
|
26
26
|
this.world = options.world || "default";
|