@muyichengshayu/promptx 0.1.7 → 0.1.9
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 +15 -0
- package/README.md +24 -7
- package/apps/server/src/agents/claudeCodeRunner.js +5 -12
- package/apps/server/src/agents/index.js +2 -0
- package/apps/server/src/agents/openCodeRunner.js +459 -0
- package/apps/server/src/codex.js +6 -26
- package/apps/server/src/codexRunRuntime.js +38 -18
- package/apps/server/src/codexSessions.js +2 -2
- package/apps/server/src/index.js +3 -4
- package/apps/server/src/processControl.js +149 -0
- package/apps/web/dist/assets/CodexSessionManagerDialog-P3XnZ1Qt.js +1 -0
- package/apps/web/dist/assets/{TaskDiffReviewDialog-CnTgp_Sl.js → TaskDiffReviewDialog-DMzLretW.js} +2 -2
- package/apps/web/dist/assets/WorkbenchSettingsDialog-Ds1YR3GK.js +21 -0
- package/apps/web/dist/assets/WorkbenchView-Dvfcqpk_.js +216 -0
- package/apps/web/dist/assets/{index-BCO_uh3c.js → index-C1w_TyJ6.js} +6 -6
- package/apps/web/dist/assets/index-GbR48zHS.css +1 -0
- package/apps/web/dist/assets/{info-PhcZJ0GI.js → info-sxVO7QMl.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/docs/agent-run-protocol.md +2 -1
- package/package.json +1 -1
- package/packages/shared/src/index.js +1 -1
- package/scripts/doctor.mjs +76 -29
- package/apps/web/dist/assets/CodexSessionManagerDialog-XSoi47mH.js +0 -1
- package/apps/web/dist/assets/WorkbenchSettingsDialog-CywMkVzL.js +0 -21
- package/apps/web/dist/assets/WorkbenchView-AAJ60xm3.js +0 -216
- package/apps/web/dist/assets/index-BpuKxoB2.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.9
|
|
4
|
+
|
|
5
|
+
- 新增 `PromptX Glass Light` 毛玻璃主题,统一覆盖工作台主容器、任务列表、设置面板、弹窗、下拉、状态标签与图片预览层。
|
|
6
|
+
- 优化执行区视觉层次,补齐提示词 / 执行过程 / 回复三类消息卡片,以及 Markdown 中引用、代码块、表格等内容样式。
|
|
7
|
+
- 单独收敛手机端观感:压低玻璃强度与阴影,收紧任务卡、详情头部、设置页导航与表单布局。
|
|
8
|
+
- 修复 Markdown 表格在新主题下布局错乱的问题,改为使用外层滚动容器承载表格边框与横向滚动。
|
|
9
|
+
- 编辑区右上角主按钮在任务执行中保持“发送”文案并禁用,不再切换成“停止”,减少误触和语义跳变。
|
|
10
|
+
|
|
11
|
+
## 0.1.8
|
|
12
|
+
|
|
13
|
+
- 新增 OpenCode 执行引擎接入,补齐 runner、契约测试、Doctor 检查与文档说明,并支持在项目配置里直接选择 OpenCode。
|
|
14
|
+
- 项目管理里的执行引擎列表改为服务端下发,避免前后端版本不一致时前端看不到新引擎选项。
|
|
15
|
+
- 优化项目编辑规则:项目未执行前允许切换执行引擎,执行后自动锁定,保持与工作目录一致的配置心智。
|
|
16
|
+
- 强化运行停止逻辑:用户点击停止后会立即进入强制停止流程,并补齐更可靠的跨平台子进程终止处理与“正在停止...”反馈。
|
|
17
|
+
|
|
3
18
|
## 0.1.7
|
|
4
19
|
|
|
5
20
|
- 新增多引擎运行器抽象,除了 Codex 之外,现已支持接入 Claude Code,并统一项目层的执行引擎配置与展示。
|
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
PromptX 是一个面向本机 AI 协作的轻量工作台。
|
|
4
4
|
|
|
5
|
-
它适合先整理需求、截图、文本、PDF、禅道 Bug 等上下文,再持续发送给本机
|
|
5
|
+
它适合先整理需求、截图、文本、PDF、禅道 Bug 等上下文,再持续发送给本机 AI Agent,在同一页里查看执行过程和多轮结果。
|
|
6
6
|
|
|
7
7
|
## 核心能力
|
|
8
8
|
|
|
9
9
|
- 左侧管理任务,中间查看项目执行过程,右侧整理输入内容
|
|
10
10
|
- 支持文本、图片、`md`、`txt`、`pdf`
|
|
11
|
-
-
|
|
11
|
+
- 支持为任务绑定本机项目,并持续复用同一个执行引擎线程
|
|
12
|
+
- 支持多执行引擎,当前已接入 `Codex`、`Claude Code`、`OpenCode`
|
|
12
13
|
- 支持查看执行过程、代码变更和最终回复
|
|
13
14
|
- 支持公开页与 Raw 导出
|
|
14
15
|
- 内置禅道 Chrome 扩展,可一键把 Bug 内容带入工作台
|
|
@@ -30,8 +31,12 @@ PromptX 是一个面向本机 AI 协作的轻量工作台。
|
|
|
30
31
|
## 运行前提
|
|
31
32
|
|
|
32
33
|
- 已安装 Node,支持 `20`、`22`、`24`,推荐 `22`
|
|
33
|
-
-
|
|
34
|
-
-
|
|
34
|
+
- 本机至少安装一个可用执行引擎
|
|
35
|
+
- 当前支持:
|
|
36
|
+
- `codex --version`
|
|
37
|
+
- `claude --version`
|
|
38
|
+
- `opencode --version`
|
|
39
|
+
- 如使用 Codex,建议开启高权限并使用满血模式
|
|
35
40
|
|
|
36
41
|
## 安装
|
|
37
42
|
|
|
@@ -67,9 +72,21 @@ promptx doctor
|
|
|
67
72
|
1. 打开工作台,新建或选择一个任务
|
|
68
73
|
2. 在右侧整理文本、图片、文件等上下文
|
|
69
74
|
3. 在中间选择一个 PromptX 项目
|
|
70
|
-
4.
|
|
75
|
+
4. 为项目选择执行引擎,并点击发送
|
|
71
76
|
5. 在中间继续查看执行过程,并按需多轮发送
|
|
72
77
|
|
|
78
|
+
## 当前支持的执行引擎
|
|
79
|
+
|
|
80
|
+
- `Codex`
|
|
81
|
+
- `Claude Code`
|
|
82
|
+
- `OpenCode`
|
|
83
|
+
|
|
84
|
+
PromptX 内部已经开始使用统一的 agent run 事件协议,后续继续扩展其他执行引擎时,前端展示层和执行过程面板可以尽量复用。
|
|
85
|
+
|
|
86
|
+
如需查看这套协议约定,可参考:
|
|
87
|
+
|
|
88
|
+
- `docs/agent-run-protocol.md`
|
|
89
|
+
|
|
73
90
|
## 远程访问 Relay(预览)
|
|
74
91
|
|
|
75
92
|
如果你希望在手机上远程访问自己电脑上的 PromptX,或想在云端部署多租户 Relay,请直接查看:
|
|
@@ -103,10 +120,10 @@ promptx doctor
|
|
|
103
120
|
|
|
104
121
|
## 注意事项
|
|
105
122
|
|
|
106
|
-
- 当前只支持 Codex,不支持其他模型后端
|
|
107
123
|
- 当前以本机单用户使用为主,不包含账号体系和团队权限
|
|
108
124
|
- 默认仅监听本机地址;如需跨设备访问,建议通过 Tailscale
|
|
109
|
-
-
|
|
125
|
+
- 如果执行引擎运行在受限权限下,文件读写和自动修改能力会明显受限
|
|
126
|
+
- 不同执行引擎的工具能力、输出事件丰富度和稳定性可能会有差异
|
|
110
127
|
|
|
111
128
|
## 本地数据目录
|
|
112
129
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createTurnCompletedEvent,
|
|
17
17
|
getAgentEngineLabel,
|
|
18
18
|
} from '../../../../packages/shared/src/index.js'
|
|
19
|
+
import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
|
|
19
20
|
|
|
20
21
|
const CLAUDE_CODE_BIN = process.env.CLAUDE_CODE_BIN || 'claude'
|
|
21
22
|
const CLAUDE_DEFAULT_ARGS = ['--dangerously-skip-permissions']
|
|
@@ -68,16 +69,10 @@ function resolveClaudeCodeBinary() {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function createClaudeSpawn(commandArgs = [], cwd = '') {
|
|
71
|
-
const options = {
|
|
72
|
-
|
|
72
|
+
const options = createManagedSpawnOptions({
|
|
73
|
+
cwd,
|
|
73
74
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const normalizedCwd = String(cwd || '').trim()
|
|
78
|
-
if (normalizedCwd) {
|
|
79
|
-
options.cwd = normalizedCwd
|
|
80
|
-
}
|
|
75
|
+
})
|
|
81
76
|
|
|
82
77
|
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_CLAUDE_CODE_BIN)) {
|
|
83
78
|
return spawn(
|
|
@@ -524,9 +519,7 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
|
|
|
524
519
|
child,
|
|
525
520
|
result,
|
|
526
521
|
cancel() {
|
|
527
|
-
|
|
528
|
-
child.kill('SIGTERM')
|
|
529
|
-
}
|
|
522
|
+
forceStopChildProcess(child)
|
|
530
523
|
},
|
|
531
524
|
}
|
|
532
525
|
}
|
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
} from '../../../../packages/shared/src/index.js'
|
|
7
7
|
import { codexRunner } from './codexRunner.js'
|
|
8
8
|
import { claudeCodeRunner } from './claudeCodeRunner.js'
|
|
9
|
+
import { openCodeRunner } from './openCodeRunner.js'
|
|
9
10
|
|
|
10
11
|
const runnerRegistry = new Map([
|
|
11
12
|
[codexRunner.engine, codexRunner],
|
|
12
13
|
[claudeCodeRunner.engine, claudeCodeRunner],
|
|
14
|
+
[openCodeRunner.engine, openCodeRunner],
|
|
13
15
|
])
|
|
14
16
|
|
|
15
17
|
export function getAgentRunner(engine = AGENT_ENGINES.CODEX) {
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
4
|
+
import {
|
|
5
|
+
AGENT_ENGINES,
|
|
6
|
+
AGENT_RUN_ITEM_TYPES,
|
|
7
|
+
createAgentEventEnvelopeEvent,
|
|
8
|
+
createCompletedEnvelopeEvent,
|
|
9
|
+
createErrorEvent,
|
|
10
|
+
createItemCompletedEvent,
|
|
11
|
+
createItemStartedEvent,
|
|
12
|
+
createStatusEnvelopeEvent,
|
|
13
|
+
createStderrEnvelopeEvent,
|
|
14
|
+
createStdoutEnvelopeEvent,
|
|
15
|
+
createThreadStartedEvent,
|
|
16
|
+
createTurnCompletedEvent,
|
|
17
|
+
getAgentEngineLabel,
|
|
18
|
+
} from '../../../../packages/shared/src/index.js'
|
|
19
|
+
import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
|
|
20
|
+
|
|
21
|
+
const OPENCODE_BIN = process.env.OPENCODE_BIN || 'opencode'
|
|
22
|
+
const RESOLVED_OPENCODE_BIN = resolveOpenCodeBinary()
|
|
23
|
+
|
|
24
|
+
function resolveOpenCodeBinary() {
|
|
25
|
+
if (process.platform !== 'win32') {
|
|
26
|
+
return OPENCODE_BIN
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (path.extname(OPENCODE_BIN)) {
|
|
30
|
+
return OPENCODE_BIN
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(`${OPENCODE_BIN}.cmd`)) {
|
|
34
|
+
return `${OPENCODE_BIN}.cmd`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(`${OPENCODE_BIN}.bat`)) {
|
|
38
|
+
return `${OPENCODE_BIN}.bat`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(OPENCODE_BIN)) {
|
|
42
|
+
return OPENCODE_BIN
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const output = execFileSync('where.exe', [OPENCODE_BIN], {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
}).trim()
|
|
51
|
+
|
|
52
|
+
if (!output) {
|
|
53
|
+
return OPENCODE_BIN
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const candidates = output
|
|
57
|
+
.split(/\r?\n/g)
|
|
58
|
+
.map((line) => line.trim())
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
|
|
61
|
+
return candidates.find((item) => /\.(cmd|bat)$/i.test(item))
|
|
62
|
+
|| candidates.find((item) => /\.(exe|com)$/i.test(item))
|
|
63
|
+
|| candidates[0]
|
|
64
|
+
|| OPENCODE_BIN
|
|
65
|
+
} catch {
|
|
66
|
+
return OPENCODE_BIN
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createOpenCodeSpawn(commandArgs = [], cwd = '') {
|
|
71
|
+
const options = createManagedSpawnOptions({
|
|
72
|
+
cwd,
|
|
73
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_OPENCODE_BIN)) {
|
|
77
|
+
return spawn(
|
|
78
|
+
process.env.ComSpec || 'cmd.exe',
|
|
79
|
+
['/d', '/s', '/c', RESOLVED_OPENCODE_BIN, ...commandArgs],
|
|
80
|
+
options
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return spawn(RESOLVED_OPENCODE_BIN, commandArgs, options)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeSpawnError(error) {
|
|
88
|
+
if (error?.code === 'ENOENT') {
|
|
89
|
+
const attempted = RESOLVED_OPENCODE_BIN === OPENCODE_BIN
|
|
90
|
+
? OPENCODE_BIN
|
|
91
|
+
: `${OPENCODE_BIN} -> ${RESOLVED_OPENCODE_BIN}`
|
|
92
|
+
|
|
93
|
+
return new Error(
|
|
94
|
+
`找不到 OpenCode CLI(尝试执行:${attempted})。请先确认终端里可以运行 \`opencode --version\`,或设置环境变量 \`OPENCODE_BIN\`。`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return error
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseJsonLine(line = '') {
|
|
102
|
+
const text = String(line || '').trim()
|
|
103
|
+
if (!text) {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(text)
|
|
109
|
+
} catch {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function splitBufferedLines(buffer = '') {
|
|
115
|
+
const text = String(buffer || '')
|
|
116
|
+
if (!text) {
|
|
117
|
+
return { lines: [], rest: '' }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
121
|
+
const parts = normalized.split('\n')
|
|
122
|
+
const rest = parts.pop() || ''
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
lines: parts.map((line) => line.trim()).filter(Boolean),
|
|
126
|
+
rest,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function flushBufferedText(buffer = '') {
|
|
131
|
+
const { lines, rest } = splitBufferedLines(buffer)
|
|
132
|
+
const tail = String(rest || '').trim()
|
|
133
|
+
return tail ? [...lines, tail] : lines
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function summarizeOpenCodeInput(input = {}) {
|
|
137
|
+
if (!input || typeof input !== 'object') {
|
|
138
|
+
return ''
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const command = String(input.command || '').trim()
|
|
142
|
+
if (command) {
|
|
143
|
+
return command
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const singleValueKeys = ['filePath', 'path', 'pattern', 'query', 'url', 'description']
|
|
147
|
+
for (const key of singleValueKeys) {
|
|
148
|
+
const value = String(input[key] || '').trim()
|
|
149
|
+
if (value) {
|
|
150
|
+
return value
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const compact = JSON.stringify(input)
|
|
156
|
+
return compact.length <= 240 ? compact : `${compact.slice(0, 237)}...`
|
|
157
|
+
} catch {
|
|
158
|
+
return ''
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function stringifyOpenCodeOutput(output) {
|
|
163
|
+
if (typeof output === 'string') {
|
|
164
|
+
return output.trim()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (output == null) {
|
|
168
|
+
return ''
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const compact = JSON.stringify(output)
|
|
173
|
+
return compact.length <= 12000 ? compact : `${compact.slice(0, 11997)}...`
|
|
174
|
+
} catch {
|
|
175
|
+
return String(output || '').trim()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildOpenCodeToolCommand(event = {}) {
|
|
180
|
+
const part = event?.part && typeof event.part === 'object' ? event.part : event
|
|
181
|
+
const toolName = String(part?.tool || 'OpenCode tool').trim() || 'OpenCode tool'
|
|
182
|
+
const input = part?.state?.input && typeof part.state.input === 'object'
|
|
183
|
+
? part.state.input
|
|
184
|
+
: {}
|
|
185
|
+
const inputSummary = summarizeOpenCodeInput(input)
|
|
186
|
+
return inputSummary ? `${toolName}: ${inputSummary}` : toolName
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function extractOpenCodeText(event = {}) {
|
|
190
|
+
return String(event?.part?.text || event?.text || '').trim()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function extractOpenCodeSessionId(event = {}) {
|
|
194
|
+
const candidates = [
|
|
195
|
+
event?.sessionID,
|
|
196
|
+
event?.sessionId,
|
|
197
|
+
event?.part?.sessionID,
|
|
198
|
+
event?.part?.sessionId,
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
return candidates.map((value) => String(value || '').trim()).find(Boolean) || ''
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function extractOpenCodeUsage(event = {}) {
|
|
205
|
+
const tokens = event?.part?.tokens
|
|
206
|
+
if (!tokens || typeof tokens !== 'object') {
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
input_tokens: Number(tokens.input) || 0,
|
|
212
|
+
output_tokens: Number(tokens.output) || 0,
|
|
213
|
+
cached_input_tokens: Number(tokens.cache?.read) || 0,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function createOpenCodeRunStatusEvent(session = {}) {
|
|
218
|
+
const hasExistingThread = Boolean(
|
|
219
|
+
String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return createStatusEnvelopeEvent({
|
|
223
|
+
stage: hasExistingThread ? 'resuming' : 'starting',
|
|
224
|
+
message: hasExistingThread
|
|
225
|
+
? '已连接 PromptX 项目,正在继续这轮执行。'
|
|
226
|
+
: '已创建 PromptX 项目,正在启动第一轮执行。',
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function createOpenCodeNormalizationState() {
|
|
231
|
+
return {
|
|
232
|
+
turnStarted: false,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function normalizeOpenCodeEvents(event = {}, state = createOpenCodeNormalizationState()) {
|
|
237
|
+
const eventType = String(event?.type || '').trim().toLowerCase()
|
|
238
|
+
const normalizedEvents = []
|
|
239
|
+
|
|
240
|
+
if (eventType === 'step_start') {
|
|
241
|
+
if (!state.turnStarted) {
|
|
242
|
+
state.turnStarted = true
|
|
243
|
+
normalizedEvents.push({ type: 'turn.started' })
|
|
244
|
+
}
|
|
245
|
+
return normalizedEvents
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (eventType === 'tool_use') {
|
|
249
|
+
const command = buildOpenCodeToolCommand(event)
|
|
250
|
+
const status = String(event?.part?.state?.status || '').trim().toLowerCase()
|
|
251
|
+
const output = stringifyOpenCodeOutput(event?.part?.state?.output)
|
|
252
|
+
|
|
253
|
+
normalizedEvents.push(createItemStartedEvent({
|
|
254
|
+
type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
|
|
255
|
+
command,
|
|
256
|
+
status: 'in_progress',
|
|
257
|
+
}))
|
|
258
|
+
|
|
259
|
+
if (status === 'completed' || status === 'failed' || status === 'error') {
|
|
260
|
+
normalizedEvents.push(createItemCompletedEvent({
|
|
261
|
+
type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
|
|
262
|
+
command,
|
|
263
|
+
status: status === 'completed' ? 'completed' : 'failed',
|
|
264
|
+
exit_code: status === 'completed' ? 0 : 1,
|
|
265
|
+
aggregated_output: output,
|
|
266
|
+
}))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return normalizedEvents
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (eventType === 'text') {
|
|
273
|
+
const text = extractOpenCodeText(event)
|
|
274
|
+
if (!text) {
|
|
275
|
+
return normalizedEvents
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
normalizedEvents.push(createItemCompletedEvent({
|
|
279
|
+
type: AGENT_RUN_ITEM_TYPES.AGENT_MESSAGE,
|
|
280
|
+
text,
|
|
281
|
+
}))
|
|
282
|
+
return normalizedEvents
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (eventType === 'step_finish') {
|
|
286
|
+
const reason = String(event?.part?.reason || '').trim().toLowerCase()
|
|
287
|
+
if (reason === 'stop') {
|
|
288
|
+
const usage = extractOpenCodeUsage(event)
|
|
289
|
+
normalizedEvents.push(createTurnCompletedEvent(usage ? { usage } : {}))
|
|
290
|
+
return normalizedEvents
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return normalizedEvents
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (eventType === 'error') {
|
|
297
|
+
const message = extractOpenCodeText(event) || String(event?.message || event?.error || '').trim()
|
|
298
|
+
if (message) {
|
|
299
|
+
normalizedEvents.push(createErrorEvent(message))
|
|
300
|
+
}
|
|
301
|
+
return normalizedEvents
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return [{
|
|
305
|
+
type: `opencode.${eventType || 'event'}`,
|
|
306
|
+
detail: extractOpenCodeText(event),
|
|
307
|
+
}]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function normalizeOpenCodeEvent(event = {}, state = createOpenCodeNormalizationState()) {
|
|
311
|
+
return normalizeOpenCodeEvents(event, state)[0] || null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createExecArgs(session, prompt) {
|
|
315
|
+
const args = [
|
|
316
|
+
'run',
|
|
317
|
+
'--format',
|
|
318
|
+
'json',
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
const sessionId = String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
|
|
322
|
+
if (sessionId) {
|
|
323
|
+
args.push('--session', sessionId)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (session?.cwd) {
|
|
327
|
+
args.push('--dir', session.cwd)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
args.push(String(prompt || ''))
|
|
331
|
+
return args
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function streamPromptToOpenCodeSession(sessionInput, prompt, callbacks = {}) {
|
|
335
|
+
const session = sessionInput && typeof sessionInput === 'object' ? sessionInput : null
|
|
336
|
+
const normalizedPrompt = String(prompt || '').trim()
|
|
337
|
+
|
|
338
|
+
if (!session?.id || !session?.cwd) {
|
|
339
|
+
throw new Error('缺少 PromptX 项目。')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!normalizedPrompt) {
|
|
343
|
+
throw new Error('没有可发送的提示词。')
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const onEvent = typeof callbacks.onEvent === 'function' ? callbacks.onEvent : () => {}
|
|
347
|
+
const onThreadStarted = typeof callbacks.onThreadStarted === 'function' ? callbacks.onThreadStarted : () => {}
|
|
348
|
+
|
|
349
|
+
const child = createOpenCodeSpawn(createExecArgs(session, normalizedPrompt), session.cwd)
|
|
350
|
+
onEvent(createOpenCodeRunStatusEvent(session))
|
|
351
|
+
|
|
352
|
+
let stdoutBuffer = ''
|
|
353
|
+
let stderrBuffer = ''
|
|
354
|
+
let lastStderrLine = ''
|
|
355
|
+
let finalMessage = ''
|
|
356
|
+
let finalSessionId = String(session.engineSessionId || session.engineThreadId || session.codexThreadId || '').trim()
|
|
357
|
+
const normalizationState = createOpenCodeNormalizationState()
|
|
358
|
+
|
|
359
|
+
const rememberSessionId = (sessionId) => {
|
|
360
|
+
const value = String(sessionId || '').trim()
|
|
361
|
+
if (!value || value === finalSessionId) {
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
finalSessionId = value
|
|
366
|
+
onThreadStarted(value)
|
|
367
|
+
onEvent(createAgentEventEnvelopeEvent(createThreadStartedEvent(value)))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const emitOpenCodeJsonLine = (line) => {
|
|
371
|
+
const event = parseJsonLine(line)
|
|
372
|
+
if (!event) {
|
|
373
|
+
onEvent(createStdoutEnvelopeEvent(line))
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const sessionId = extractOpenCodeSessionId(event)
|
|
378
|
+
if (sessionId) {
|
|
379
|
+
rememberSessionId(sessionId)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const normalizedEvents = normalizeOpenCodeEvents(event, normalizationState)
|
|
383
|
+
normalizedEvents.forEach((normalizedEvent) => {
|
|
384
|
+
onEvent(createAgentEventEnvelopeEvent(normalizedEvent))
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
if (String(event?.type || '').trim().toLowerCase() === 'text') {
|
|
388
|
+
const text = extractOpenCodeText(event)
|
|
389
|
+
if (text) {
|
|
390
|
+
finalMessage = `${finalMessage}${finalMessage ? '\n' : ''}${text}`
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
child.stdout.on('data', (chunk) => {
|
|
396
|
+
stdoutBuffer += chunk.toString()
|
|
397
|
+
const { lines, rest } = splitBufferedLines(stdoutBuffer)
|
|
398
|
+
stdoutBuffer = rest
|
|
399
|
+
lines.forEach(emitOpenCodeJsonLine)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
child.stderr.on('data', (chunk) => {
|
|
403
|
+
stderrBuffer += chunk.toString()
|
|
404
|
+
const { lines, rest } = splitBufferedLines(stderrBuffer)
|
|
405
|
+
stderrBuffer = rest
|
|
406
|
+
lines.forEach((line) => {
|
|
407
|
+
lastStderrLine = line
|
|
408
|
+
onEvent(createStderrEnvelopeEvent(line))
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
const result = new Promise((resolve, reject) => {
|
|
413
|
+
child.on('error', (error) => {
|
|
414
|
+
reject(normalizeSpawnError(error))
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
child.on('close', (code) => {
|
|
418
|
+
flushBufferedText(stdoutBuffer).forEach(emitOpenCodeJsonLine)
|
|
419
|
+
flushBufferedText(stderrBuffer).forEach((line) => {
|
|
420
|
+
lastStderrLine = line
|
|
421
|
+
onEvent(createStderrEnvelopeEvent(line))
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
if (code !== 0) {
|
|
425
|
+
reject(new Error(lastStderrLine || 'OpenCode 执行失败。'))
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const message = finalMessage.trim()
|
|
430
|
+
onEvent(createCompletedEnvelopeEvent(message))
|
|
431
|
+
|
|
432
|
+
resolve({
|
|
433
|
+
sessionId: session.id,
|
|
434
|
+
threadId: finalSessionId,
|
|
435
|
+
message,
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
child,
|
|
442
|
+
result,
|
|
443
|
+
cancel() {
|
|
444
|
+
forceStopChildProcess(child)
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export const openCodeRunner = {
|
|
450
|
+
engine: AGENT_ENGINES.OPENCODE,
|
|
451
|
+
label: getAgentEngineLabel(AGENT_ENGINES.OPENCODE),
|
|
452
|
+
supportsWorkspaceHistory: false,
|
|
453
|
+
listKnownWorkspaces() {
|
|
454
|
+
return []
|
|
455
|
+
},
|
|
456
|
+
streamSessionPrompt(session, prompt, callbacks = {}) {
|
|
457
|
+
return streamPromptToOpenCodeSession(session, prompt, callbacks)
|
|
458
|
+
},
|
|
459
|
+
}
|
package/apps/server/src/codex.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createStderrEnvelopeEvent,
|
|
14
14
|
createStdoutEnvelopeEvent,
|
|
15
15
|
} from '../../../packages/shared/src/index.js'
|
|
16
|
+
import { createManagedSpawnOptions, forceStopChildProcess } from './processControl.js'
|
|
16
17
|
|
|
17
18
|
const CODEX_BIN = process.env.CODEX_BIN || 'codex'
|
|
18
19
|
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
|
|
@@ -79,16 +80,10 @@ function resolveCodexBinary() {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function createCodexSpawn(commandArgs = [], cwd = '') {
|
|
82
|
-
const options = {
|
|
83
|
-
|
|
83
|
+
const options = createManagedSpawnOptions({
|
|
84
|
+
cwd,
|
|
84
85
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
const normalizedCwd = String(cwd || '').trim()
|
|
88
|
-
|
|
89
|
-
if (normalizedCwd) {
|
|
90
|
-
options.cwd = normalizedCwd
|
|
91
|
-
}
|
|
86
|
+
})
|
|
92
87
|
|
|
93
88
|
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_CODEX_BIN)) {
|
|
94
89
|
return spawn(
|
|
@@ -550,25 +545,10 @@ export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {})
|
|
|
550
545
|
child,
|
|
551
546
|
result,
|
|
552
547
|
cancel() {
|
|
553
|
-
if (child.killed) {
|
|
548
|
+
if (child.killed || !child.pid) {
|
|
554
549
|
return
|
|
555
550
|
}
|
|
556
|
-
|
|
557
|
-
if (process.platform === 'win32' && child.pid) {
|
|
558
|
-
try {
|
|
559
|
-
execFileSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
|
|
560
|
-
stdio: 'ignore',
|
|
561
|
-
windowsHide: true,
|
|
562
|
-
})
|
|
563
|
-
return
|
|
564
|
-
} catch {
|
|
565
|
-
// Fall through to the default child kill when taskkill is unavailable.
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (!child.killed) {
|
|
570
|
-
child.kill('SIGTERM')
|
|
571
|
-
}
|
|
551
|
+
forceStopChildProcess(child)
|
|
572
552
|
},
|
|
573
553
|
}
|
|
574
554
|
}
|