@muyichengshayu/promptx 0.1.0
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 +80 -0
- package/apps/server/src/appPaths.js +102 -0
- package/apps/server/src/codex.js +585 -0
- package/apps/server/src/codexRunRuntime.js +212 -0
- package/apps/server/src/codexRuns.js +525 -0
- package/apps/server/src/codexSessions.js +149 -0
- package/apps/server/src/db.js +389 -0
- package/apps/server/src/gitDiff.js +1425 -0
- package/apps/server/src/index.js +909 -0
- package/apps/server/src/pdf.js +383 -0
- package/apps/server/src/repository.js +484 -0
- package/apps/server/src/sseHub.js +69 -0
- package/apps/server/src/upload.js +22 -0
- package/apps/server/src/workspaceFiles.js +662 -0
- package/apps/web/dist/assets/CodexSessionManagerDialog-c35LrKjV.js +6 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-BYcla0q4.js +12 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-C0uQRStP.js +1 -0
- package/apps/web/dist/assets/WorkbenchView-Cp_qHNdX.js +216 -0
- package/apps/web/dist/assets/index-D9ui_gwj.js +25 -0
- package/apps/web/dist/assets/index-DDNrspNi.css +1 -0
- package/apps/web/dist/index.html +16 -0
- package/bin/promptx.js +60 -0
- package/package.json +58 -0
- package/packages/shared/src/index.js +121 -0
- package/scripts/doctor.mjs +251 -0
- package/scripts/service.mjs +308 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { all, get, run, transaction } from './db.js'
|
|
3
|
+
import { getPromptxCodexSessionById } from './codexSessions.js'
|
|
4
|
+
import { captureRunGitBaseline, captureRunGitFinalSnapshot, captureTaskGitBaseline } from './gitDiff.js'
|
|
5
|
+
import { getTaskBySlug, updateTaskCodexSession } from './repository.js'
|
|
6
|
+
|
|
7
|
+
const TERMINAL_RUN_STATUSES = new Set(['completed', 'error', 'stopped', 'interrupted'])
|
|
8
|
+
const EVENT_FLUSH_DELAY_MS = 180
|
|
9
|
+
|
|
10
|
+
const pendingRunEventsByRunId = new Map()
|
|
11
|
+
let pendingRunEventsFlushTimer = null
|
|
12
|
+
|
|
13
|
+
function parseEventPayload(rawValue = '{}') {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(rawValue || '{}')
|
|
16
|
+
} catch {
|
|
17
|
+
return {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toCodexRunEvent(row) {
|
|
22
|
+
return {
|
|
23
|
+
id: Number(row.id),
|
|
24
|
+
seq: Number(row.seq),
|
|
25
|
+
eventType: String(row.event_type || '').trim() || 'event',
|
|
26
|
+
payload: parseEventPayload(row.payload_json),
|
|
27
|
+
createdAt: row.created_at,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toCodexRun(row, events = []) {
|
|
32
|
+
if (!row) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
id: row.id,
|
|
38
|
+
taskSlug: row.task_slug,
|
|
39
|
+
sessionId: row.session_id,
|
|
40
|
+
prompt: row.prompt || '',
|
|
41
|
+
status: row.status || 'running',
|
|
42
|
+
responseMessage: row.response_message || '',
|
|
43
|
+
errorMessage: row.error_message || '',
|
|
44
|
+
createdAt: row.created_at,
|
|
45
|
+
updatedAt: row.updated_at,
|
|
46
|
+
startedAt: row.started_at || row.created_at,
|
|
47
|
+
finishedAt: row.finished_at || '',
|
|
48
|
+
completed: TERMINAL_RUN_STATUSES.has(String(row.status || '')),
|
|
49
|
+
eventCount: Math.max(0, Number(row.event_count) || 0),
|
|
50
|
+
lastEventSeq: Math.max(0, Number(row.last_event_seq) || 0),
|
|
51
|
+
events,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadEventsForRunIds(runIds = [], afterSeq = 0) {
|
|
56
|
+
if (!runIds.length) {
|
|
57
|
+
return new Map()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const placeholders = runIds.map(() => '?').join(', ')
|
|
61
|
+
const rows = all(
|
|
62
|
+
`SELECT id, run_id, seq, event_type, payload_json, created_at
|
|
63
|
+
FROM codex_run_events
|
|
64
|
+
WHERE run_id IN (${placeholders})
|
|
65
|
+
AND seq > ?
|
|
66
|
+
ORDER BY run_id ASC, seq ASC, id ASC`,
|
|
67
|
+
[...runIds, Math.max(0, Number(afterSeq) || 0)]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const grouped = new Map()
|
|
71
|
+
rows.forEach((row) => {
|
|
72
|
+
const runId = row.run_id
|
|
73
|
+
if (!grouped.has(runId)) {
|
|
74
|
+
grouped.set(runId, [])
|
|
75
|
+
}
|
|
76
|
+
grouped.get(runId).push(toCodexRunEvent(row))
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return grouped
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getTaskRowBySlug(slug) {
|
|
83
|
+
const targetSlug = String(slug || '').trim()
|
|
84
|
+
if (!targetSlug) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return get(
|
|
89
|
+
`SELECT id, slug
|
|
90
|
+
FROM tasks
|
|
91
|
+
WHERE slug = ?`,
|
|
92
|
+
[targetSlug]
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getRunRowById(runId) {
|
|
97
|
+
const targetId = String(runId || '').trim()
|
|
98
|
+
if (!targetId) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return get(
|
|
103
|
+
`SELECT id, task_slug, session_id, prompt, status, response_message, error_message, created_at, updated_at, started_at, finished_at
|
|
104
|
+
FROM codex_runs
|
|
105
|
+
WHERE id = ?`,
|
|
106
|
+
[targetId]
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function clearPendingRunEventsFlushTimer() {
|
|
111
|
+
if (pendingRunEventsFlushTimer) {
|
|
112
|
+
clearTimeout(pendingRunEventsFlushTimer)
|
|
113
|
+
pendingRunEventsFlushTimer = null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function flushPendingRunEvents(runId = '') {
|
|
118
|
+
const normalizedRunId = String(runId || '').trim()
|
|
119
|
+
const targetRunIds = normalizedRunId
|
|
120
|
+
? [normalizedRunId]
|
|
121
|
+
: [...pendingRunEventsByRunId.keys()]
|
|
122
|
+
|
|
123
|
+
const flushableRunIds = targetRunIds.filter((item) => {
|
|
124
|
+
const events = pendingRunEventsByRunId.get(item)
|
|
125
|
+
return Array.isArray(events) && events.length > 0
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (!flushableRunIds.length) {
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let insertedCount = 0
|
|
133
|
+
transaction(() => {
|
|
134
|
+
flushableRunIds.forEach((targetRunId) => {
|
|
135
|
+
const events = pendingRunEventsByRunId.get(targetRunId) || []
|
|
136
|
+
if (!events.length) {
|
|
137
|
+
pendingRunEventsByRunId.delete(targetRunId)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
events.forEach((event) => {
|
|
142
|
+
run(
|
|
143
|
+
`INSERT INTO codex_run_events (run_id, seq, event_type, payload_json, created_at)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
145
|
+
[targetRunId, event.seq, event.eventType, event.payloadJson, event.createdAt]
|
|
146
|
+
)
|
|
147
|
+
insertedCount += 1
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
pendingRunEventsByRunId.delete(targetRunId)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (!pendingRunEventsByRunId.size) {
|
|
155
|
+
clearPendingRunEventsFlushTimer()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return insertedCount
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function schedulePendingRunEventsFlush() {
|
|
162
|
+
if (pendingRunEventsFlushTimer) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pendingRunEventsFlushTimer = setTimeout(() => {
|
|
167
|
+
pendingRunEventsFlushTimer = null
|
|
168
|
+
flushPendingRunEvents()
|
|
169
|
+
}, EVENT_FLUSH_DELAY_MS)
|
|
170
|
+
pendingRunEventsFlushTimer.unref?.()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isTerminalRunStatus(status = '') {
|
|
174
|
+
return TERMINAL_RUN_STATUSES.has(String(status || '').trim())
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getCodexRunById(runId, options = {}) {
|
|
178
|
+
flushPendingRunEvents(runId)
|
|
179
|
+
const row = getRunRowById(runId)
|
|
180
|
+
if (!row) {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!options.withEvents) {
|
|
185
|
+
return toCodexRun(row)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const events = loadEventsForRunIds([row.id]).get(row.id) || []
|
|
189
|
+
return toCodexRun(row, events)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function listTaskCodexRuns(taskSlug, limit = 20) {
|
|
193
|
+
return listTaskCodexRunsWithOptions(taskSlug, { limit })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function listTaskCodexRunsWithOptions(taskSlug, options = {}) {
|
|
197
|
+
const task = getTaskRowBySlug(taskSlug)
|
|
198
|
+
if (!task) {
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
flushPendingRunEvents()
|
|
203
|
+
const includeEvents = Boolean(options.includeEvents)
|
|
204
|
+
const limit = Math.max(1, Number(options.limit) || 20)
|
|
205
|
+
const rows = all(
|
|
206
|
+
`SELECT
|
|
207
|
+
runs.id,
|
|
208
|
+
runs.task_slug,
|
|
209
|
+
runs.session_id,
|
|
210
|
+
runs.prompt,
|
|
211
|
+
runs.status,
|
|
212
|
+
runs.response_message,
|
|
213
|
+
runs.error_message,
|
|
214
|
+
runs.created_at,
|
|
215
|
+
runs.updated_at,
|
|
216
|
+
runs.started_at,
|
|
217
|
+
runs.finished_at,
|
|
218
|
+
COUNT(events.id) AS event_count,
|
|
219
|
+
MAX(events.seq) AS last_event_seq
|
|
220
|
+
FROM codex_runs
|
|
221
|
+
AS runs
|
|
222
|
+
LEFT JOIN codex_run_events AS events
|
|
223
|
+
ON events.run_id = runs.id
|
|
224
|
+
WHERE runs.task_slug = ?
|
|
225
|
+
GROUP BY
|
|
226
|
+
runs.id,
|
|
227
|
+
runs.task_slug,
|
|
228
|
+
runs.session_id,
|
|
229
|
+
runs.prompt,
|
|
230
|
+
runs.status,
|
|
231
|
+
runs.response_message,
|
|
232
|
+
runs.error_message,
|
|
233
|
+
runs.created_at,
|
|
234
|
+
runs.updated_at,
|
|
235
|
+
runs.started_at,
|
|
236
|
+
runs.finished_at
|
|
237
|
+
ORDER BY runs.created_at DESC
|
|
238
|
+
LIMIT ?`,
|
|
239
|
+
[task.slug, limit]
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const eventsByRunId = includeEvents
|
|
243
|
+
? loadEventsForRunIds(rows.map((row) => row.id))
|
|
244
|
+
: new Map()
|
|
245
|
+
return rows.map((row) => toCodexRun(row, eventsByRunId.get(row.id) || []))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function listCodexRunEvents(runId, options = {}) {
|
|
249
|
+
flushPendingRunEvents(runId)
|
|
250
|
+
const targetRun = getRunRowById(runId)
|
|
251
|
+
if (!targetRun) {
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const afterSeq = Math.max(0, Number(options.afterSeq) || 0)
|
|
256
|
+
const limit = Math.max(1, Number(options.limit) || 500)
|
|
257
|
+
const rows = all(
|
|
258
|
+
`SELECT id, run_id, seq, event_type, payload_json, created_at
|
|
259
|
+
FROM codex_run_events
|
|
260
|
+
WHERE run_id = ?
|
|
261
|
+
AND seq > ?
|
|
262
|
+
ORDER BY seq ASC, id ASC
|
|
263
|
+
LIMIT ?`,
|
|
264
|
+
[targetRun.id, afterSeq, limit]
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return rows.map(toCodexRunEvent)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createCodexRun(input = {}) {
|
|
271
|
+
const taskSlug = String(input.taskSlug || '').trim()
|
|
272
|
+
const sessionId = String(input.sessionId || '').trim()
|
|
273
|
+
const prompt = String(input.prompt || '').trim()
|
|
274
|
+
|
|
275
|
+
if (!taskSlug) {
|
|
276
|
+
throw new Error('缺少任务。')
|
|
277
|
+
}
|
|
278
|
+
if (!sessionId) {
|
|
279
|
+
throw new Error('请先选择 PromptX 项目。')
|
|
280
|
+
}
|
|
281
|
+
if (!prompt) {
|
|
282
|
+
throw new Error('没有可发送的提示词。')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const task = getTaskBySlug(taskSlug)
|
|
286
|
+
if (!task || task.expired) {
|
|
287
|
+
throw new Error('任务不存在。')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const session = getPromptxCodexSessionById(sessionId)
|
|
291
|
+
if (!session) {
|
|
292
|
+
throw new Error('没有找到对应的 PromptX 项目。')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const now = new Date().toISOString()
|
|
296
|
+
const runId = `pxcr_${nanoid(12)}`
|
|
297
|
+
|
|
298
|
+
transaction(() => {
|
|
299
|
+
run(
|
|
300
|
+
`INSERT INTO codex_runs (
|
|
301
|
+
id, task_slug, session_id, prompt, status,
|
|
302
|
+
response_message, error_message, created_at, updated_at, started_at, finished_at
|
|
303
|
+
)
|
|
304
|
+
VALUES (?, ?, ?, ?, 'running', '', '', ?, ?, ?, NULL)`,
|
|
305
|
+
[runId, task.slug, session.id, prompt, now, now, now]
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
updateTaskCodexSession(task.slug, session.id)
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
captureTaskGitBaseline(task.slug, session.cwd)
|
|
313
|
+
captureRunGitBaseline(runId, session.cwd)
|
|
314
|
+
} catch {
|
|
315
|
+
// Ignore diff baseline failures so they do not block the Codex run itself.
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return getCodexRunById(runId, { withEvents: true })
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function captureTerminalRunSnapshot(runRecord) {
|
|
322
|
+
const sessionId = String(runRecord?.session_id || runRecord?.sessionId || '').trim()
|
|
323
|
+
if (!sessionId) {
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const session = getPromptxCodexSessionById(sessionId)
|
|
328
|
+
if (!session?.cwd) {
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
return captureRunGitFinalSnapshot(runRecord.id, session.cwd)
|
|
334
|
+
} catch {
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function appendCodexRunEvent(runId, payloadOrSeq = {}, maybeSeqOrPayload = 1) {
|
|
340
|
+
const targetRun = getRunRowById(runId)
|
|
341
|
+
if (!targetRun) {
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const seqFirst = typeof payloadOrSeq === 'number'
|
|
346
|
+
const seq = Math.max(1, Number(seqFirst ? payloadOrSeq : maybeSeqOrPayload) || 1)
|
|
347
|
+
const rawPayload = seqFirst ? maybeSeqOrPayload : payloadOrSeq
|
|
348
|
+
const normalizedPayload = rawPayload && typeof rawPayload === 'object'
|
|
349
|
+
? rawPayload
|
|
350
|
+
: { type: 'info', message: String(rawPayload || '') }
|
|
351
|
+
const now = new Date().toISOString()
|
|
352
|
+
const eventType = String(normalizedPayload.type || '').trim() || 'event'
|
|
353
|
+
const payloadJson = JSON.stringify(normalizedPayload)
|
|
354
|
+
|
|
355
|
+
const pendingEvents = pendingRunEventsByRunId.get(targetRun.id) || []
|
|
356
|
+
pendingEvents.push({
|
|
357
|
+
seq,
|
|
358
|
+
eventType,
|
|
359
|
+
payloadJson,
|
|
360
|
+
createdAt: now,
|
|
361
|
+
})
|
|
362
|
+
pendingRunEventsByRunId.set(targetRun.id, pendingEvents)
|
|
363
|
+
schedulePendingRunEventsFlush()
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
id: 0,
|
|
367
|
+
seq,
|
|
368
|
+
eventType,
|
|
369
|
+
payload: normalizedPayload,
|
|
370
|
+
createdAt: now,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function updateCodexRun(runId, patch = {}) {
|
|
375
|
+
const existing = getRunRowById(runId)
|
|
376
|
+
if (!existing) {
|
|
377
|
+
return null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const status = String(patch.status || existing.status || 'running').trim() || 'running'
|
|
381
|
+
const responseMessage = Object.prototype.hasOwnProperty.call(patch, 'responseMessage')
|
|
382
|
+
? String(patch.responseMessage || '')
|
|
383
|
+
: String(existing.response_message || '')
|
|
384
|
+
const errorMessage = Object.prototype.hasOwnProperty.call(patch, 'errorMessage')
|
|
385
|
+
? String(patch.errorMessage || '')
|
|
386
|
+
: String(existing.error_message || '')
|
|
387
|
+
const finishedAt = Object.prototype.hasOwnProperty.call(patch, 'finishedAt')
|
|
388
|
+
? String(patch.finishedAt || '')
|
|
389
|
+
: String(existing.finished_at || '')
|
|
390
|
+
const updatedAt = patch.updatedAt || new Date().toISOString()
|
|
391
|
+
|
|
392
|
+
if (isTerminalRunStatus(status) || finishedAt) {
|
|
393
|
+
flushPendingRunEvents(existing.id)
|
|
394
|
+
captureTerminalRunSnapshot(existing)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
transaction(() => {
|
|
398
|
+
run(
|
|
399
|
+
`UPDATE codex_runs
|
|
400
|
+
SET status = ?, response_message = ?, error_message = ?, finished_at = ?, updated_at = ?
|
|
401
|
+
WHERE id = ?`,
|
|
402
|
+
[status, responseMessage, errorMessage, finishedAt || null, updatedAt, existing.id]
|
|
403
|
+
)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
return getCodexRunById(existing.id, { withEvents: true })
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function listRunningCodexSessionIds() {
|
|
410
|
+
return all(
|
|
411
|
+
`SELECT DISTINCT session_id
|
|
412
|
+
FROM codex_runs
|
|
413
|
+
WHERE status = 'running'`
|
|
414
|
+
).map((row) => String(row.session_id || '').trim()).filter(Boolean)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function listRunningCodexTaskSlugs() {
|
|
418
|
+
return all(
|
|
419
|
+
`SELECT DISTINCT task_slug
|
|
420
|
+
FROM codex_runs
|
|
421
|
+
WHERE status = 'running'`
|
|
422
|
+
).map((row) => String(row.task_slug || '').trim()).filter(Boolean)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function getRunningCodexRunBySessionId(sessionId) {
|
|
426
|
+
const targetId = String(sessionId || '').trim()
|
|
427
|
+
if (!targetId) {
|
|
428
|
+
return null
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const row = get(
|
|
432
|
+
`SELECT id, task_slug, session_id, prompt, status, response_message, error_message, created_at, updated_at, started_at, finished_at
|
|
433
|
+
FROM codex_runs
|
|
434
|
+
WHERE session_id = ?
|
|
435
|
+
AND status = 'running'
|
|
436
|
+
ORDER BY created_at DESC
|
|
437
|
+
LIMIT 1`,
|
|
438
|
+
[targetId]
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return toCodexRun(row)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function getRunningCodexRunByTaskSlug(taskSlug) {
|
|
445
|
+
const targetSlug = String(taskSlug || '').trim()
|
|
446
|
+
if (!targetSlug) {
|
|
447
|
+
return null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const row = get(
|
|
451
|
+
`SELECT id, task_slug, session_id, prompt, status, response_message, error_message, created_at, updated_at, started_at, finished_at
|
|
452
|
+
FROM codex_runs
|
|
453
|
+
WHERE task_slug = ?
|
|
454
|
+
AND status = 'running'
|
|
455
|
+
ORDER BY created_at DESC
|
|
456
|
+
LIMIT 1`,
|
|
457
|
+
[targetSlug]
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return toCodexRun(row)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function hasRunningCodexRunsForTask(taskSlug) {
|
|
464
|
+
const task = getTaskRowBySlug(taskSlug)
|
|
465
|
+
if (!task) {
|
|
466
|
+
return false
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return Boolean(
|
|
470
|
+
get(
|
|
471
|
+
`SELECT 1
|
|
472
|
+
FROM codex_runs
|
|
473
|
+
WHERE task_slug = ?
|
|
474
|
+
AND status = 'running'
|
|
475
|
+
LIMIT 1`,
|
|
476
|
+
[task.slug]
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function deleteTaskCodexRuns(taskSlug) {
|
|
482
|
+
const task = getTaskRowBySlug(taskSlug)
|
|
483
|
+
if (!task) {
|
|
484
|
+
return { error: 'not_found' }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
transaction(() => {
|
|
488
|
+
run('DELETE FROM codex_runs WHERE task_slug = ?', [task.slug])
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
return { ok: true }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function markRunningCodexRunsInterrupted(message = '服务已重启,之前的执行已中断。') {
|
|
495
|
+
const runningRuns = all(
|
|
496
|
+
`SELECT id
|
|
497
|
+
FROM codex_runs
|
|
498
|
+
WHERE status = 'running'
|
|
499
|
+
ORDER BY created_at ASC`
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
runningRuns.forEach((row) => {
|
|
503
|
+
const existingEvents = listCodexRunEvents(row.id) || []
|
|
504
|
+
const nextSeq = existingEvents.length
|
|
505
|
+
? Math.max(...existingEvents.map((item) => Number(item.seq) || 0)) + 1
|
|
506
|
+
: 1
|
|
507
|
+
|
|
508
|
+
appendCodexRunEvent(row.id, {
|
|
509
|
+
type: 'interrupted',
|
|
510
|
+
message,
|
|
511
|
+
}, nextSeq)
|
|
512
|
+
|
|
513
|
+
updateCodexRun(row.id, {
|
|
514
|
+
status: 'interrupted',
|
|
515
|
+
errorMessage: message,
|
|
516
|
+
finishedAt: new Date().toISOString(),
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
return runningRuns.length
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function markInterruptedCodexRuns(message) {
|
|
524
|
+
return markRunningCodexRunsInterrupted(message)
|
|
525
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { nanoid } from 'nanoid'
|
|
4
|
+
import { all, get, run, transaction } from './db.js'
|
|
5
|
+
|
|
6
|
+
function createHttpError(message, statusCode = 400) {
|
|
7
|
+
const error = new Error(message)
|
|
8
|
+
error.statusCode = statusCode
|
|
9
|
+
return error
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toCodexSession(row) {
|
|
13
|
+
if (!row) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
title: row.title,
|
|
20
|
+
cwd: row.cwd,
|
|
21
|
+
codexThreadId: row.codex_thread_id || '',
|
|
22
|
+
running: false,
|
|
23
|
+
createdAt: row.created_at,
|
|
24
|
+
updatedAt: row.updated_at,
|
|
25
|
+
started: Boolean(row.codex_thread_id),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeTitle(input = '', cwd = '') {
|
|
30
|
+
const title = String(input || '').trim().slice(0, 140)
|
|
31
|
+
if (title) {
|
|
32
|
+
return title
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const baseName = path.basename(String(cwd || '').trim())
|
|
36
|
+
return baseName || 'PromptX 项目'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeCwd(input = '') {
|
|
40
|
+
const cwd = String(input || '').trim()
|
|
41
|
+
if (!cwd) {
|
|
42
|
+
throw createHttpError('请先填写工作目录。')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resolved = path.resolve(cwd)
|
|
46
|
+
if (!fs.existsSync(resolved)) {
|
|
47
|
+
throw createHttpError('工作目录不存在,请重新确认。')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stats = fs.statSync(resolved)
|
|
51
|
+
if (!stats.isDirectory()) {
|
|
52
|
+
throw createHttpError('工作目录必须是文件夹。')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return resolved
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listPromptxCodexSessions(limit = 30) {
|
|
59
|
+
const rows = all(
|
|
60
|
+
`SELECT id, title, cwd, codex_thread_id, created_at, updated_at
|
|
61
|
+
FROM codex_sessions
|
|
62
|
+
ORDER BY updated_at DESC
|
|
63
|
+
LIMIT ?`,
|
|
64
|
+
[Math.max(1, Number(limit) || 30)]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return rows.map(toCodexSession)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getPromptxCodexSessionById(sessionId) {
|
|
71
|
+
const targetId = String(sessionId || '').trim()
|
|
72
|
+
if (!targetId) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return toCodexSession(
|
|
77
|
+
get(
|
|
78
|
+
`SELECT id, title, cwd, codex_thread_id, created_at, updated_at
|
|
79
|
+
FROM codex_sessions
|
|
80
|
+
WHERE id = ?`,
|
|
81
|
+
[targetId]
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createPromptxCodexSession(input = {}) {
|
|
87
|
+
const cwd = normalizeCwd(input.cwd)
|
|
88
|
+
const title = normalizeTitle(input.title, cwd)
|
|
89
|
+
const now = new Date().toISOString()
|
|
90
|
+
const id = `pxcs_${nanoid(12)}`
|
|
91
|
+
|
|
92
|
+
transaction(() => {
|
|
93
|
+
run(
|
|
94
|
+
`INSERT INTO codex_sessions (id, title, cwd, codex_thread_id, created_at, updated_at)
|
|
95
|
+
VALUES (?, ?, ?, '', ?, ?)`,
|
|
96
|
+
[id, title, cwd, now, now]
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return getPromptxCodexSessionById(id)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function updatePromptxCodexSession(sessionId, patch = {}) {
|
|
104
|
+
const existing = getPromptxCodexSessionById(sessionId)
|
|
105
|
+
if (!existing) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const wantsCwd = Object.prototype.hasOwnProperty.call(patch, 'cwd')
|
|
110
|
+
const nextCwd = wantsCwd
|
|
111
|
+
? normalizeCwd(patch.cwd)
|
|
112
|
+
: existing.cwd
|
|
113
|
+
|
|
114
|
+
if (existing.started && wantsCwd && nextCwd !== existing.cwd) {
|
|
115
|
+
throw createHttpError('已启动的 PromptX 项目不能直接修改工作目录。', 409)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const title = Object.prototype.hasOwnProperty.call(patch, 'title')
|
|
119
|
+
? normalizeTitle(patch.title, nextCwd)
|
|
120
|
+
: existing.title
|
|
121
|
+
const codexThreadId = Object.prototype.hasOwnProperty.call(patch, 'codexThreadId')
|
|
122
|
+
? String(patch.codexThreadId || '').trim()
|
|
123
|
+
: existing.codexThreadId
|
|
124
|
+
const updatedAt = patch.updatedAt || new Date().toISOString()
|
|
125
|
+
|
|
126
|
+
transaction(() => {
|
|
127
|
+
run(
|
|
128
|
+
`UPDATE codex_sessions
|
|
129
|
+
SET title = ?, cwd = ?, codex_thread_id = ?, updated_at = ?
|
|
130
|
+
WHERE id = ?`,
|
|
131
|
+
[title, nextCwd, codexThreadId, updatedAt, existing.id]
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return getPromptxCodexSessionById(existing.id)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function deletePromptxCodexSession(sessionId) {
|
|
139
|
+
const existing = getPromptxCodexSessionById(sessionId)
|
|
140
|
+
if (!existing) {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
transaction(() => {
|
|
145
|
+
run('DELETE FROM codex_sessions WHERE id = ?', [existing.id])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return existing
|
|
149
|
+
}
|