@muyichengshayu/promptx 0.2.0 → 0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - 新增工作台 `shell` 命令模式:在右侧编辑区输入 `!pwd`、`!git status`、`!pnpm test` 这类纯文本命令时,PromptX 会直接按当前选中的 `Codex / Claude Code / OpenCode` 语义发起本地命令执行,并把结果继续收拢回原有的 `PromptX → Agent / Agent` 对话流,不额外引入一套独立的 Shell 卡片心智。
6
+ - 收紧命令模式的安全边界与展示逻辑:命令意图改为由服务端重新解析,不再信任前端直传命令;shell run 固定绑定 root project session,避免污染 member session 的 thread/identity;默认仅本机可用,Relay 需在设置里显式开启“允许远程执行命令模式”后,且仅对带内部标记的 relay 转发请求放行。
7
+ - 为远程命令模式补齐配置与回归测试:Relay 设置页新增高权限开关与明确警告,诊断信息也会带上该配置;同时新增本地允许、远程默认拒绝、仅 relay 显式开启可用、环境变量覆盖等测试,降低后续回归风险。
8
+ - 修复 runner 大事件上报导致任务卡死的问题:内部 runner events 接口提高了 body limit,避免长过程日志或大批量事件刷新时触发 `Request body is too large`,导致一轮任务停在中间。
9
+ - 改进代码变更审查对 Git submodule 的支持:代码变更与 diff review 现在会展开 submodule 内部的真实文件改动,而不再只显示顶层 submodule 占位项;相关任务级、轮次级与子文件明细链路都已补齐测试。
10
+ - 补充开发环境的 `Vite allowedHosts` 配置,方便在代理、局域网或特定本地域名下更稳定地访问工作台开发服务。
11
+
3
12
  ## 0.2.0
4
13
 
5
14
  - 项目正式升级为多 Agent 协作模型:一个项目可同时绑定 `Codex / Claude Code / OpenCode`,右侧可直接切换本轮发送目标,中栏也支持按 Agent 过滤整轮消息流,适合同一任务里拆分“方案 / 执行 / 复核”等分工。
