@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,484 @@
|
|
|
1
|
+
import { customAlphabet } from 'nanoid'
|
|
2
|
+
import {
|
|
3
|
+
BLOCK_TYPES,
|
|
4
|
+
buildRawTaskText,
|
|
5
|
+
clampText,
|
|
6
|
+
deriveTitleFromBlocks,
|
|
7
|
+
getExpiryValue,
|
|
8
|
+
normalizeExpiry,
|
|
9
|
+
normalizeVisibility,
|
|
10
|
+
resolveExpiresAt,
|
|
11
|
+
slugifyTitle,
|
|
12
|
+
summarizeTask,
|
|
13
|
+
} from '../../../packages/shared/src/index.js'
|
|
14
|
+
import { all, get, run, transaction } from './db.js'
|
|
15
|
+
|
|
16
|
+
const slugTail = customAlphabet('abcdefghijkmnpqrstuvwxyz23456789', 6)
|
|
17
|
+
const tokenId = customAlphabet('abcdefghijkmnpqrstuvwxyz23456789', 20)
|
|
18
|
+
|
|
19
|
+
function toBlock(row) {
|
|
20
|
+
return {
|
|
21
|
+
id: Number(row.id),
|
|
22
|
+
type: row.type,
|
|
23
|
+
content: row.content,
|
|
24
|
+
sortOrder: Number(row.sort_order),
|
|
25
|
+
meta: JSON.parse(row.meta_json || '{}'),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toTask(row, blocks = [], options = {}) {
|
|
30
|
+
const codexRunCount = Math.max(0, Number(options.codexRunCount) || 0)
|
|
31
|
+
const displayTitle = row.title || row.auto_title || deriveTitleFromBlocks(blocks)
|
|
32
|
+
return {
|
|
33
|
+
id: Number(row.id),
|
|
34
|
+
slug: row.slug,
|
|
35
|
+
title: row.title,
|
|
36
|
+
autoTitle: row.auto_title || '',
|
|
37
|
+
lastPromptPreview: row.last_prompt_preview || '',
|
|
38
|
+
codexSessionId: row.codex_session_id || '',
|
|
39
|
+
displayTitle,
|
|
40
|
+
visibility: row.visibility,
|
|
41
|
+
expiresAt: row.expires_at,
|
|
42
|
+
expiry: getExpiryValue(row.expires_at),
|
|
43
|
+
codexRunCount,
|
|
44
|
+
createdAt: row.created_at,
|
|
45
|
+
updatedAt: row.updated_at,
|
|
46
|
+
blocks,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureSlug(title) {
|
|
51
|
+
const base = slugifyTitle(title)
|
|
52
|
+
let slug = `${base}-${slugTail()}`
|
|
53
|
+
while (get('SELECT 1 FROM tasks WHERE slug = ?', [slug])) {
|
|
54
|
+
slug = `${base}-${slugTail()}`
|
|
55
|
+
}
|
|
56
|
+
return slug
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isExpired(task) {
|
|
60
|
+
return Boolean(task.expiresAt && new Date(task.expiresAt).getTime() <= Date.now())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadBlocks(taskId) {
|
|
64
|
+
return all(
|
|
65
|
+
`SELECT id, type, content, sort_order, meta_json
|
|
66
|
+
FROM blocks
|
|
67
|
+
WHERE task_id = ?
|
|
68
|
+
ORDER BY sort_order ASC, id ASC`,
|
|
69
|
+
[taskId]
|
|
70
|
+
).map(toBlock)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadBlocksForTasks(taskIds = []) {
|
|
74
|
+
if (!taskIds.length) {
|
|
75
|
+
return new Map()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const placeholders = taskIds.map(() => '?').join(', ')
|
|
79
|
+
const rows = all(
|
|
80
|
+
`SELECT task_id, type, content, sort_order, id
|
|
81
|
+
FROM blocks
|
|
82
|
+
WHERE task_id IN (${placeholders})
|
|
83
|
+
ORDER BY task_id ASC, sort_order ASC, id ASC`,
|
|
84
|
+
taskIds
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const grouped = new Map()
|
|
88
|
+
rows.forEach((row) => {
|
|
89
|
+
const taskId = Number(row.task_id)
|
|
90
|
+
if (!grouped.has(taskId)) {
|
|
91
|
+
grouped.set(taskId, [])
|
|
92
|
+
}
|
|
93
|
+
grouped.get(taskId).push({
|
|
94
|
+
type: row.type,
|
|
95
|
+
content: row.content,
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return grouped
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadListMetadata(taskIds = []) {
|
|
103
|
+
if (!taskIds.length) {
|
|
104
|
+
return {
|
|
105
|
+
blockCountByTaskId: new Map(),
|
|
106
|
+
firstTextByTaskId: new Map(),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const placeholders = taskIds.map(() => '?').join(', ')
|
|
111
|
+
const countRows = all(
|
|
112
|
+
`SELECT task_id, COUNT(*) AS block_count
|
|
113
|
+
FROM blocks
|
|
114
|
+
WHERE task_id IN (${placeholders})
|
|
115
|
+
GROUP BY task_id`,
|
|
116
|
+
taskIds
|
|
117
|
+
)
|
|
118
|
+
const firstTextRows = all(
|
|
119
|
+
`SELECT task_id, content
|
|
120
|
+
FROM (
|
|
121
|
+
SELECT
|
|
122
|
+
task_id,
|
|
123
|
+
content,
|
|
124
|
+
ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY sort_order ASC, id ASC) AS row_num
|
|
125
|
+
FROM blocks
|
|
126
|
+
WHERE task_id IN (${placeholders})
|
|
127
|
+
AND type IN (?, ?)
|
|
128
|
+
AND TRIM(content) != ''
|
|
129
|
+
) ranked
|
|
130
|
+
WHERE row_num = 1`,
|
|
131
|
+
[...taskIds, BLOCK_TYPES.TEXT, BLOCK_TYPES.IMPORTED_TEXT]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
blockCountByTaskId: new Map(
|
|
136
|
+
countRows.map((row) => [Number(row.task_id), Number(row.block_count)])
|
|
137
|
+
),
|
|
138
|
+
firstTextByTaskId: new Map(
|
|
139
|
+
firstTextRows.map((row) => [Number(row.task_id), row.content || ''])
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getCodexRunCountByTaskSlug(taskSlug = '') {
|
|
145
|
+
return Math.max(
|
|
146
|
+
0,
|
|
147
|
+
Number(
|
|
148
|
+
get(
|
|
149
|
+
`SELECT COUNT(*) AS count
|
|
150
|
+
FROM codex_runs
|
|
151
|
+
WHERE task_slug = ?`,
|
|
152
|
+
[String(taskSlug || '').trim()]
|
|
153
|
+
)?.count || 0
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function loadCodexRunCounts(taskSlugs = []) {
|
|
159
|
+
const normalizedSlugs = [...new Set(taskSlugs.map((slug) => String(slug || '').trim()).filter(Boolean))]
|
|
160
|
+
if (!normalizedSlugs.length) {
|
|
161
|
+
return new Map()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const placeholders = normalizedSlugs.map(() => '?').join(', ')
|
|
165
|
+
const rows = all(
|
|
166
|
+
`SELECT task_slug, COUNT(*) AS count
|
|
167
|
+
FROM codex_runs
|
|
168
|
+
WHERE task_slug IN (${placeholders})
|
|
169
|
+
GROUP BY task_slug`,
|
|
170
|
+
normalizedSlugs
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return new Map(
|
|
174
|
+
rows.map((row) => [String(row.task_slug || '').trim(), Math.max(0, Number(row.count) || 0)])
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function collectImagePaths(blocks = []) {
|
|
179
|
+
return blocks
|
|
180
|
+
.filter((block) => block.type === BLOCK_TYPES.IMAGE && block.content)
|
|
181
|
+
.map((block) => block.content)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function mapTaskSummary(row, firstText = '', blockCount = 0, codexRunCount = 0) {
|
|
185
|
+
const textBlock = firstText
|
|
186
|
+
? [{ type: BLOCK_TYPES.TEXT, content: firstText }]
|
|
187
|
+
: []
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
slug: row.slug,
|
|
191
|
+
title: row.title || '',
|
|
192
|
+
autoTitle: row.auto_title || deriveTitleFromBlocks(textBlock) || '',
|
|
193
|
+
lastPromptPreview: row.last_prompt_preview || '',
|
|
194
|
+
codexSessionId: row.codex_session_id || '',
|
|
195
|
+
createdAt: row.created_at,
|
|
196
|
+
updatedAt: row.updated_at,
|
|
197
|
+
visibility: row.visibility,
|
|
198
|
+
expiresAt: row.expires_at,
|
|
199
|
+
preview: summarizeTask({ blocks: textBlock }),
|
|
200
|
+
codexRunCount: Math.max(0, Number(codexRunCount) || 0),
|
|
201
|
+
blockCount,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeBlockInput(block = {}) {
|
|
206
|
+
const type =
|
|
207
|
+
block.type === BLOCK_TYPES.IMAGE
|
|
208
|
+
? BLOCK_TYPES.IMAGE
|
|
209
|
+
: block.type === BLOCK_TYPES.IMPORTED_TEXT
|
|
210
|
+
? BLOCK_TYPES.IMPORTED_TEXT
|
|
211
|
+
: BLOCK_TYPES.TEXT
|
|
212
|
+
const content = clampText(
|
|
213
|
+
block.content || '',
|
|
214
|
+
type === BLOCK_TYPES.IMAGE ? 1000 : 50000
|
|
215
|
+
)
|
|
216
|
+
const meta =
|
|
217
|
+
type === BLOCK_TYPES.IMPORTED_TEXT
|
|
218
|
+
? {
|
|
219
|
+
fileName: clampText(block.meta?.fileName || '', 180),
|
|
220
|
+
collapsed: Boolean(block.meta?.collapsed),
|
|
221
|
+
}
|
|
222
|
+
: {}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
id: Number.isInteger(Number(block.id)) ? Number(block.id) : null,
|
|
226
|
+
type,
|
|
227
|
+
content,
|
|
228
|
+
meta,
|
|
229
|
+
metaJson: JSON.stringify(meta),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function listTasks(limit = 30) {
|
|
234
|
+
const rows = all(
|
|
235
|
+
`SELECT id, slug, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at
|
|
236
|
+
FROM tasks
|
|
237
|
+
ORDER BY updated_at DESC
|
|
238
|
+
LIMIT ?`,
|
|
239
|
+
[Math.max(1, Number(limit) || 30)]
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const taskIds = rows.map((row) => Number(row.id))
|
|
243
|
+
const {
|
|
244
|
+
blockCountByTaskId,
|
|
245
|
+
firstTextByTaskId,
|
|
246
|
+
} = loadListMetadata(taskIds)
|
|
247
|
+
const codexRunCountBySlug = loadCodexRunCounts(rows.map((row) => row.slug))
|
|
248
|
+
|
|
249
|
+
return rows.map((row) =>
|
|
250
|
+
mapTaskSummary(
|
|
251
|
+
row,
|
|
252
|
+
firstTextByTaskId.get(Number(row.id)) || '',
|
|
253
|
+
blockCountByTaskId.get(Number(row.id)) || 0,
|
|
254
|
+
codexRunCountBySlug.get(String(row.slug || '').trim()) || 0
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function getTaskBySlug(slug) {
|
|
260
|
+
const row = get(
|
|
261
|
+
`SELECT id, slug, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at
|
|
262
|
+
FROM tasks
|
|
263
|
+
WHERE slug = ?`,
|
|
264
|
+
[slug]
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if (!row) {
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const task = toTask(row, loadBlocks(row.id), {
|
|
272
|
+
codexRunCount: getCodexRunCountByTaskSlug(row.slug),
|
|
273
|
+
})
|
|
274
|
+
return isExpired(task) ? { ...task, expired: true } : task
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function createTask(input = {}) {
|
|
278
|
+
const now = new Date().toISOString()
|
|
279
|
+
const title = clampText(input.title || '', 140)
|
|
280
|
+
const autoTitle = clampText(input.autoTitle || '', 140)
|
|
281
|
+
const lastPromptPreview = clampText(input.lastPromptPreview || '', 280)
|
|
282
|
+
const codexSessionId = clampText(input.codexSessionId || '', 120)
|
|
283
|
+
const visibility = normalizeVisibility(input.visibility)
|
|
284
|
+
const expiresAt = resolveExpiresAt(normalizeExpiry(input.expiry || 'none'))
|
|
285
|
+
const slug = ensureSlug(title)
|
|
286
|
+
const editToken = tokenId()
|
|
287
|
+
|
|
288
|
+
transaction(() => {
|
|
289
|
+
run(
|
|
290
|
+
`INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
|
|
291
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
292
|
+
[slug, editToken, title, autoTitle, lastPromptPreview, codexSessionId, visibility, expiresAt, now, now]
|
|
293
|
+
)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
...getTaskBySlug(slug),
|
|
298
|
+
editToken,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function updateTask(slug, input = {}) {
|
|
303
|
+
const existing = get(
|
|
304
|
+
`SELECT id, edit_token, codex_session_id
|
|
305
|
+
FROM tasks
|
|
306
|
+
WHERE slug = ?`,
|
|
307
|
+
[slug]
|
|
308
|
+
)
|
|
309
|
+
if (!existing) {
|
|
310
|
+
return { error: 'not_found' }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const title = clampText(input.title || '', 140)
|
|
314
|
+
const autoTitle = clampText(input.autoTitle || '', 140)
|
|
315
|
+
const lastPromptPreview = clampText(input.lastPromptPreview || '', 280)
|
|
316
|
+
const codexSessionId = Object.prototype.hasOwnProperty.call(input, 'codexSessionId')
|
|
317
|
+
? clampText(input.codexSessionId || '', 120)
|
|
318
|
+
: String(existing.codex_session_id || '')
|
|
319
|
+
const visibility = normalizeVisibility(input.visibility)
|
|
320
|
+
const expiresAt = resolveExpiresAt(normalizeExpiry(input.expiry || 'none'))
|
|
321
|
+
const updatedAt = new Date().toISOString()
|
|
322
|
+
const blocks = Array.isArray(input.blocks) ? input.blocks.map(normalizeBlockInput) : []
|
|
323
|
+
const currentBlocks = loadBlocks(existing.id)
|
|
324
|
+
const currentBlockMap = new Map(currentBlocks.map((block) => [block.id, block]))
|
|
325
|
+
|
|
326
|
+
transaction(() => {
|
|
327
|
+
run(
|
|
328
|
+
`UPDATE tasks
|
|
329
|
+
SET title = ?, auto_title = ?, last_prompt_preview = ?, codex_session_id = ?, visibility = ?, expires_at = ?, updated_at = ?
|
|
330
|
+
WHERE slug = ?`,
|
|
331
|
+
[title, autoTitle, lastPromptPreview, codexSessionId, visibility, expiresAt, updatedAt, slug]
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
const incomingIds = new Set()
|
|
335
|
+
|
|
336
|
+
blocks.forEach((block, index) => {
|
|
337
|
+
const currentBlock = block.id ? currentBlockMap.get(block.id) : null
|
|
338
|
+
|
|
339
|
+
if (currentBlock) {
|
|
340
|
+
incomingIds.add(currentBlock.id)
|
|
341
|
+
|
|
342
|
+
const currentMetaJson = JSON.stringify(currentBlock.meta || {})
|
|
343
|
+
const unchanged =
|
|
344
|
+
currentBlock.type === block.type
|
|
345
|
+
&& currentBlock.content === block.content
|
|
346
|
+
&& currentBlock.sortOrder === index
|
|
347
|
+
&& currentMetaJson === block.metaJson
|
|
348
|
+
|
|
349
|
+
if (!unchanged) {
|
|
350
|
+
run(
|
|
351
|
+
`UPDATE blocks
|
|
352
|
+
SET type = ?, content = ?, sort_order = ?, meta_json = ?
|
|
353
|
+
WHERE id = ? AND task_id = ?`,
|
|
354
|
+
[block.type, block.content, index, block.metaJson, currentBlock.id, existing.id]
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
run(
|
|
361
|
+
`INSERT INTO blocks (task_id, type, content, sort_order, meta_json, created_at)
|
|
362
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
363
|
+
[existing.id, block.type, block.content, index, block.metaJson, updatedAt]
|
|
364
|
+
)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
currentBlocks.forEach((block) => {
|
|
368
|
+
if (!incomingIds.has(block.id)) {
|
|
369
|
+
run('DELETE FROM blocks WHERE id = ? AND task_id = ?', [block.id, existing.id])
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
return getTaskBySlug(slug)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function deleteTask(slug) {
|
|
378
|
+
const row = get('SELECT id, edit_token FROM tasks WHERE slug = ?', [slug])
|
|
379
|
+
if (!row) {
|
|
380
|
+
return { error: 'not_found' }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const blocks = loadBlocks(row.id)
|
|
384
|
+
const removedAssets = collectImagePaths(blocks)
|
|
385
|
+
|
|
386
|
+
transaction(() => {
|
|
387
|
+
run('DELETE FROM tasks WHERE slug = ?', [slug])
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return { ok: true, removedAssets }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function purgeExpiredTasks(now = new Date().toISOString()) {
|
|
394
|
+
const rows = all(
|
|
395
|
+
`SELECT id
|
|
396
|
+
FROM tasks
|
|
397
|
+
WHERE expires_at IS NOT NULL
|
|
398
|
+
AND expires_at <= ?`,
|
|
399
|
+
[now]
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if (!rows.length) {
|
|
403
|
+
return { removedAssets: [], removedCount: 0 }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const taskIds = rows.map((row) => Number(row.id))
|
|
407
|
+
const blocksByTaskId = loadBlocksForTasks(taskIds)
|
|
408
|
+
const removedAssets = taskIds.flatMap((taskId) =>
|
|
409
|
+
collectImagePaths(blocksByTaskId.get(taskId) || [])
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
const placeholders = taskIds.map(() => '?').join(', ')
|
|
413
|
+
transaction(() => {
|
|
414
|
+
run(`DELETE FROM tasks WHERE id IN (${placeholders})`, taskIds)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
removedAssets,
|
|
419
|
+
removedCount: taskIds.length,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function buildTaskExports(task) {
|
|
424
|
+
return {
|
|
425
|
+
raw: buildRawTaskText(task),
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function canEditTask(slug) {
|
|
430
|
+
return Boolean(get('SELECT 1 FROM tasks WHERE slug = ?', [slug]))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function updateTaskCodexSession(slug, codexSessionId = '') {
|
|
434
|
+
const existing = get('SELECT slug FROM tasks WHERE slug = ?', [slug])
|
|
435
|
+
if (!existing) {
|
|
436
|
+
return null
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const normalizedSessionId = String(codexSessionId || '').trim()
|
|
440
|
+
const updatedAt = new Date().toISOString()
|
|
441
|
+
|
|
442
|
+
transaction(() => {
|
|
443
|
+
run(
|
|
444
|
+
`UPDATE tasks
|
|
445
|
+
SET codex_session_id = ?, updated_at = ?
|
|
446
|
+
WHERE slug = ?`,
|
|
447
|
+
[normalizedSessionId, updatedAt, slug]
|
|
448
|
+
)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
return getTaskBySlug(slug)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function clearTaskCodexSessionReferences(codexSessionId = '') {
|
|
455
|
+
const normalizedSessionId = String(codexSessionId || '').trim()
|
|
456
|
+
if (!normalizedSessionId) {
|
|
457
|
+
return []
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const matchedRows = all(
|
|
461
|
+
`SELECT slug
|
|
462
|
+
FROM tasks
|
|
463
|
+
WHERE codex_session_id = ?`,
|
|
464
|
+
[normalizedSessionId]
|
|
465
|
+
)
|
|
466
|
+
const matchedTaskSlugs = matchedRows
|
|
467
|
+
.map((row) => String(row?.slug || '').trim())
|
|
468
|
+
.filter(Boolean)
|
|
469
|
+
|
|
470
|
+
if (!matchedTaskSlugs.length) {
|
|
471
|
+
return []
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
transaction(() => {
|
|
475
|
+
run(
|
|
476
|
+
`UPDATE tasks
|
|
477
|
+
SET codex_session_id = ''
|
|
478
|
+
WHERE codex_session_id = ?`,
|
|
479
|
+
[normalizedSessionId]
|
|
480
|
+
)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
return matchedTaskSlugs
|
|
484
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function createSseHub(options = {}) {
|
|
2
|
+
const clients = new Set()
|
|
3
|
+
const pingIntervalMs = Math.max(1000, Number(options.pingIntervalMs) || 15000)
|
|
4
|
+
let nextEventId = 0
|
|
5
|
+
|
|
6
|
+
function createSseMessage(payload) {
|
|
7
|
+
return `id: ${++nextEventId}\ndata: ${JSON.stringify(payload)}\n\n`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function write(target, payload) {
|
|
11
|
+
if (!target || target.destroyed || target.writableEnded) {
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
target.write(createSseMessage(payload))
|
|
17
|
+
return true
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function broadcast(type, payload = {}) {
|
|
24
|
+
const message = {
|
|
25
|
+
type: String(type || '').trim(),
|
|
26
|
+
sentAt: new Date().toISOString(),
|
|
27
|
+
...payload,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const client of [...clients]) {
|
|
31
|
+
if (!write(client, message)) {
|
|
32
|
+
clients.delete(client)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function addClient(target) {
|
|
38
|
+
if (!target) {
|
|
39
|
+
return () => {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
clients.add(target)
|
|
43
|
+
return () => {
|
|
44
|
+
clients.delete(target)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pingTimer = setInterval(() => {
|
|
49
|
+
for (const client of [...clients]) {
|
|
50
|
+
if (!client || client.destroyed || client.writableEnded) {
|
|
51
|
+
clients.delete(client)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
client.write(': ping\n\n')
|
|
57
|
+
} catch {
|
|
58
|
+
clients.delete(client)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, pingIntervalMs)
|
|
62
|
+
pingTimer.unref?.()
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
addClient,
|
|
66
|
+
broadcast,
|
|
67
|
+
write,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { nanoid } from 'nanoid'
|
|
3
|
+
|
|
4
|
+
export function normalizeUploadFileName(fileName = '', fallback = 'file') {
|
|
5
|
+
const normalized = String(fileName || '')
|
|
6
|
+
.trim()
|
|
7
|
+
.split(/[\\/]/)
|
|
8
|
+
.pop()
|
|
9
|
+
return normalized || fallback
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getSafeTempExtension(fileName = '', fallback = '') {
|
|
13
|
+
const extension = path.extname(normalizeUploadFileName(fileName)).toLowerCase()
|
|
14
|
+
if (/^\.[a-z0-9]{1,10}$/.test(extension)) {
|
|
15
|
+
return extension
|
|
16
|
+
}
|
|
17
|
+
return fallback
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createTempFilePath(tmpDir, fileName = '', fallbackExt = '') {
|
|
21
|
+
return path.join(tmpDir, `${nanoid(12)}${getSafeTempExtension(fileName, fallbackExt)}`)
|
|
22
|
+
}
|