@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.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/bin/quickforge.mjs +72 -7
  3. package/dist/assets/{anthropic-By-wpU1w.js → anthropic-DLvtwHL2.js} +2 -2
  4. package/dist/assets/{azure-openai-responses-C8spS__i.js → azure-openai-responses-D68z7hLN.js} +1 -1
  5. package/dist/assets/css-utils-rkE68RDy.js +1 -0
  6. package/dist/assets/{google-DiIcyajo.js → google-B_sSaRBM.js} +1 -1
  7. package/dist/assets/{google-gemini-cli-BXZFGMXD.js → google-gemini-cli-CYqGXjGi.js} +1 -1
  8. package/dist/assets/google-shared-XhYUKiGZ.js +11 -0
  9. package/dist/assets/{google-vertex-D93MV5Cx.js → google-vertex-DSMuB4YB.js} +1 -1
  10. package/dist/assets/icons-BsZ9PlYY.js +1 -0
  11. package/dist/assets/index-BqFfVQJM.css +3 -0
  12. package/dist/assets/index-DoraECXN.js +3187 -0
  13. package/dist/assets/lit-vendor-1dsGB-Iy.js +2 -0
  14. package/dist/assets/{mistral-BAJNGYqd.js → mistral-BZngRB4x.js} +2 -2
  15. package/dist/assets/{openai-codex-responses-BHHCy65K.js → openai-codex-responses-Niu7xDYK.js} +1 -1
  16. package/dist/assets/openai-completions-B2bhb9k0.js +5 -0
  17. package/dist/assets/{openai-responses-CP9-AyAD.js → openai-responses-CDYDv8yL.js} +1 -1
  18. package/dist/assets/{openai-responses-shared-_z7sua8J.js → openai-responses-shared-BIKPTpEQ.js} +1 -1
  19. package/dist/assets/react-vendor-Ds3ovY0w.js +9 -0
  20. package/dist/assets/rolldown-runtime-CkqCuyE9.js +1 -0
  21. package/dist/index.html +7 -3
  22. package/package.json +14 -13
  23. package/server/agent-manager.mjs +1053 -0
  24. package/server/conversation-compaction.mjs +302 -0
  25. package/server/custom-commands.mjs +344 -0
  26. package/server/index.mjs +322 -32
  27. package/server/project-config.mjs +80 -31
  28. package/server/reasoning-cache.mjs +51 -0
  29. package/server/restart-supervisor.mjs +38 -0
  30. package/server/routes/agent.mjs +323 -0
  31. package/server/routes/backup.mjs +250 -0
  32. package/server/routes/instructions.mjs +6 -17
  33. package/server/routes/project.mjs +46 -10
  34. package/server/routes/scheduled-tasks.mjs +424 -0
  35. package/server/routes/shared-conversation.mjs +404 -0
  36. package/server/routes/shares.mjs +84 -0
  37. package/server/routes/skills.mjs +145 -0
  38. package/server/routes/static.mjs +4 -3
  39. package/server/routes/storage.mjs +58 -10
  40. package/server/routes/system.mjs +35 -0
  41. package/server/routes/tools.mjs +53 -2
  42. package/server/session-utils.mjs +102 -0
  43. package/server/share-store.mjs +468 -0
  44. package/server/skills.mjs +539 -0
  45. package/server/storage.mjs +247 -6
  46. package/server/system-prompt.mjs +67 -0
  47. package/server/tools/definitions.mjs +120 -0
  48. package/server/tools/index.mjs +167 -46
  49. package/server/utils/logger.mjs +34 -0
  50. package/server/utils/network.mjs +38 -0
  51. package/server/utils/platform.mjs +30 -0
  52. package/server/utils/response.mjs +8 -1
  53. package/skills/ai-context-package/SKILL.md +104 -0
  54. package/skills/ai-context-package/skill.json +9 -0
  55. package/skills/code-review/SKILL.md +23 -0
  56. package/skills/code-review/skill.json +9 -0
  57. package/skills/frontend-react/SKILL.md +22 -0
  58. package/skills/frontend-react/skill.json +9 -0
  59. package/skills/quickforge-project/SKILL.md +22 -0
  60. package/skills/quickforge-project/skill.json +9 -0
  61. package/dist/assets/chunk-62oNxeRG.js +0 -1
  62. package/dist/assets/confirm-dialog-4mZt9XEq.js +0 -1
  63. package/dist/assets/google-shared-CXUHW-9O.js +0 -11
  64. package/dist/assets/index-Bq6VHkyY.js +0 -3048
  65. package/dist/assets/index-D7uXa1RT.css +0 -3
  66. package/dist/assets/openai-completions-BtZAvOiJ.js +0 -5
  67. package/dist/assets/prompt-dialog-BGMKszUz.js +0 -1
  68. /package/dist/assets/{github-copilot-headers-C0toI16e.js → github-copilot-headers-CrI0CIJ7.js} +0 -0
  69. /package/dist/assets/{hash-fDQBJsbb.js → hash-Bt1aVMQ3.js} +0 -0
  70. /package/dist/assets/{headers-Drkm68SQ.js → headers-5EYI0_pl.js} +0 -0
  71. /package/dist/assets/{openai-CuiHR4mv.js → openai-Cn7eGqwa.js} +0 -0
  72. /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
+ }