@@ -7,22 +7,32 @@ import {
7
7
  import { codexRunner } from './codexRunner.js'
8
8
  import { claudeCodeRunner } from './claudeCodeRunner.js'
9
9
  import { openCodeRunner } from './openCodeRunner.js'
10
+ import { shellRunner } from './shellRunner.js'
10
11
 
11
12
  const runnerRegistry = new Map([
12
13
  [codexRunner.engine, codexRunner],
13
14
  [claudeCodeRunner.engine, claudeCodeRunner],
14
15
  [openCodeRunner.engine, openCodeRunner],
16
+ [shellRunner.engine, shellRunner],
15
17
  ])
16
18
 
19
+ function normalizeRunnerEngine(engine = AGENT_ENGINES.CODEX) {
20
+ const normalized = String(engine || '').trim().toLowerCase()
21
+ if (normalized === shellRunner.engine) {
22
+ return shellRunner.engine
23
+ }
24
+ return normalizeAgentEngine(normalized)
25
+ }
26
+
17
27
  export function getAgentRunner(engine = AGENT_ENGINES.CODEX) {
18
- return runnerRegistry.get(normalizeAgentEngine(engine)) || null
28
+ return runnerRegistry.get(normalizeRunnerEngine(engine)) || null
19
29
  }
20
30
 
21
31
  export function assertAgentRunner(engine = AGENT_ENGINES.CODEX) {
22
- const normalized = normalizeAgentEngine(engine)
32
+ const normalized = normalizeRunnerEngine(engine)
23
33
  const runner = getAgentRunner(normalized)
24
34
  if (!runner) {
25
- throw new Error(`当前还不支持执行引擎:${getAgentEngineLabel(normalized)}`)
35
+ throw new Error(`当前还不支持执行引擎:${normalized === shellRunner.engine ? shellRunner.label : getAgentEngineLabel(normalized)}`)
26
36
  }
27
37
  return runner
28
38
  }
@@ -0,0 +1,216 @@
1
+ import { spawn } from 'node:child_process'
2
+ import {
3
+ createAgentEventEnvelopeEvent,
4
+ createCompletedEnvelopeEvent,
5
+ createStderrEnvelopeEvent,
6
+ createStdoutEnvelopeEvent,
7
+ } from '../../../../packages/shared/src/agentRunEnvelopeEvents.js'
8
+ import {
9
+ AGENT_RUN_EVENT_TYPES,
10
+ AGENT_RUN_ITEM_TYPES,
11
+ createItemCompletedEvent,
12
+ createItemStartedEvent,
13
+ } from '../../../../packages/shared/src/agentRunEvents.js'
14
+ import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
15
+
16
+ const SHELL_ENGINE = 'shell'
17
+ const MAX_SHELL_OUTPUT_TAIL_LENGTH = Math.max(
18
+ 16 * 1024,
19
+ Number(process.env.PROMPTX_SHELL_OUTPUT_TAIL_LENGTH) || 128 * 1024
20
+ )
21
+
22
+ function appendOutputTail(current = '', chunk = '', maxLength = MAX_SHELL_OUTPUT_TAIL_LENGTH) {
23
+ const next = `${String(current || '')}${String(chunk || '')}`
24
+ if (next.length <= maxLength) {
25
+ return next
26
+ }
27
+ return next.slice(next.length - maxLength)
28
+ }
29
+
30
+ function splitBufferedLines(buffer = '') {
31
+ const normalized = String(buffer || '')
32
+ if (!normalized) {
33
+ return { lines: [], rest: '' }
34
+ }
35
+
36
+ const parts = normalized.split(/\r?\n/g)
37
+ const rest = /(?:\r?\n)$/.test(normalized) ? '' : parts.pop() || ''
38
+ return {
39
+ lines: parts.filter(Boolean),
40
+ rest,
41
+ }
42
+ }
43
+
44
+ function getShellCommand(command = '') {
45
+ const raw = String(command || '').trim()
46
+ if (!raw) {
47
+ return {
48
+ executable: '',
49
+ args: [],
50
+ displayCommand: '',
51
+ }
52
+ }
53
+
54
+ if (process.platform === 'win32') {
55
+ const executable = process.env.ComSpec || 'cmd.exe'
56
+ return {
57
+ executable,
58
+ args: ['/d', '/s', '/c', raw],
59
+ displayCommand: `${executable} /d /s /c ${raw}`,
60
+ }
61
+ }
62
+
63
+ const executable = process.env.SHELL || '/bin/zsh'
64
+ return {
65
+ executable,
66
+ args: ['-lc', raw],
67
+ displayCommand: `${executable} -lc ${raw}`,
68
+ }
69
+ }
70
+
71
+ function createCommandItem(command = '', status = 'running', aggregatedOutput = '', exitCode = null) {
72
+ return {
73
+ type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
74
+ command: String(command || '').trim(),
75
+ status: String(status || '').trim() || 'running',
76
+ aggregated_output: String(aggregatedOutput || ''),
77
+ ...(typeof exitCode === 'number' ? { exit_code: exitCode } : {}),
78
+ }
79
+ }
80
+
81
+ export const shellRunner = {
82
+ engine: SHELL_ENGINE,
83
+ label: 'Shell',
84
+ supportsWorkspaceHistory: false,
85
+ streamSessionPrompt(session, prompt, callbacks = {}) {
86
+ const command = String(prompt || '').trim()
87
+ if (!command) {
88
+ throw new Error('缺少要执行的命令。')
89
+ }
90
+
91
+ const cwd = String(session?.cwd || '').trim()
92
+ const { executable, args, displayCommand } = getShellCommand(command)
93
+ if (!executable) {
94
+ throw new Error('当前环境没有可用的 shell。')
95
+ }
96
+
97
+ const onEvent = typeof callbacks.onEvent === 'function' ? callbacks.onEvent : () => {}
98
+ const child = spawn(executable, args, createManagedSpawnOptions({ cwd }))
99
+ let stdoutBuffer = ''
100
+ let stderrBuffer = ''
101
+ let outputTail = ''
102
+ let settled = false
103
+
104
+ onEvent(createAgentEventEnvelopeEvent(createItemStartedEvent(createCommandItem(displayCommand, 'running'))))
105
+
106
+ const result = new Promise((resolve, reject) => {
107
+ const rejectWithOutput = (message, payload = {}) => {
108
+ const error = new Error(String(message || '命令执行失败。'))
109
+ error.output = String(payload.output || outputTail || '').trim()
110
+ error.exitCode = typeof payload.exitCode === 'number' ? payload.exitCode : null
111
+ reject(error)
112
+ }
113
+
114
+ const flushStdout = (buffer, force = false) => {
115
+ const { lines, rest } = splitBufferedLines(buffer)
116
+ lines.forEach((line) => {
117
+ onEvent(createStdoutEnvelopeEvent(line))
118
+ outputTail = appendOutputTail(outputTail, `${line}\n`)
119
+ })
120
+ if (force && rest) {
121
+ onEvent(createStdoutEnvelopeEvent(rest))
122
+ outputTail = appendOutputTail(outputTail, `${rest}\n`)
123
+ return ''
124
+ }
125
+ return rest
126
+ }
127
+
128
+ const flushStderr = (buffer, force = false) => {
129
+ const { lines, rest } = splitBufferedLines(buffer)
130
+ lines.forEach((line) => {
131
+ onEvent(createStderrEnvelopeEvent(line))
132
+ outputTail = appendOutputTail(outputTail, `${line}\n`)
133
+ })
134
+ if (force && rest) {
135
+ onEvent(createStderrEnvelopeEvent(rest))
136
+ outputTail = appendOutputTail(outputTail, `${rest}\n`)
137
+ return ''
138
+ }
139
+ return rest
140
+ }
141
+
142
+ child.stdout?.on('data', (chunk) => {
143
+ stdoutBuffer += chunk.toString()
144
+ stdoutBuffer = flushStdout(stdoutBuffer)
145
+ })
146
+
147
+ child.stderr?.on('data', (chunk) => {
148
+ stderrBuffer += chunk.toString()
149
+ stderrBuffer = flushStderr(stderrBuffer)
150
+ })
151
+
152
+ child.once('error', (error) => {
153
+ if (settled) {
154
+ return
155
+ }
156
+ settled = true
157
+ stdoutBuffer = flushStdout(stdoutBuffer, true)
158
+ stderrBuffer = flushStderr(stderrBuffer, true)
159
+ const finalOutput = String(outputTail || '').trim()
160
+ onEvent(createAgentEventEnvelopeEvent(createItemCompletedEvent(createCommandItem(
161
+ displayCommand,
162
+ 'failed',
163
+ finalOutput,
164
+ 1
165
+ ))))
166
+ rejectWithOutput(error?.message || '命令启动失败。', {
167
+ output: finalOutput,
168
+ exitCode: 1,
169
+ })
170
+ })
171
+
172
+ child.once('close', (code, signal) => {
173
+ if (settled) {
174
+ return
175
+ }
176
+ settled = true
177
+ stdoutBuffer = flushStdout(stdoutBuffer, true)
178
+ stderrBuffer = flushStderr(stderrBuffer, true)
179
+ const exitCode = Number.isInteger(code) ? code : (signal ? 1 : 0)
180
+ const finalOutput = String(outputTail || '').trim()
181
+ const success = exitCode === 0
182
+
183
+ onEvent(createAgentEventEnvelopeEvent(createItemCompletedEvent(createCommandItem(
184
+ displayCommand,
185
+ success ? 'completed' : 'failed',
186
+ finalOutput,
187
+ exitCode
188
+ ))))
189
+
190
+ if (success) {
191
+ const message = finalOutput || `命令执行完成:${command}`
192
+ onEvent(createCompletedEnvelopeEvent(message))
193
+ resolve({
194
+ sessionId: String(session?.id || '').trim(),
195
+ threadId: '',
196
+ message,
197
+ })
198
+ return
199
+ }
200
+
201
+ rejectWithOutput(`命令执行失败(exit ${exitCode})`, {
202
+ output: finalOutput,
203
+ exitCode,
204
+ })
205
+ })
206
+ })
207
+
208
+ return {
209
+ child,
210
+ result,
211
+ cancel(options = {}) {
212
+ forceStopChildProcess(child, options)
213
+ },
214
+ }
215
+ },
216
+ }
@@ -19,6 +19,10 @@ const QUEUED_HEARTBEAT_INTERVAL_MS = Math.max(
19
19
  const DEFAULT_STOP_TIMEOUT_MS = Math.max(1000, Number(process.env.PROMPTX_RUNNER_STOP_TIMEOUT_MS) || 10000)
20
20
  const STOP_TIMEOUT_BUFFER_MS = Math.max(500, Number(process.env.PROMPTX_RUNNER_STOP_TIMEOUT_BUFFER_MS) || 2000)
21
21
  const DEFAULT_MAX_CONCURRENT_RUNS = Math.max(1, Number(process.env.PROMPTX_RUNNER_MAX_CONCURRENT_RUNS) || 3)
22
+ const DEFAULT_EVENT_BATCH_BYTES = Math.max(
23
+ 64 * 1024,
24
+ Number(process.env.PROMPTX_RUNNER_EVENT_BATCH_BYTES) || 512 * 1024
25
+ )
22
26
  const RUNNER_ID = String(process.env.PROMPTX_RUNNER_ID || 'local-runner').trim() || 'local-runner'
23
27
  const DISPOSE_POLL_INTERVAL_MS = Math.max(50, Number(process.env.PROMPTX_RUNNER_DISPOSE_POLL_MS) || 100)
24
28
 
@@ -26,23 +30,67 @@ function nowIso() {
26
30
  return new Date().toISOString()
27
31
  }
28
32
 
33
+ function estimateJsonBytes(value) {
34
+ try {
35
+ return Buffer.byteLength(JSON.stringify(value), 'utf8')
36
+ } catch {
37
+ return Buffer.byteLength(String(value || ''), 'utf8')
38
+ }
39
+ }
40
+
41
+ function splitEventItemsIntoBatches(items = [], metadata = {}, maxBatchBytes = DEFAULT_EVENT_BATCH_BYTES) {
42
+ const normalizedLimit = Math.max(16 * 1024, Number(maxBatchBytes) || DEFAULT_EVENT_BATCH_BYTES)
43
+ const normalizedItems = Array.isArray(items) ? items.filter(Boolean) : []
44
+ if (!normalizedItems.length) {
45
+ return []
46
+ }
47
+
48
+ const metadataBytes = estimateJsonBytes(metadata)
49
+ const batches = []
50
+ let currentBatch = []
51
+ let currentBytes = metadataBytes
52
+
53
+ normalizedItems.forEach((item) => {
54
+ const itemBytes = estimateJsonBytes(item) + 1
55
+ if (currentBatch.length && currentBytes + itemBytes > normalizedLimit) {
56
+ batches.push(currentBatch)
57
+ currentBatch = [item]
58
+ currentBytes = metadataBytes + itemBytes
59
+ return
60
+ }
61
+
62
+ currentBatch.push(item)
63
+ currentBytes += itemBytes
64
+ })
65
+
66
+ if (currentBatch.length) {
67
+ batches.push(currentBatch)
68
+ }
69
+
70
+ return batches
71
+ }
72
+
29
73
  function normalizeMaxConcurrentRuns(value, fallback = DEFAULT_MAX_CONCURRENT_RUNS) {
30
74
  const normalizedFallback = Math.max(1, Number(fallback) || DEFAULT_MAX_CONCURRENT_RUNS)
31
75
  return Math.max(1, Number(value) || normalizedFallback)
32
76
  }
33
77
 
34
78
  function normalizeSession(payload = {}) {
79
+ const engine = String(payload.engine || '').trim() || 'codex'
80
+ const isShellEngine = engine === 'shell'
35
81
  return {
36
82
  id: String(payload.sessionId || payload.id || '').trim(),
37
83
  title: String(payload.sessionTitle || payload.title || '').trim(),
38
- engine: String(payload.engine || '').trim() || 'codex',
84
+ engine,
39
85
  cwd: String(payload.cwd || '').trim(),
40
- codexThreadId: String(payload.codexThreadId || payload.engineThreadId || '').trim(),
41
- engineSessionId: String(payload.engineSessionId || '').trim(),
42
- engineThreadId: String(payload.engineThreadId || payload.codexThreadId || '').trim(),
43
- engineMeta: payload.engineMeta && typeof payload.engineMeta === 'object' ? payload.engineMeta : {},
86
+ codexThreadId: isShellEngine ? '' : String(payload.codexThreadId || payload.engineThreadId || '').trim(),
87
+ engineSessionId: isShellEngine ? '' : String(payload.engineSessionId || '').trim(),
88
+ engineThreadId: isShellEngine ? '' : String(payload.engineThreadId || payload.codexThreadId || '').trim(),
89
+ engineMeta: isShellEngine
90
+ ? {}
91
+ : (payload.engineMeta && typeof payload.engineMeta === 'object' ? payload.engineMeta : {}),
44
92
  running: true,
45
- started: Boolean(String(payload.engineThreadId || payload.codexThreadId || '').trim()),
93
+ started: isShellEngine ? false : Boolean(String(payload.engineThreadId || payload.codexThreadId || '').trim()),
46
94
  createdAt: String(payload.sessionCreatedAt || '').trim(),
47
95
  updatedAt: String(payload.sessionUpdatedAt || '').trim(),
48
96
  }
@@ -286,13 +334,19 @@ export function createRunManager(options = {}) {
286
334
  }
287
335
 
288
336
  const pendingItems = context.eventBuffer.splice(0, context.eventBuffer.length)
337
+ const batches = splitEventItemsIntoBatches(pendingItems, { runnerId: RUNNER_ID })
289
338
  context.flushing = true
290
339
 
340
+ let batchIndex = 0
291
341
  try {
292
- await serverClient.postEvents(pendingItems, { runnerId: RUNNER_ID })
342
+ for (batchIndex = 0; batchIndex < batches.length; batchIndex += 1) {
343
+ const batch = batches[batchIndex]
344
+ await serverClient.postEvents(batch, { runnerId: RUNNER_ID })
345
+ }
293
346
  return pendingItems.length
294
347
  } catch (error) {
295
- context.eventBuffer.unshift(...pendingItems)
348
+ const remainingItems = batches.slice(batchIndex).flat()
349
+ context.eventBuffer.unshift(...remainingItems)
296
350
  context.eventFlushFailureCount = Math.max(0, Number(context.eventFlushFailureCount) || 0) + 1
297
351
  context.lastEventFlushFailureAt = nowIso()
298
352
  context.lastEventFlushFailureMessage = String(error?.message || error || '').trim()
@@ -472,6 +526,11 @@ export function createRunManager(options = {}) {
472
526
  }
473
527
 
474
528
  async function handleStreamError(context, error) {
529
+ const errorOutput = String(error?.output || '').trim()
530
+ const errorMessage = [String(error?.message || '执行引擎运行失败。').trim(), errorOutput]
531
+ .filter(Boolean)
532
+ .join('\n\n')
533
+
475
534
  if (context.stopRequestedAt) {
476
535
  const stopReason = classifyStoppedErrorReason(context)
477
536
  await finalizeRun(context, 'stopped', {
@@ -485,7 +544,7 @@ export function createRunManager(options = {}) {
485
544
  }
486
545
 
487
546
  await finalizeRun(context, 'error', {
488
- errorMessage: error?.message || '执行引擎运行失败。',
547
+ errorMessage,
489
548
  })
490
549
  }
491
550
 
@@ -8,14 +8,29 @@ import { codexRunner } from './codexRunner.js'
8
8
  import { claudeCodeRunner } from './claudeCodeRunner.js'
9
9
  import { openCodeRunner } from './openCodeRunner.js'
10
10
 
11
+ const SHELL_ENGINE = 'shell'
12
+ const shellRunner = {
13
+ engine: SHELL_ENGINE,
14
+ label: 'Shell',
15
+ }
16
+
11
17
  const runnerRegistry = new Map([
12
18
  [codexRunner.engine, codexRunner],
13
19
  [claudeCodeRunner.engine, claudeCodeRunner],
14
20
  [openCodeRunner.engine, openCodeRunner],
21
+ [shellRunner.engine, shellRunner],
15
22
  ])
16
23
 
24
+ function normalizeRunnerEngine(engine = AGENT_ENGINES.CODEX) {
25
+ const normalized = String(engine || '').trim().toLowerCase()
26
+ if (normalized === SHELL_ENGINE) {
27
+ return SHELL_ENGINE
28
+ }
29
+ return normalizeAgentEngine(normalized)
30
+ }
31
+
17
32
  export function getAgentRunner(engine = AGENT_ENGINES.CODEX) {
18
- return runnerRegistry.get(normalizeAgentEngine(engine)) || null
33
+ return runnerRegistry.get(normalizeRunnerEngine(engine)) || null
19
34
  }
20
35
 
21
36
  export function listAvailableAgentEngines() {
@@ -30,10 +45,10 @@ export function listEnabledAgentEngines() {
30
45
  }
31
46
 
32
47
  export function assertAgentRunner(engine = AGENT_ENGINES.CODEX) {
33
- const normalized = normalizeAgentEngine(engine)
48
+ const normalized = normalizeRunnerEngine(engine)
34
49
  const runner = getAgentRunner(normalized)
35
50
  if (!runner) {
36
- throw new Error(`当前还不支持执行引擎:${getAgentEngineLabel(normalized)}`)
51
+ throw new Error(`当前还不支持执行引擎:${normalized === SHELL_ENGINE ? shellRunner.label : getAgentEngineLabel(normalized)}`)
37
52
  }
38
53
  return runner
39
54
  }
@@ -43,6 +43,15 @@ function parsePromptBlocks(rawValue = '[]') {
43
43
  }
44
44
  }
45
45
 
46
+ function parseRunMeta(rawValue = '{}') {
47
+ try {
48
+ const parsed = JSON.parse(rawValue || '{}')
49
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
50
+ } catch {
51
+ return {}
52
+ }
53
+ }
54
+
46
55
  function trimBoundaryBlankLines(value = '') {
47
56
  const lines = String(value || '').replace(/\r\n/g, '\n').split('\n')
48
57
 
@@ -112,6 +121,7 @@ function toCodexRun(row, events = null) {
112
121
  taskSlug: row.task_slug,
113
122
  sessionId: row.session_id,
114
123
  engine: String(row.engine || '').trim() || 'codex',
124
+ displayEngine: String(parseRunMeta(row.run_meta_json).displayEngine || '').trim(),
115
125
  prompt: row.prompt || '',
116
126
  promptBlocks: parsePromptBlocks(row.prompt_blocks_json),
117
127
  status: row.status || 'running',
@@ -177,7 +187,7 @@ function getRunRowById(runId) {
177
187
  }
178
188
 
179
189
  return get(
180
- `SELECT id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at
190
+ `SELECT id, task_slug, session_id, engine, run_meta_json, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at
181
191
  FROM codex_runs
182
192
  WHERE id = ?`,
183
193
  [targetId]
@@ -291,6 +301,7 @@ export function listTaskCodexRunsWithOptions(taskSlug, options = {}) {
291
301
  runs.task_slug,
292
302
  runs.session_id,
293
303
  runs.engine,
304
+ runs.run_meta_json,
294
305
  runs.prompt,
295
306
  runs.prompt_blocks_json,
296
307
  runs.status,
@@ -312,6 +323,7 @@ export function listTaskCodexRunsWithOptions(taskSlug, options = {}) {
312
323
  runs.task_slug,
313
324
  runs.session_id,
314
325
  runs.engine,
326
+ runs.run_meta_json,
315
327
  runs.prompt,
316
328
  runs.prompt_blocks_json,
317
329
  runs.status,
@@ -367,6 +379,8 @@ export function createCodexRun(input = {}) {
367
379
  const prompt = String(input.prompt || '').trim()
368
380
  const promptBlocks = normalizePromptBlocks(input.promptBlocks)
369
381
  const initialStatus = String(input.status || 'queued').trim() || 'queued'
382
+ const engine = String(input.engine || '').trim()
383
+ const displayEngine = String(input.displayEngine || '').trim()
370
384
 
371
385
  if (!taskSlug) {
372
386
  throw new Error('缺少任务。')
@@ -387,7 +401,8 @@ export function createCodexRun(input = {}) {
387
401
  if (!session) {
388
402
  throw new Error('没有找到对应的 PromptX 项目。')
389
403
  }
390
- assertAgentRunner(session.engine)
404
+ const runEngine = engine || session.engine || 'codex'
405
+ assertAgentRunner(runEngine)
391
406
 
392
407
  const now = new Date().toISOString()
393
408
  const runId = `pxcr_${nanoid(12)}`
@@ -396,11 +411,11 @@ export function createCodexRun(input = {}) {
396
411
  transaction(() => {
397
412
  run(
398
413
  `INSERT INTO codex_runs (
399
- id, task_slug, session_id, engine, prompt, prompt_blocks_json, status,
414
+ id, task_slug, session_id, engine, run_meta_json, prompt, prompt_blocks_json, status,
400
415
  response_message, error_message, created_at, updated_at, started_at, finished_at
401
416
  )
402
- VALUES (?, ?, ?, ?, ?, ?, ?, '', '', ?, ?, ?, NULL)`,
403
- [runId, task.slug, session.id, session.engine || 'codex', prompt, JSON.stringify(promptBlocks), initialStatus, now, now, startedAt]
417
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', '', ?, ?, ?, NULL)`,
418
+ [runId, task.slug, session.id, runEngine, JSON.stringify({ displayEngine }), prompt, JSON.stringify(promptBlocks), initialStatus, now, now, startedAt]
404
419
  )
405
420
  })
406
421
 
@@ -645,7 +660,7 @@ export function getRunningCodexRunBySessionId(sessionId) {
645
660
  const activeStatuses = [...ACTIVE_RUN_STATUSES]
646
661
  const placeholders = activeStatuses.map(() => '?').join(', ')
647
662
  const row = get(
648
- `SELECT id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at
663
+ `SELECT id, task_slug, session_id, engine, run_meta_json, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at
649
664
  FROM codex_runs
650
665
  WHERE session_id = ?
651
666
  AND status IN (${placeholders})
@@ -666,7 +681,7 @@ export function getRunningCodexRunByTaskSlug(taskSlug) {
666
681
  const activeStatuses = [...ACTIVE_RUN_STATUSES]
667
682
  const placeholders = activeStatuses.map(() => '?').join(', ')
668
683
  const row = get(
669
- `SELECT id, task_slug, session_id, engine, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at
684
+ `SELECT id, task_slug, session_id, engine, run_meta_json, prompt, prompt_blocks_json, status, response_message, error_message, created_at, updated_at, started_at, finished_at
670
685
  FROM codex_runs
671
686
  WHERE task_slug = ?
672
687
  AND status IN (${placeholders})
@@ -715,6 +730,7 @@ export function listStaleActiveCodexRuns(maxAgeMs = 20000, now = new Date()) {
715
730
  runs.task_slug,
716
731
  runs.session_id,
717
732
  runs.engine,
733
+ runs.run_meta_json,
718
734
  runs.prompt,
719
735
  runs.prompt_blocks_json,
720
736
  runs.status,
@@ -733,6 +749,7 @@ export function listStaleActiveCodexRuns(maxAgeMs = 20000, now = new Date()) {
733
749
  runs.task_slug,
734
750
  runs.session_id,
735
751
  runs.engine,
752
+ runs.run_meta_json,
736
753
  runs.prompt,
737
754
  runs.prompt_blocks_json,
738
755
  runs.status,
@@ -231,6 +231,7 @@ function migrateToV1() {
231
231
  task_slug TEXT NOT NULL,
232
232
  session_id TEXT NOT NULL,
233
233
  engine TEXT NOT NULL DEFAULT 'codex',
234
+ run_meta_json TEXT NOT NULL DEFAULT '{}',
234
235
  prompt TEXT NOT NULL DEFAULT '',
235
236
  prompt_blocks_json TEXT NOT NULL DEFAULT '[]',
236
237
  status TEXT NOT NULL,
@@ -356,6 +357,7 @@ function applyAdditiveSchemaPatches() {
356
357
  WHERE COALESCE(NULLIF(engine_thread_id, ''), '') = ''`,
357
358
  `ALTER TABLE codex_runs ADD COLUMN prompt_blocks_json TEXT NOT NULL DEFAULT '[]'`,
358
359
  `ALTER TABLE codex_runs ADD COLUMN engine TEXT NOT NULL DEFAULT 'codex'`,
360
+ `ALTER TABLE codex_runs ADD COLUMN run_meta_json TEXT NOT NULL DEFAULT '{}'`,
359
361
  `UPDATE codex_runs
360
362
  SET engine = COALESCE(NULLIF(engine, ''), 'codex')
361
363
  WHERE COALESCE(NULLIF(engine, ''), '') = ''`,