@muyichengshayu/promptx 0.2.0 → 0.2.2
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 +13 -0
- package/apps/runner/src/engines/index.js +13 -3
- package/apps/runner/src/engines/shellRunner.js +216 -0
- package/apps/runner/src/runManager.js +68 -9
- package/apps/server/src/agents/index.js +18 -3
- package/apps/server/src/codexRuns.js +24 -7
- package/apps/server/src/db.js +2 -0
- package/apps/server/src/gitDiff.js +279 -17
- package/apps/server/src/internalRoutes.js +8 -1
- package/apps/server/src/relayClient.js +5 -1
- package/apps/server/src/relayConfig.js +10 -0
- package/apps/server/src/runDispatchService.js +44 -13
- package/apps/server/src/runEventIngest.js +6 -4
- package/apps/server/src/taskRoutes.js +72 -0
- package/apps/web/dist/assets/{CodexSessionManagerDialog-BbaObUl_.js → CodexSessionManagerDialog-CELTkz9T.js} +1 -1
- package/apps/web/dist/assets/{TaskDiffReviewDialog-ClUn4Ni4.js → TaskDiffReviewDialog-VwZmo00b.js} +1 -1
- package/apps/web/dist/assets/WorkbenchSettingsDialog-CThWkZHd.js +1 -0
- package/apps/web/dist/assets/WorkbenchView-D6auwJnA.js +60 -0
- package/apps/web/dist/assets/index-8vfFmsVl.js +2 -0
- package/apps/web/dist/assets/{index-CTHBQ5Ng.css → index-BPfQQtEB.css} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/shared/src/index.js +6 -0
- package/packages/shared/src/shellCommands.js +81 -0
- package/packages/shared/src/shellCommands.test.js +45 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-C74C5fs1.js +0 -1
- package/apps/web/dist/assets/WorkbenchView-BgfyrrvY.js +0 -57
- package/apps/web/dist/assets/index-BIa_ZvMq.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
- 修复 `WeChat` 主题下删除任务等危险确认弹窗的按钮文字不可见问题:补齐 `tool-button-danger` 与各类 `subtle` 按钮在微信主题中的语义色覆盖,避免通用按钮样式把危险操作刷成白底白字。
|
|
6
|
+
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
- 新增工作台 `shell` 命令模式:在右侧编辑区输入 `!pwd`、`!git status`、`!pnpm test` 这类纯文本命令时,PromptX 会直接按当前选中的 `Codex / Claude Code / OpenCode` 语义发起本地命令执行,并把结果继续收拢回原有的 `PromptX → Agent / Agent` 对话流,不额外引入一套独立的 Shell 卡片心智。
|
|
10
|
+
- 收紧命令模式的安全边界与展示逻辑:命令意图改为由服务端重新解析,不再信任前端直传命令;shell run 固定绑定 root project session,避免污染 member session 的 thread/identity;默认仅本机可用,Relay 需在设置里显式开启“允许远程执行命令模式”后,且仅对带内部标记的 relay 转发请求放行。
|
|
11
|
+
- 为远程命令模式补齐配置与回归测试:Relay 设置页新增高权限开关与明确警告,诊断信息也会带上该配置;同时新增本地允许、远程默认拒绝、仅 relay 显式开启可用、环境变量覆盖等测试,降低后续回归风险。
|
|
12
|
+
- 修复 runner 大事件上报导致任务卡死的问题:内部 runner events 接口提高了 body limit,避免长过程日志或大批量事件刷新时触发 `Request body is too large`,导致一轮任务停在中间。
|
|
13
|
+
- 改进代码变更审查对 Git submodule 的支持:代码变更与 diff review 现在会展开 submodule 内部的真实文件改动,而不再只显示顶层 submodule 占位项;相关任务级、轮次级与子文件明细链路都已补齐测试。
|
|
14
|
+
- 补充开发环境的 `Vite allowedHosts` 配置,方便在代理、局域网或特定本地域名下更稳定地访问工作台开发服务。
|
|
15
|
+
|
|
3
16
|
## 0.2.0
|
|
4
17
|
|
|
5
18
|
- 项目正式升级为多 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(
|
|
28
|
+
return runnerRegistry.get(normalizeRunnerEngine(engine)) || null
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export function assertAgentRunner(engine = AGENT_ENGINES.CODEX) {
|
|
22
|
-
const normalized =
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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,
|
package/apps/server/src/db.js
CHANGED
|
@@ -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, ''), '') = ''`,
|