@shawnstack/quickforge 1.1.0 → 1.2.1
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/README.md +1 -1
- package/bin/quickforge.mjs +72 -7
- package/dist/assets/{anthropic-By-wpU1w.js → anthropic-DLvtwHL2.js} +2 -2
- package/dist/assets/{azure-openai-responses-C8spS__i.js → azure-openai-responses-D68z7hLN.js} +1 -1
- package/dist/assets/css-utils-rkE68RDy.js +1 -0
- package/dist/assets/{google-DiIcyajo.js → google-B_sSaRBM.js} +1 -1
- package/dist/assets/{google-gemini-cli-BXZFGMXD.js → google-gemini-cli-CYqGXjGi.js} +1 -1
- package/dist/assets/google-shared-XhYUKiGZ.js +11 -0
- package/dist/assets/{google-vertex-D93MV5Cx.js → google-vertex-DSMuB4YB.js} +1 -1
- package/dist/assets/icons-BsZ9PlYY.js +1 -0
- package/dist/assets/index-BqFfVQJM.css +3 -0
- package/dist/assets/index-DoraECXN.js +3187 -0
- package/dist/assets/lit-vendor-1dsGB-Iy.js +2 -0
- package/dist/assets/{mistral-BAJNGYqd.js → mistral-BZngRB4x.js} +2 -2
- package/dist/assets/{openai-codex-responses-BHHCy65K.js → openai-codex-responses-Niu7xDYK.js} +1 -1
- package/dist/assets/openai-completions-B2bhb9k0.js +5 -0
- package/dist/assets/{openai-responses-CP9-AyAD.js → openai-responses-CDYDv8yL.js} +1 -1
- package/dist/assets/{openai-responses-shared-_z7sua8J.js → openai-responses-shared-BIKPTpEQ.js} +1 -1
- package/dist/assets/react-vendor-Ds3ovY0w.js +9 -0
- package/dist/assets/rolldown-runtime-CkqCuyE9.js +1 -0
- package/dist/index.html +7 -3
- package/package.json +14 -13
- package/server/agent-manager.mjs +1053 -0
- package/server/conversation-compaction.mjs +302 -0
- package/server/custom-commands.mjs +344 -0
- package/server/index.mjs +322 -32
- package/server/project-config.mjs +80 -31
- package/server/reasoning-cache.mjs +51 -0
- package/server/restart-supervisor.mjs +38 -0
- package/server/routes/agent.mjs +323 -0
- package/server/routes/backup.mjs +250 -0
- package/server/routes/instructions.mjs +6 -17
- package/server/routes/project.mjs +46 -10
- package/server/routes/scheduled-tasks.mjs +424 -0
- package/server/routes/shared-conversation.mjs +404 -0
- package/server/routes/shares.mjs +84 -0
- package/server/routes/skills.mjs +145 -0
- package/server/routes/static.mjs +4 -3
- package/server/routes/storage.mjs +58 -10
- package/server/routes/system.mjs +35 -0
- package/server/routes/tools.mjs +53 -2
- package/server/session-utils.mjs +102 -0
- package/server/share-store.mjs +468 -0
- package/server/skills.mjs +539 -0
- package/server/storage.mjs +247 -6
- package/server/system-prompt.mjs +67 -0
- package/server/tools/definitions.mjs +120 -0
- package/server/tools/index.mjs +167 -46
- package/server/utils/logger.mjs +34 -0
- package/server/utils/network.mjs +38 -0
- package/server/utils/platform.mjs +30 -0
- package/server/utils/response.mjs +8 -1
- package/skills/ai-context-package/SKILL.md +104 -0
- package/skills/ai-context-package/skill.json +9 -0
- package/skills/code-review/SKILL.md +23 -0
- package/skills/code-review/skill.json +9 -0
- package/skills/frontend-react/SKILL.md +22 -0
- package/skills/frontend-react/skill.json +9 -0
- package/skills/quickforge-project/SKILL.md +22 -0
- package/skills/quickforge-project/skill.json +9 -0
- package/dist/assets/chunk-62oNxeRG.js +0 -1
- package/dist/assets/confirm-dialog-4mZt9XEq.js +0 -1
- package/dist/assets/google-shared-CXUHW-9O.js +0 -11
- package/dist/assets/index-Bq6VHkyY.js +0 -3048
- package/dist/assets/index-D7uXa1RT.css +0 -3
- package/dist/assets/openai-completions-BtZAvOiJ.js +0 -5
- package/dist/assets/prompt-dialog-BGMKszUz.js +0 -1
- /package/dist/assets/{github-copilot-headers-C0toI16e.js → github-copilot-headers-CrI0CIJ7.js} +0 -0
- /package/dist/assets/{hash-fDQBJsbb.js → hash-Bt1aVMQ3.js} +0 -0
- /package/dist/assets/{headers-Drkm68SQ.js → headers-5EYI0_pl.js} +0 -0
- /package/dist/assets/{openai-CuiHR4mv.js → openai-Cn7eGqwa.js} +0 -0
- /package/dist/assets/{transform-messages-BFwlToJ0.js → transform-messages-CV4kCtBB.js} +0 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { streamSimple } from '@mariozechner/pi-ai'
|
|
2
|
+
import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
|
|
3
|
+
import { readStore, atomicUpdate } from '../storage.mjs'
|
|
4
|
+
import { createAgent, runPrompt, getSessionEventBus } from '../agent-manager.mjs'
|
|
5
|
+
import { getActiveProject, readProjectConfig } from '../project-config.mjs'
|
|
6
|
+
import { logger } from '../utils/logger.mjs'
|
|
7
|
+
|
|
8
|
+
const STORE = 'scheduled-tasks'
|
|
9
|
+
const RUN_CHECK_INTERVAL_MS = 30 * 1000
|
|
10
|
+
const cronRegex = /^(\*|\d{1,2}|\d{1,2}-\d{1,2}|\d{1,2}\/\d{1,2}|\*\/\d{1,2})(\s+(\*|\d{1,2}|\d{1,2}-\d{1,2}|\d{1,2}\/\d{1,2}|\*\/\d{1,2})){4}$/
|
|
11
|
+
const minuteMs = 60 * 1000
|
|
12
|
+
const hourMs = 60 * minuteMs
|
|
13
|
+
const dayMs = 24 * hourMs
|
|
14
|
+
|
|
15
|
+
let schedulerTimer = null
|
|
16
|
+
let running = false
|
|
17
|
+
const runningTaskIds = new Set()
|
|
18
|
+
|
|
19
|
+
function createId() {
|
|
20
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseCronField(field, min, max) {
|
|
24
|
+
if (field === '*') return { any: true, values: [] }
|
|
25
|
+
const values = new Set()
|
|
26
|
+
for (const part of field.split(',')) {
|
|
27
|
+
if (/^\*\/\d+$/.test(part)) {
|
|
28
|
+
const step = Number(part.slice(2))
|
|
29
|
+
for (let value = min; value <= max; value += step) values.add(value)
|
|
30
|
+
} else if (/^\d+-\d+$/.test(part)) {
|
|
31
|
+
const [start, end] = part.split('-').map(Number)
|
|
32
|
+
for (let value = Math.max(start, min); value <= Math.min(end, max); value += 1) values.add(value)
|
|
33
|
+
} else if (/^\d+$/.test(part)) {
|
|
34
|
+
const value = Number(part)
|
|
35
|
+
if (value >= min && value <= max) values.add(value)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { any: false, values: [...values] }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cronMatches(date, cronExpression) {
|
|
42
|
+
const fields = String(cronExpression || '').trim().split(/\s+/)
|
|
43
|
+
if (fields.length !== 5) return false
|
|
44
|
+
const checks = [
|
|
45
|
+
[date.getMinutes(), parseCronField(fields[0], 0, 59)],
|
|
46
|
+
[date.getHours(), parseCronField(fields[1], 0, 23)],
|
|
47
|
+
[date.getDate(), parseCronField(fields[2], 1, 31)],
|
|
48
|
+
[date.getMonth() + 1, parseCronField(fields[3], 1, 12)],
|
|
49
|
+
[date.getDay(), parseCronField(fields[4], 0, 6)],
|
|
50
|
+
]
|
|
51
|
+
return checks.every(([value, rule]) => rule.any || rule.values.includes(value))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nextCronRun(cronExpression, base = new Date()) {
|
|
55
|
+
const cursor = new Date(base.getTime() + minuteMs)
|
|
56
|
+
cursor.setSeconds(0, 0)
|
|
57
|
+
const maxChecks = 366 * 24 * 60
|
|
58
|
+
for (let index = 0; index < maxChecks; index += 1) {
|
|
59
|
+
if (cronMatches(cursor, cronExpression)) return cursor
|
|
60
|
+
cursor.setMinutes(cursor.getMinutes() + 1)
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeAiJson(text) {
|
|
66
|
+
const raw = String(text || '').trim()
|
|
67
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
|
|
68
|
+
const candidate = fenced?.[1] ?? raw
|
|
69
|
+
const start = candidate.indexOf('{')
|
|
70
|
+
const end = candidate.lastIndexOf('}')
|
|
71
|
+
if (start < 0 || end < start) return null
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(candidate.slice(start, end + 1))
|
|
74
|
+
} catch {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractTitle(instruction) {
|
|
80
|
+
return instruction
|
|
81
|
+
.replace(/^(请|帮我|给我|麻烦)?/, '')
|
|
82
|
+
.replace(/(每天|每日|明天|今天|每周[一二三四五六日天]?|每月\d{1,2}[号日]?|每隔\d+\s*(分钟|小时)).*?(提醒我|帮我|执行|运行|生成|检查)?/, '')
|
|
83
|
+
.trim()
|
|
84
|
+
.slice(0, 32) || 'AI 定时任务'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getApiKey(provider) {
|
|
88
|
+
try {
|
|
89
|
+
const keys = await readStore('provider-keys')
|
|
90
|
+
return keys?.[provider] || undefined
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function parseScheduledTaskInstructionWithAi(instruction, model, thinkingLevel = 'off') {
|
|
97
|
+
const text = String(instruction || '').trim()
|
|
98
|
+
if (!text) return { needMoreInfo: true, question: '请输入要创建的定时任务。' }
|
|
99
|
+
if (!model) return { needMoreInfo: true, question: '请先选择用于解析任务的大模型。' }
|
|
100
|
+
|
|
101
|
+
const now = new Date()
|
|
102
|
+
const systemPrompt = `你是定时任务解析器。把用户的中文自然语言定时任务解析为 JSON。
|
|
103
|
+
只输出 JSON,不要 Markdown,不要解释。
|
|
104
|
+
字段:
|
|
105
|
+
- title: 简短任务名称
|
|
106
|
+
- instruction: 到时间后真正交给 AI 执行的指令,去掉时间规则,保留要做什么
|
|
107
|
+
- cronExpression: 5 位 cron,格式为 "分钟 小时 日 月 周",周日用 0。不支持秒。
|
|
108
|
+
- scheduleRule: 给用户看的中文执行规则
|
|
109
|
+
- question: 如果时间或任务不明确,写一句追问
|
|
110
|
+
规则:
|
|
111
|
+
- 如果信息明确,question 为空字符串。
|
|
112
|
+
- 如果信息不明确,不要编造 cronExpression。
|
|
113
|
+
- 当前时间:${now.toISOString()},本地时区:${Intl.DateTimeFormat().resolvedOptions().timeZone || 'local'}。
|
|
114
|
+
示例输出:{"title":"生成日报","instruction":"生成销售日报","cronExpression":"0 9 * * *","scheduleRule":"每天 09:00","question":""}`
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const stream = streamSimple(
|
|
118
|
+
model,
|
|
119
|
+
{
|
|
120
|
+
systemPrompt,
|
|
121
|
+
messages: [{ role: 'user', content: text, timestamp: Date.now() }],
|
|
122
|
+
tools: [],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
apiKey: await getApiKey(model.provider),
|
|
126
|
+
maxTokens: 600,
|
|
127
|
+
temperature: 0,
|
|
128
|
+
reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
|
|
129
|
+
maxRetryDelayMs: 60000,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
const message = await stream.result()
|
|
133
|
+
const content = Array.isArray(message.content)
|
|
134
|
+
? message.content.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('\n')
|
|
135
|
+
: ''
|
|
136
|
+
const parsed = normalizeAiJson(content)
|
|
137
|
+
if (!parsed) return { needMoreInfo: true, question: 'AI 没有返回有效 JSON,请重试或换一个模型。' }
|
|
138
|
+
if (parsed.question) return { needMoreInfo: true, question: String(parsed.question) }
|
|
139
|
+
if (!cronRegex.test(String(parsed.cronExpression || '').trim())) {
|
|
140
|
+
return { needMoreInfo: true, question: 'AI 未能生成有效的 cron 表达式,请补充更明确的执行时间。' }
|
|
141
|
+
}
|
|
142
|
+
const nextRun = nextCronRun(String(parsed.cronExpression).trim())
|
|
143
|
+
if (!nextRun) return { needMoreInfo: true, question: '无法计算下一次执行时间,请换一个时间规则。' }
|
|
144
|
+
return {
|
|
145
|
+
needMoreInfo: false,
|
|
146
|
+
task: {
|
|
147
|
+
title: String(parsed.title || extractTitle(text)).slice(0, 80),
|
|
148
|
+
instruction: String(parsed.instruction || text).trim(),
|
|
149
|
+
scheduleType: 'cron',
|
|
150
|
+
scheduleRule: String(parsed.scheduleRule || parsed.cronExpression).trim(),
|
|
151
|
+
cronExpression: String(parsed.cronExpression).trim(),
|
|
152
|
+
nextRunAt: nextRun.toISOString(),
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logger.warn('AI scheduled task parsing failed:', error?.message || error)
|
|
157
|
+
return { needMoreInfo: true, question: `AI 解析失败:${error?.message || '请检查模型配置和 API Key 后重试。'}` }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function calculateNextRun(task) {
|
|
162
|
+
if (task.cronExpression) {
|
|
163
|
+
return nextCronRun(task.cronExpression)?.toISOString()
|
|
164
|
+
}
|
|
165
|
+
const current = new Date(task.nextRunAt)
|
|
166
|
+
if (task.scheduleType === 'once') return undefined
|
|
167
|
+
if (task.scheduleType === 'interval') {
|
|
168
|
+
const interval = task.scheduleRule.match(/每隔\s*(\d+)\s*(分钟|小时)/)
|
|
169
|
+
const amount = Number(interval?.[1] ?? '30')
|
|
170
|
+
const unit = interval?.[2] ?? '分钟'
|
|
171
|
+
return new Date(Date.now() + amount * (unit === '小时' ? hourMs : minuteMs)).toISOString()
|
|
172
|
+
}
|
|
173
|
+
if (task.scheduleType === 'daily') return new Date(current.getTime() + dayMs).toISOString()
|
|
174
|
+
if (task.scheduleType === 'weekly') return new Date(current.getTime() + 7 * dayMs).toISOString()
|
|
175
|
+
if (task.scheduleType === 'monthly') {
|
|
176
|
+
current.setMonth(current.getMonth() + 1)
|
|
177
|
+
return current.toISOString()
|
|
178
|
+
}
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function getTasks() {
|
|
183
|
+
const data = await readStore(STORE)
|
|
184
|
+
return Object.values(data).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function updateTask(taskId, updater) {
|
|
188
|
+
let updated = null
|
|
189
|
+
await atomicUpdate(STORE, (data) => {
|
|
190
|
+
if (!data[taskId]) return data
|
|
191
|
+
updated = updater(data[taskId])
|
|
192
|
+
data[taskId] = updated
|
|
193
|
+
return data
|
|
194
|
+
})
|
|
195
|
+
return updated
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function repairRecurringTaskStatuses() {
|
|
199
|
+
const now = Date.now()
|
|
200
|
+
await atomicUpdate(STORE, (data) => {
|
|
201
|
+
for (const [taskId, task] of Object.entries(data)) {
|
|
202
|
+
if (!task?.cronExpression || task.status !== 'expired') continue
|
|
203
|
+
const nextRunAt = task.nextRunAt && new Date(task.nextRunAt).getTime() > now
|
|
204
|
+
? task.nextRunAt
|
|
205
|
+
: nextCronRun(task.cronExpression)?.toISOString()
|
|
206
|
+
data[taskId] = {
|
|
207
|
+
...task,
|
|
208
|
+
status: 'enabled',
|
|
209
|
+
nextRunAt: nextRunAt ?? new Date(Date.now() + minuteMs).toISOString(),
|
|
210
|
+
updatedAt: new Date().toISOString(),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return data
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function executeTask(task, trigger = 'schedule') {
|
|
218
|
+
if (runningTaskIds.has(task.id)) return
|
|
219
|
+
runningTaskIds.add(task.id)
|
|
220
|
+
const runId = createId()
|
|
221
|
+
const startedAt = new Date().toISOString()
|
|
222
|
+
|
|
223
|
+
await updateTask(task.id, (current) => ({
|
|
224
|
+
...current,
|
|
225
|
+
status: 'running',
|
|
226
|
+
currentRunId: runId,
|
|
227
|
+
runs: [{ id: runId, status: 'running', trigger, startedAt }, ...(current.runs || [])].slice(0, 20),
|
|
228
|
+
}))
|
|
229
|
+
|
|
230
|
+
const sessionId = `scheduled-${task.id}-${Date.now().toString(36)}`
|
|
231
|
+
let settled = false
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const config = await readProjectConfig()
|
|
235
|
+
const selectedProject = task.projectId ? config.projects.find((project) => project.id === task.projectId) : null
|
|
236
|
+
const activeProject = selectedProject ?? getActiveProject(config)
|
|
237
|
+
const settings = await readStore('settings')
|
|
238
|
+
const yoloMode = settings?.['yolo-mode'] === true || settings?.['yolo-mode'] === 'true'
|
|
239
|
+
|
|
240
|
+
const session = await createAgent(sessionId, {
|
|
241
|
+
scope: activeProject ? 'project' : 'global',
|
|
242
|
+
projectId: activeProject?.id || null,
|
|
243
|
+
yoloMode,
|
|
244
|
+
model: task.model,
|
|
245
|
+
thinkingLevel: task.thinkingLevel,
|
|
246
|
+
title: `[定时任务] ${task.title}`,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const eventBus = getSessionEventBus(sessionId)
|
|
250
|
+
const finished = new Promise((resolve) => {
|
|
251
|
+
const timeout = setTimeout(() => resolve({ ok: false, error: '执行超时' }), 30 * 60 * 1000)
|
|
252
|
+
eventBus?.on('agent_event', (event) => {
|
|
253
|
+
if (event.type !== 'agent_end') return
|
|
254
|
+
clearTimeout(timeout)
|
|
255
|
+
resolve({ ok: !session.agent.state.errorMessage, error: session.agent.state.errorMessage })
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
await runPrompt(sessionId, task.instruction)
|
|
260
|
+
const result = await finished
|
|
261
|
+
settled = true
|
|
262
|
+
const finishedAt = new Date().toISOString()
|
|
263
|
+
const latestTask = (await readStore(STORE))[task.id] ?? task
|
|
264
|
+
const nextRunAt = calculateNextRun(latestTask)
|
|
265
|
+
const recurring = Boolean(latestTask.cronExpression) || !['once'].includes(latestTask.scheduleType)
|
|
266
|
+
|
|
267
|
+
await updateTask(task.id, (current) => ({
|
|
268
|
+
...current,
|
|
269
|
+
status: result.ok ? (nextRunAt || recurring ? 'enabled' : 'expired') : 'failed',
|
|
270
|
+
currentRunId: null,
|
|
271
|
+
lastRunAt: finishedAt,
|
|
272
|
+
nextRunAt: nextRunAt ?? (recurring ? new Date(Date.now() + minuteMs).toISOString() : current.nextRunAt),
|
|
273
|
+
lastSessionId: sessionId,
|
|
274
|
+
runs: (current.runs || []).map((run) => run.id === runId ? {
|
|
275
|
+
...run,
|
|
276
|
+
status: result.ok ? 'success' : 'failed',
|
|
277
|
+
result: result.ok ? `已完成,结果保存在会话 ${sessionId}` : undefined,
|
|
278
|
+
errorMessage: result.error,
|
|
279
|
+
sessionId,
|
|
280
|
+
finishedAt,
|
|
281
|
+
} : run),
|
|
282
|
+
}))
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const finishedAt = new Date().toISOString()
|
|
285
|
+
await updateTask(task.id, (current) => ({
|
|
286
|
+
...current,
|
|
287
|
+
status: 'failed',
|
|
288
|
+
currentRunId: null,
|
|
289
|
+
lastRunAt: finishedAt,
|
|
290
|
+
runs: (current.runs || []).map((run) => run.id === runId ? {
|
|
291
|
+
...run,
|
|
292
|
+
status: 'failed',
|
|
293
|
+
errorMessage: error?.message || String(error),
|
|
294
|
+
finishedAt,
|
|
295
|
+
} : run),
|
|
296
|
+
}))
|
|
297
|
+
} finally {
|
|
298
|
+
runningTaskIds.delete(task.id)
|
|
299
|
+
if (!settled) logger.warn(`Scheduled task ${task.id} finished without normal agent_end`)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function schedulerTick() {
|
|
304
|
+
if (running) return
|
|
305
|
+
running = true
|
|
306
|
+
try {
|
|
307
|
+
await repairRecurringTaskStatuses()
|
|
308
|
+
const now = Date.now()
|
|
309
|
+
const tasks = await getTasks()
|
|
310
|
+
for (const task of tasks) {
|
|
311
|
+
if (task.status !== 'enabled') continue
|
|
312
|
+
if (!task.nextRunAt || new Date(task.nextRunAt).getTime() > now) continue
|
|
313
|
+
executeTask(task, 'schedule').catch((error) => logger.error(`Scheduled task ${task.id} failed:`, error))
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
running = false
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function startScheduledTaskRunner() {
|
|
321
|
+
if (schedulerTimer) return
|
|
322
|
+
schedulerTimer = setInterval(() => {
|
|
323
|
+
schedulerTick().catch((error) => logger.error('Scheduled task tick failed:', error))
|
|
324
|
+
}, RUN_CHECK_INTERVAL_MS)
|
|
325
|
+
schedulerTick().catch((error) => logger.error('Scheduled task initial tick failed:', error))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function stopScheduledTaskRunner() {
|
|
329
|
+
if (!schedulerTimer) return
|
|
330
|
+
clearInterval(schedulerTimer)
|
|
331
|
+
schedulerTimer = null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function handleScheduledTasksApi(req, res, url) {
|
|
335
|
+
const parts = url.pathname.split('/').filter(Boolean)
|
|
336
|
+
|
|
337
|
+
if (req.method === 'POST' && url.pathname === '/api/scheduled-tasks/parse') {
|
|
338
|
+
const body = await readJsonBody(req)
|
|
339
|
+
sendJson(res, 200, await parseScheduledTaskInstructionWithAi(body?.instruction, body?.model, body?.thinkingLevel))
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (req.method === 'GET' && url.pathname === '/api/scheduled-tasks') {
|
|
344
|
+
sendJson(res, 200, { tasks: await getTasks() })
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (req.method === 'POST' && url.pathname === '/api/scheduled-tasks') {
|
|
349
|
+
const body = await readJsonBody(req)
|
|
350
|
+
const parsed = body?.task
|
|
351
|
+
if (!parsed) {
|
|
352
|
+
const error = new Error('Missing task')
|
|
353
|
+
error.statusCode = 400
|
|
354
|
+
throw error
|
|
355
|
+
}
|
|
356
|
+
const now = new Date().toISOString()
|
|
357
|
+
const task = {
|
|
358
|
+
id: createId(),
|
|
359
|
+
title: parsed.title,
|
|
360
|
+
instruction: parsed.instruction,
|
|
361
|
+
scheduleType: parsed.scheduleType,
|
|
362
|
+
scheduleRule: parsed.scheduleRule,
|
|
363
|
+
cronExpression: parsed.cronExpression,
|
|
364
|
+
nextRunAt: parsed.nextRunAt,
|
|
365
|
+
model: body?.model,
|
|
366
|
+
thinkingLevel: body?.thinkingLevel || (body?.model?.reasoning ? 'medium' : 'off'),
|
|
367
|
+
projectId: body?.projectId || null,
|
|
368
|
+
projectName: body?.projectName || null,
|
|
369
|
+
status: 'enabled',
|
|
370
|
+
createdAt: now,
|
|
371
|
+
updatedAt: now,
|
|
372
|
+
runs: [],
|
|
373
|
+
}
|
|
374
|
+
await atomicUpdate(STORE, (data) => {
|
|
375
|
+
data[task.id] = task
|
|
376
|
+
return data
|
|
377
|
+
})
|
|
378
|
+
sendJson(res, 200, { task })
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (parts[0] === 'api' && parts[1] === 'scheduled-tasks' && parts[2]) {
|
|
383
|
+
const taskId = decodeSegment(parts[2])
|
|
384
|
+
const action = parts[3]
|
|
385
|
+
|
|
386
|
+
if (req.method === 'DELETE' && !action) {
|
|
387
|
+
await atomicUpdate(STORE, (data) => {
|
|
388
|
+
delete data[taskId]
|
|
389
|
+
return data
|
|
390
|
+
})
|
|
391
|
+
sendJson(res, 200, { ok: true })
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (req.method === 'POST' && action === 'pause') {
|
|
396
|
+
const task = await updateTask(taskId, (current) => ({ ...current, status: 'paused', updatedAt: new Date().toISOString() }))
|
|
397
|
+
sendJson(res, 200, { task })
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (req.method === 'POST' && action === 'resume') {
|
|
402
|
+
const task = await updateTask(taskId, (current) => ({ ...current, status: 'enabled', updatedAt: new Date().toISOString() }))
|
|
403
|
+
sendJson(res, 200, { task })
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (req.method === 'POST' && action === 'run') {
|
|
408
|
+
const data = await readStore(STORE)
|
|
409
|
+
const task = data[taskId]
|
|
410
|
+
if (!task) {
|
|
411
|
+
const error = new Error('Task not found')
|
|
412
|
+
error.statusCode = 404
|
|
413
|
+
throw error
|
|
414
|
+
}
|
|
415
|
+
executeTask(task, 'manual').catch((error) => logger.error(`Manual scheduled task ${task.id} failed:`, error))
|
|
416
|
+
sendJson(res, 200, { ok: true })
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const error = new Error('Not found')
|
|
422
|
+
error.statusCode = 404
|
|
423
|
+
throw error
|
|
424
|
+
}
|