@luchenglong/cli-bridge 0.2.8 → 0.2.10
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 +91 -0
- package/PROVIDER-CONTRACT.md +87 -5
- package/README.md +12 -11
- package/docs/HANDOVER.md +109 -15
- package/dynamic/agy.host.js +1 -1
- package/dynamic/claude.host.js +1858 -0
- package/dynamic/claude.host.template.mjs +108 -0
- package/dynamic/codebuddy.host.js +1427 -1379
- package/dynamic/codex.host.js +1930 -0
- package/dynamic/codex.host.template.mjs +137 -0
- package/dynamic/mimo.host.js +721 -515
- package/dynamic/mimo.host.template.mjs +2 -0
- package/dynamic/opencode.host.js +741 -585
- package/dynamic/opencode.host.template.mjs +2 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +74 -90
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +2 -3
- package/mcp/cli-bridge-mcp.mjs +4 -2
- package/mcp/providers/claude.mjs +231 -0
- package/mcp/providers/codebuddy.mjs +427 -427
- package/mcp/providers/codex.mjs +239 -0
- package/package.json +6 -2
- package/preset/claude-first/agent.cordis.yml +136 -0
- package/preset/claude-first/cli-bridge.mjs +131 -0
- package/preset/claude-first/plugin-list.mjs +28 -0
- package/preset/claude-first/preset.yml +3 -0
- package/preset/codex-first/agent.cordis.yml +136 -0
- package/preset/codex-first/cli-bridge.mjs +160 -0
- package/preset/codex-first/plugin-list.mjs +28 -0
- package/preset/codex-first/preset.yml +3 -0
- package/providers/claude-exe.mjs +112 -0
- package/providers/claude-models.mjs +338 -0
- package/providers/claude-policy.mjs +37 -0
- package/providers/claude.mjs +561 -0
- package/providers/codebuddy-catalog.mjs +367 -599
- package/providers/codebuddy-consts.mjs +72 -72
- package/providers/codebuddy-models-source.mjs +254 -0
- package/providers/codebuddy-models-tool.mjs +106 -106
- package/providers/codebuddy-schemas.mjs +46 -46
- package/providers/codebuddy.mjs +600 -578
- package/providers/codex-cli.mjs +405 -0
- package/providers/codex-models.mjs +107 -0
- package/providers/codex.mjs +585 -0
- package/providers/mimo-pure.mjs +270 -111
- package/providers/mimo.mjs +453 -517
- package/providers/opencode-exe.mjs +190 -63
- package/providers/opencode.mjs +553 -587
- package/providers/registry.mjs +25 -1
- package/scripts/build.mjs +261 -262
- package/scripts/verify.mjs +8 -2
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// providers/codex.mjs — codex provider descriptor(第五家 CLI,无旧包可迁)。
|
|
2
|
+
//
|
|
3
|
+
// 基线 = 本机实测 codex-cli 0.98.0(Windows,mise/cherrystudio shim)+ 真实 argv/事件流。
|
|
4
|
+
// 写代码时最容易踩的几条(全部实机验证过,见 docs 里 codex 任务卡):
|
|
5
|
+
// • 非交互 = `codex exec [OPTIONS] "PROMPT"`,prompt 是**位置参数**,必须放最后。
|
|
6
|
+
// • ⚠️ `codex exec` **不接受** `-a/--ask-for-approval`(clap 直接报 unexpected argument;
|
|
7
|
+
// 那是根命令 `codex` 的旗)。要设审批策略只能走 `-c approval_policy="never"`。
|
|
8
|
+
// • ⚠️ `codex exec resume` 只认 [SESSION_ID] [PROMPT] + -c/-m/-i/--json/--last/--all/
|
|
9
|
+
// --skip-git-repo-check/--full-auto;**没有 -C、没有 -s、没有 -o、没有 --add-dir**
|
|
10
|
+
// (实测传 -C 报 unexpected argument)。续聊的 cwd 只能靠 spawn 的 cwd。
|
|
11
|
+
// • 非 git 目录必须带 --skip-git-repo-check,否则直接拒绝运行。
|
|
12
|
+
// • `--json` → NDJSON 事件流:thread.started / turn.started / item.started /
|
|
13
|
+
// item.completed / turn.completed;失败另加顶层 `error` + `turn.failed`。
|
|
14
|
+
// ⚠️ item.completed 里也可能出现 item.type==="error" 的**告警**(如「此会话原模型与
|
|
15
|
+
// 当前模型不同」),那是正常续聊的提示,绝不可当成致命错误(实测 --last 续聊就带)。
|
|
16
|
+
// • 退出码:0 成功 / 1 运行失败(stdout 仍是合法 JSON)/ 2 参数错误。
|
|
17
|
+
// • 无 --json 时 stdout 就是纯文本答复,stderr 有元信息头(version/workdir/model/
|
|
18
|
+
// provider/approval/sandbox/session id)—— 诊断只读它,不解析成结果。
|
|
19
|
+
//
|
|
20
|
+
// 红线(PROVIDER-CONTRACT.md §1/§8):
|
|
21
|
+
// - 本文件会被 build 内联进 dynamic 沙箱(new Function):绝不出现 import.meta 字面量;
|
|
22
|
+
// process 只经 typeof 守卫读取;宿主能力全部走 deps/attach(hostOpts)。
|
|
23
|
+
// - 只允许 import '../core/bridge-kit.mjs' 与同目录 codex-*.mjs 片文件(片文件由 build
|
|
24
|
+
// 自动内联进同一作用域;主文件超 600 行红线所以必须拆)。
|
|
25
|
+
import { createStatusEngine, createRunner, clampInt, isLimited, endpointHost } from '../core/bridge-kit.mjs'
|
|
26
|
+
import {
|
|
27
|
+
codexSetHost, codexResolveExePrefix, codexExeCandidates, codexReadConfig,
|
|
28
|
+
codexModelsEndpoint, codexCachedModelOptions, codexCachedLiveModels, codexLiveModels,
|
|
29
|
+
codexCatalogModels, codexBuiltinModels, codexHomeDir
|
|
30
|
+
} from './codex-cli.mjs'
|
|
31
|
+
import { codexModelsTool } from './codex-models.mjs'
|
|
32
|
+
export { codexResolveExePrefix, codexExeCandidates }
|
|
33
|
+
|
|
34
|
+
function nowIso() { return new Date().toISOString() }
|
|
35
|
+
|
|
36
|
+
// 实测枚举(根命令 help + exec help)。
|
|
37
|
+
const SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access']
|
|
38
|
+
const APPROVAL_POLICIES = ['untrusted', 'on-failure', 'on-request', 'never']
|
|
39
|
+
const REASONING_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh']
|
|
40
|
+
|
|
41
|
+
// 手动拉取(契约 §3.2):面板只把 error 原文显示给用户,所以失败文案必须自带下一步。
|
|
42
|
+
// codex-cli 没有 models 子命令,live 清单只能走端点 HTTP —— 排查顺序就围绕端点/凭据写。
|
|
43
|
+
const REFRESH_TIMEOUT_MS = 20000
|
|
44
|
+
const REFRESH_HINT = '排查顺序:① 看 ~/.codex/config.toml 的 [model_providers.<当前 provider>] 里 base_url / experimental_bearer_token(或 auth.json 的 OPENAI_BASE_URL / OPENAI_API_KEY)是否指向可用端点;② 可用 CODEX_BRIDGE_MODELS_URL / CODEX_BRIDGE_MODELS_TOKEN 临时覆盖端点与凭据;③ 再点一次「拉取可用模型」(codex_models 默认 source=live 读的是同一份清单)。'
|
|
45
|
+
|
|
46
|
+
// ── 三工具 schema ────────────────────────────────────────────────────────────
|
|
47
|
+
const RUN_PARAMS = {
|
|
48
|
+
type: 'object',
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
required: ['prompt'],
|
|
51
|
+
properties: {
|
|
52
|
+
prompt: { type: 'string', description: 'The task instruction for codex. On first dispatch for a topic, include a compact CONTEXT preamble (goal, repo root, key paths, decisions/constraints; point at paths, do not paste file contents — codex reads the repo itself). For follow-ups on the same topic, use codex_continue with latest=true or sessionId (codex keeps the thread context; state only what is new).' },
|
|
53
|
+
mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'auto follows the DSH plan state; plan = read-only sandbox (no writes), accept-edits = workspace-write. Default auto. An explicit sandboxMode overrides this.' },
|
|
54
|
+
model: { type: 'string', description: 'Optional codex model id (e.g. "minimax/minimax-m3.1-flash:free"). Omit to use the CLI Bridge settings default, or codex\'s own ~/.codex/config.toml model. Run codex_models first; the server rejects unknown ids with HTTP 400.' },
|
|
55
|
+
sandboxMode: { type: 'string', enum: ['read-only', 'workspace-write', 'danger-full-access'], description: 'Optional codex sandbox policy (-s). Overrides the mode-derived default and the settings panel value.' },
|
|
56
|
+
reasoningEffort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh'], description: 'Optional model_reasoning_effort override (-c model_reasoning_effort="…"). Ignored by models that do not support it.' },
|
|
57
|
+
cwd: { type: 'string', description: 'Working directory for codex (-C, and the spawn cwd). Defaults to the DSH workspace root.' },
|
|
58
|
+
addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra writable directories (--add-dir). Ignored on resume (codex exec resume has no --add-dir).' },
|
|
59
|
+
images: { type: 'array', items: { type: 'string' }, description: 'Image files to attach to the prompt (-i).' },
|
|
60
|
+
timeoutSec: { type: 'integer', description: 'Execution timeout seconds (10-3600, default 900).' },
|
|
61
|
+
background: { type: 'boolean', description: 'Defaults to true: dispatch returns a jobId immediately; collect with job_output, watch progress with codex_status. Set false to block for the final answer.' }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const CONTINUE_PARAMS = {
|
|
65
|
+
type: 'object',
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
required: ['prompt'],
|
|
68
|
+
properties: {
|
|
69
|
+
prompt: { type: 'string', description: 'Follow-up instruction for the ongoing codex thread. State only what is new; codex preserves the thread context.' },
|
|
70
|
+
sessionId: { type: 'string', description: 'codex thread id to resume (the thread_id from a prior codex_run result).' },
|
|
71
|
+
latest: { type: 'boolean', description: 'Resume the most recent codex thread in this working directory (exec resume --last) instead of a specific id.' },
|
|
72
|
+
mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'Execution mode; default auto. On resume codex has no -s flag, so the sandbox is applied via -c sandbox_mode="…".' },
|
|
73
|
+
model: { type: 'string', description: 'Optional codex model id.' },
|
|
74
|
+
sandboxMode: { type: 'string', enum: ['read-only', 'workspace-write', 'danger-full-access'], description: 'Optional sandbox policy applied through -c sandbox_mode="…" (resume has no -s).' },
|
|
75
|
+
reasoningEffort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh'], description: 'Optional model_reasoning_effort override.' },
|
|
76
|
+
cwd: { type: 'string', description: 'Working directory (spawn cwd only — resume has no -C).' },
|
|
77
|
+
images: { type: 'array', items: { type: 'string' }, description: 'Image files to attach (-i).' },
|
|
78
|
+
timeoutSec: { type: 'integer', description: 'Execution timeout seconds (10-3600, default 900).' },
|
|
79
|
+
background: { type: 'boolean', description: 'Defaults to true: dispatch returns a jobId immediately; collect with job_output, watch progress with codex_status. Set false to block.' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
|
|
83
|
+
|
|
84
|
+
export default function defineProvider(deps = {}) {
|
|
85
|
+
const env = deps.env || {}
|
|
86
|
+
let host = null
|
|
87
|
+
let settingsRef = { binPath: '', defaultModel: '', sandboxMode: '', approvalPolicy: '' }
|
|
88
|
+
codexSetHost(env, deps.nodeFs || null, deps.nodePath || null)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 取当前生效的偏好设置(三条来源按优先级合并,每次 buildArgv 重读 —— 面板改默认
|
|
92
|
+
* 模型后无需重启即生效):
|
|
93
|
+
* 1) deps.readSettings():MCP 形态(真 Node,每次现读设置文件);
|
|
94
|
+
* 2) host.getSettings('codex'):preset/dynamic 形态(attach 注入);
|
|
95
|
+
* 3) settingsRef:attach 时固化的一次性快照 / 缺省值。
|
|
96
|
+
*/
|
|
97
|
+
function currentSettings() {
|
|
98
|
+
const out = Object.assign({}, settingsRef)
|
|
99
|
+
try {
|
|
100
|
+
if (typeof deps.readSettings === 'function') {
|
|
101
|
+
const r = deps.readSettings()
|
|
102
|
+
if (r && typeof r === 'object') {
|
|
103
|
+
const part = ('partition' in r || 'path' in r || 'home' in r) ? r.partition : r
|
|
104
|
+
if (part && typeof part === 'object') Object.assign(out, part)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (e) { /* 设置读失败 = 用缺省,不影响本次调用 */ }
|
|
108
|
+
try {
|
|
109
|
+
if (host && typeof host.getSettings === 'function') {
|
|
110
|
+
const s = host.getSettings('codex')
|
|
111
|
+
if (s && typeof s === 'object') Object.assign(out, s)
|
|
112
|
+
}
|
|
113
|
+
} catch (e) { /* 同上 */ }
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sandboxFor(mode, args, settings) {
|
|
118
|
+
if (args && args.sandboxMode && SANDBOX_MODES.indexOf(String(args.sandboxMode)) >= 0) return String(args.sandboxMode)
|
|
119
|
+
if (settings && settings.sandboxMode && SANDBOX_MODES.indexOf(String(settings.sandboxMode)) >= 0) return String(settings.sandboxMode)
|
|
120
|
+
return mode === 'plan' ? 'read-only' : 'workspace-write'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const descriptor = {
|
|
124
|
+
id: 'codex', label: 'CODEX', presetId: 'codex-first', defaultBackend: null, defaultMode: 'auto',
|
|
125
|
+
exeName: 'codex', binEnvVars: ['CODEX_BIN', 'CODEX_CLI'], unavailableStatus: 'CODEX_UNAVAILABLE',
|
|
126
|
+
|
|
127
|
+
// 单后端:model_provider/base_url 属于 codex 自身配置,桥不引入第二后端概念。
|
|
128
|
+
backends() { return null },
|
|
129
|
+
normalizeBackend() { return null },
|
|
130
|
+
resolveBackend(args, target, preferred) { return null },
|
|
131
|
+
|
|
132
|
+
// 实测 flag 集(exec 与 exec resume 的可用旗不同 —— 见文件头)。
|
|
133
|
+
buildArgv(exePrefix, args, opts) {
|
|
134
|
+
const o = opts || {}
|
|
135
|
+
const a = args || {}
|
|
136
|
+
const settings = currentSettings()
|
|
137
|
+
const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
|
|
138
|
+
const resume = !!(a.sessionId || a.continueLatest || a.latest === true)
|
|
139
|
+
argv.push('exec')
|
|
140
|
+
if (resume) {
|
|
141
|
+
argv.push('resume')
|
|
142
|
+
if (a.sessionId) argv.push(String(a.sessionId))
|
|
143
|
+
else argv.push('--last')
|
|
144
|
+
}
|
|
145
|
+
// --json 是所有折叠/解析的前提;--skip-git-repo-check 让非 git 目录也能跑。
|
|
146
|
+
argv.push('--json', '--skip-git-repo-check')
|
|
147
|
+
// -C 只有 exec 有;resume 的 cwd 由 spawn 的 cwd 决定(实测 resume 传 -C 直接报错)。
|
|
148
|
+
const cwd = a.cwd ? String(a.cwd) : null
|
|
149
|
+
if (cwd && !resume) argv.push('-C', cwd)
|
|
150
|
+
|
|
151
|
+
let mode = a.mode || 'auto'
|
|
152
|
+
if (mode === 'auto') mode = o.planActive ? 'plan' : 'accept-edits'
|
|
153
|
+
const sandbox = sandboxFor(mode, a, settings)
|
|
154
|
+
if (resume) argv.push('-c', 'sandbox_mode="' + sandbox + '"')
|
|
155
|
+
else argv.push('-s', sandbox)
|
|
156
|
+
|
|
157
|
+
const approval = (settings && settings.approvalPolicy && APPROVAL_POLICIES.indexOf(String(settings.approvalPolicy)) >= 0)
|
|
158
|
+
? String(settings.approvalPolicy)
|
|
159
|
+
: ''
|
|
160
|
+
// exec 没有 -a/--ask-for-approval;等价效果只能经 -c 覆盖(TOML 字符串值)。
|
|
161
|
+
if (approval) argv.push('-c', 'approval_policy="' + approval + '"')
|
|
162
|
+
|
|
163
|
+
const model = a.model || o.defaultModel || settings.defaultModel || ''
|
|
164
|
+
if (model) argv.push('-m', String(model))
|
|
165
|
+
if (a.reasoningEffort && REASONING_EFFORTS.indexOf(String(a.reasoningEffort)) >= 0) {
|
|
166
|
+
argv.push('-c', 'model_reasoning_effort="' + String(a.reasoningEffort) + '"')
|
|
167
|
+
}
|
|
168
|
+
if (!resume && Array.isArray(a.addDirs)) for (const d of a.addDirs) { if (d) argv.push('--add-dir', String(d)) }
|
|
169
|
+
if (Array.isArray(a.images)) for (const f of a.images) { if (f) argv.push('-i', String(f)) }
|
|
170
|
+
// prompt 必须最后(位置参数)。
|
|
171
|
+
argv.push(String(a.prompt))
|
|
172
|
+
const timeoutSec = clampInt(a.timeoutSec, 900, 10, 3600)
|
|
173
|
+
return { argv, mode, timeoutSec }
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// NDJSON(--json)。宽容:坏行跳过;无任何 JSON 行返回 null(交 buildResult 走纯文本)。
|
|
177
|
+
// 终止判据 = turn.completed + 退出码 0;顶层 error / turn.failed 才算致命。
|
|
178
|
+
// ⚠️ item.completed 里 item.type==="error" 只是告警(实测续聊会带),绝不致命。
|
|
179
|
+
parseJson(stdoutText) {
|
|
180
|
+
const trimmed = String(stdoutText || '').trim()
|
|
181
|
+
if (!trimmed) return null
|
|
182
|
+
let sawJson = false
|
|
183
|
+
let threadId = null
|
|
184
|
+
let response = ''
|
|
185
|
+
let usage = null
|
|
186
|
+
let completed = false
|
|
187
|
+
let fatal = null
|
|
188
|
+
let turns = 0
|
|
189
|
+
for (const raw of trimmed.split(/\r?\n/)) {
|
|
190
|
+
const ln = raw.trim()
|
|
191
|
+
if (ln.charAt(0) !== '{') continue
|
|
192
|
+
let ev
|
|
193
|
+
try { ev = JSON.parse(ln) } catch (e) { continue }
|
|
194
|
+
if (!ev || typeof ev !== 'object') continue
|
|
195
|
+
sawJson = true
|
|
196
|
+
const t = ev.type
|
|
197
|
+
if (t === 'thread.started') {
|
|
198
|
+
if (typeof ev.thread_id === 'string') threadId = ev.thread_id
|
|
199
|
+
} else if (t === 'turn.started') {
|
|
200
|
+
turns += 1
|
|
201
|
+
} else if (t === 'turn.completed') {
|
|
202
|
+
completed = true
|
|
203
|
+
if (ev.usage && typeof ev.usage === 'object') usage = ev.usage
|
|
204
|
+
} else if (t === 'turn.failed') {
|
|
205
|
+
const msg = ev.error && typeof ev.error.message === 'string' ? ev.error.message
|
|
206
|
+
: (typeof ev.error === 'string' ? ev.error : 'turn failed')
|
|
207
|
+
fatal = { message: msg }
|
|
208
|
+
} else if (t === 'error') {
|
|
209
|
+
// 顶层 error 事件:message 常常是嵌套 JSON 字符串(实测)。
|
|
210
|
+
fatal = { message: typeof ev.message === 'string' ? ev.message : 'error event' }
|
|
211
|
+
} else if (t === 'item.completed' && ev.item && typeof ev.item === 'object') {
|
|
212
|
+
if (ev.item.type === 'agent_message' && typeof ev.item.text === 'string') response = ev.item.text
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!sawJson) return null
|
|
216
|
+
return {
|
|
217
|
+
type: completed ? 'turn.completed' : (fatal ? 'turn.failed' : 'incomplete'),
|
|
218
|
+
threadId: threadId || null,
|
|
219
|
+
response: String(response || ''),
|
|
220
|
+
usage: usage || null,
|
|
221
|
+
completed: completed,
|
|
222
|
+
error: fatal,
|
|
223
|
+
numTurns: turns || null
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// 统一结果对象(契约 §1)。成功必须同时满足:退出码 0 + 收到 turn.completed +
|
|
228
|
+
// 无顶层错误事件。只看退出码不够(实测 400 时 stdout 仍是合法事件流,退出码 1)。
|
|
229
|
+
buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
|
|
230
|
+
const exitCode = outcome ? outcome.exitCode : null
|
|
231
|
+
const stderr = stderrText ? String(stderrText).slice(-2000) : ''
|
|
232
|
+
const mk = (over) => Object.assign({
|
|
233
|
+
ok: false, status: 'ERROR', response: '', sessionId: null, durationSeconds: null,
|
|
234
|
+
numTurns: null, totalTokens: null, duration: null, exitCode, mode, backend: backend || null, stderr
|
|
235
|
+
}, over)
|
|
236
|
+
const usageOf = (u) => {
|
|
237
|
+
if (!u || typeof u !== 'object') return null
|
|
238
|
+
const i = Number(u.input_tokens) || 0
|
|
239
|
+
const o = Number(u.output_tokens) || 0
|
|
240
|
+
return (i + o) > 0 ? i + o : null
|
|
241
|
+
}
|
|
242
|
+
if (parsed && typeof parsed === 'object') {
|
|
243
|
+
const sessionId = parsed.threadId || null
|
|
244
|
+
const totalTokens = usageOf(parsed.usage)
|
|
245
|
+
if (parsed.error) {
|
|
246
|
+
const msg = String(parsed.error.message || 'turn failed')
|
|
247
|
+
return mk({
|
|
248
|
+
status: 'TURN_FAILED', response: parsed.response || '', sessionId,
|
|
249
|
+
numTurns: parsed.numTurns, totalTokens, errorMessage: msg,
|
|
250
|
+
stderr: (stderr ? stderr + ' ' : '') + msg
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
if (exitCode === 0 && parsed.completed) {
|
|
254
|
+
return {
|
|
255
|
+
ok: true, status: 'SUCCESS', response: parsed.response || '', sessionId,
|
|
256
|
+
durationSeconds: null, numTurns: parsed.numTurns || 1, totalTokens,
|
|
257
|
+
exitCode, mode, backend: backend || null, stderr
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (exitCode === 0) {
|
|
261
|
+
return mk({
|
|
262
|
+
status: 'INCOMPLETE', response: parsed.response || '', sessionId, totalTokens,
|
|
263
|
+
numTurns: parsed.numTurns,
|
|
264
|
+
stderr: (stderr ? stderr + ' ' : '') + 'codex event stream ended without turn.completed (process exited 0). The run may have been interrupted; retry, and check `codex --version` if it persists.'
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
return mk({
|
|
268
|
+
status: exitCode === 2 ? 'BAD_ARGS' : 'ERROR',
|
|
269
|
+
response: parsed.response || '', sessionId, numTurns: parsed.numTurns, totalTokens,
|
|
270
|
+
stderr: (stderr ? stderr + ' ' : '') + 'codex exited ' + exitCode
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
const text = String(stdoutText || '').trim()
|
|
274
|
+
if (exitCode === 0) {
|
|
275
|
+
return {
|
|
276
|
+
ok: true, status: 'SUCCESS', response: text, sessionId: null,
|
|
277
|
+
durationSeconds: null, numTurns: null, totalTokens: null, exitCode, mode,
|
|
278
|
+
backend: backend || null, stderr
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return mk({
|
|
282
|
+
status: exitCode === 2 ? 'BAD_ARGS' : 'ERROR',
|
|
283
|
+
response: '',
|
|
284
|
+
stderr: stderr || ('codex exited ' + exitCode),
|
|
285
|
+
rawStdout: text.slice(-2000)
|
|
286
|
+
})
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// 实时步骤折叠(写 project.current/trail)。事件形态见文件头。
|
|
290
|
+
// item 类型实测:command_execution / reasoning / agent_message / error(告警) /
|
|
291
|
+
// file_change / mcp_tool_call / web_search —— 未知类型也按通用形态记账,绝不吞事件。
|
|
292
|
+
foldStreamEvent(ev, p, summarizeArgs, maxArgLen, maxTrail) {
|
|
293
|
+
if (!ev || typeof ev !== 'object') return
|
|
294
|
+
const t = ev.type
|
|
295
|
+
if (!t) return
|
|
296
|
+
p.updatedAt = Date.now()
|
|
297
|
+
const cap = maxTrail || 12
|
|
298
|
+
const capLen = maxArgLen || 120
|
|
299
|
+
const pushTrail = (entry) => { p.trail.push(entry); if (p.trail.length > cap) p.trail.shift() }
|
|
300
|
+
if (t === 'thread.started') {
|
|
301
|
+
p.current = { tool: 'session', args: { thread_id: String(ev.thread_id || '').slice(0, capLen) }, stepIndex: p.trail.length, since: nowIso() }
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
if (t === 'turn.completed' || t === 'turn.failed') { p.current = null; return }
|
|
305
|
+
if (t === 'error') {
|
|
306
|
+
pushTrail({ stepIndex: p.trail.length, state: 'ERROR', tool: 'error', args: { message: String(ev.message || '').slice(0, capLen) }, at: nowIso() })
|
|
307
|
+
p.current = null
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
if (t !== 'item.started' && t !== 'item.completed') return
|
|
311
|
+
const item = ev.item && typeof ev.item === 'object' ? ev.item : null
|
|
312
|
+
if (!item) return
|
|
313
|
+
const kind = String(item.type || 'item')
|
|
314
|
+
const failed = kind === 'error' || item.status === 'failed' || (typeof item.exit_code === 'number' && item.exit_code !== 0)
|
|
315
|
+
const state = t === 'item.started' ? 'ACTIVE' : (failed ? 'ERROR' : 'DONE')
|
|
316
|
+
let tool = kind
|
|
317
|
+
let args = null
|
|
318
|
+
if (kind === 'command_execution') {
|
|
319
|
+
tool = 'command'
|
|
320
|
+
args = summarizeArgs({ command: item.command })
|
|
321
|
+
} else if (kind === 'agent_message') {
|
|
322
|
+
tool = 'agent_response'
|
|
323
|
+
args = { text_delta: String(item.text || '').slice(0, capLen) }
|
|
324
|
+
} else if (kind === 'reasoning') {
|
|
325
|
+
tool = 'reasoning'
|
|
326
|
+
args = { text_delta: String(item.text || item.summary || '').slice(0, capLen) }
|
|
327
|
+
} else if (kind === 'file_change') {
|
|
328
|
+
tool = 'file_change'
|
|
329
|
+
const changes = Array.isArray(item.changes) ? item.changes.map((c) => (c && (c.path || c.file)) || '').filter(Boolean) : []
|
|
330
|
+
args = summarizeArgs({ paths: changes.slice(0, 6) })
|
|
331
|
+
} else if (kind === 'mcp_tool_call') {
|
|
332
|
+
tool = 'mcp:' + String(item.server || '?') + '/' + String(item.tool || item.name || '?')
|
|
333
|
+
args = summarizeArgs(item.arguments || null)
|
|
334
|
+
} else if (kind === 'web_search') {
|
|
335
|
+
tool = 'web_search'
|
|
336
|
+
args = summarizeArgs({ query: item.query })
|
|
337
|
+
} else if (kind === 'error') {
|
|
338
|
+
tool = 'warning'
|
|
339
|
+
args = { message: String(item.message || '').slice(0, capLen) }
|
|
340
|
+
} else {
|
|
341
|
+
args = summarizeArgs({ type: kind })
|
|
342
|
+
}
|
|
343
|
+
const key = item.id ? String(item.id) : null
|
|
344
|
+
p.codexByItem = p.codexByItem || Object.create(null)
|
|
345
|
+
const prior = key ? p.codexByItem[key] : null
|
|
346
|
+
if (prior) {
|
|
347
|
+
prior.state = state
|
|
348
|
+
if (key) delete p.codexByItem[key]
|
|
349
|
+
if (p.current && p.current.itemKey === key) p.current = null
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
const entry = { stepIndex: p.trail.length, state, tool, args: args || null, at: nowIso() }
|
|
353
|
+
pushTrail(entry)
|
|
354
|
+
if (t === 'item.started') {
|
|
355
|
+
if (key) p.codexByItem[key] = entry
|
|
356
|
+
p.current = { tool, args: args || null, stepIndex: entry.stepIndex, since: nowIso(), itemKey: key }
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// 瞬时错误重试:codex 自己会重试网络,这里只兜「流断了但非限流」这一类。
|
|
361
|
+
isTransientCliError(res) {
|
|
362
|
+
if (!res || res.ok) return false
|
|
363
|
+
if (isLimited(res)) return false
|
|
364
|
+
if (res.status === 'INCOMPLETE') return true
|
|
365
|
+
const hay = String(res.errorMessage || '') + ' ' + String(res.stderr || '')
|
|
366
|
+
return /stream disconnected|connection reset|ECONNRESET|ETIMEDOUT|temporarily unavailable|overloaded|try again/i.test(hay)
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
failureHint(res) {
|
|
370
|
+
if (!res || res.ok) return ''
|
|
371
|
+
const st = String(res.status || '')
|
|
372
|
+
if (st === 'CODEX_UNAVAILABLE') return '[诊断] 未找到 codex 可执行文件:请安装 Codex CLI,或在 CLI Bridge 设置面板填写 codex 可执行文件路径(也可设 CODEX_BIN)。'
|
|
373
|
+
if (st === 'BAD_ARGS') return '[诊断] codex 拒绝了参数(exit 2):请检查 model/sandboxMode 等取值;codex_models 可列出可用模型 id。'
|
|
374
|
+
const hay = String(res.stderr || '') + ' ' + String(res.errorMessage || '') + ' ' + String(res.response || '')
|
|
375
|
+
if (/not logged in|please run.*login|unauthorized|invalid api key|401|403/i.test(hay) && !/rate.?limit|quota|限流|额度/i.test(hay)) {
|
|
376
|
+
return '[诊断] codex 凭据问题(未登录 / API key 无效):请让用户执行 `codex login`,或检查 ~/.codex/config.toml 里 model_providers.<name> 的 base_url 与 experimental_bearer_token。这不是限流。'
|
|
377
|
+
}
|
|
378
|
+
if (/not a valid model|invalid model|model not found|unknown model|unsupported model/i.test(hay)) {
|
|
379
|
+
return '[诊断] 模型 id 不被服务端接受:先用 codex_models(默认 source=live)列出该端点真实可用的 id,再传 model。注意 ~/.codex/models.json 与 model_catalog_json 只是本地目录,不代表服务端可用。'
|
|
380
|
+
}
|
|
381
|
+
if (/not inside a trusted directory|not a git repository|skip-git-repo-check/i.test(hay)) {
|
|
382
|
+
return '[诊断] codex 拒绝了当前工作目录(非 git 仓库):本桥已默认加 --skip-git-repo-check;若仍出现,说明 argv 被外部改写,请检查 buildArgv。'
|
|
383
|
+
}
|
|
384
|
+
if (/approval|sandbox|permission denied|operation not permitted/i.test(hay)) {
|
|
385
|
+
return '[诊断] 沙箱/审批拒绝了操作:非交互 exec 无法弹出审批,请调高 sandboxMode(workspace-write → danger-full-access)或在设置面板改「沙箱策略」,然后重跑。'
|
|
386
|
+
}
|
|
387
|
+
if (/quota|额度|rate.?limit|insufficient|credit|balance|402/i.test(hay)) {
|
|
388
|
+
return '[诊断] 端点额度可能耗尽:换一个(优先 :free)模型,或改用原生工具完成本任务;不要循环重试。'
|
|
389
|
+
}
|
|
390
|
+
return ''
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
isSuccessStatus(st) { return st === 'SUCCESS' },
|
|
394
|
+
|
|
395
|
+
tools: {
|
|
396
|
+
run: { name: 'codex_run', description: 'Dispatch a coding/build/debug/investigation task to the local OpenAI Codex CLI and return its final answer. Prefer this for implementation, edits, refactors, multi-file investigation and debugging in every mode. DSH fully controls codex: it always runs non-interactively (`codex exec --json`, approvals cannot block). Use read-only native tools only for quick lookups and for final build/test verification. In mode=auto the DSH plan state decides the sandbox (plan → read-only, otherwise workspace-write). Runs in the background by default (returns a jobId immediately; collect with job_output, watch progress with codex_status); set background=false to block for the final answer.', inputSchema: RUN_PARAMS },
|
|
397
|
+
continue: { name: 'codex_continue', description: 'Continue an existing codex thread with a follow-up prompt, reusing codex context (`codex exec resume`). Pass sessionId (the thread_id from a prior codex_run result), or set latest=true to resume the most recent thread in this working directory. codex exec resume has no -C/-s, so the sandbox is applied via -c and the working directory comes from the bridge session registry. Same DSH-controlled, no-prompt execution as codex_run.', inputSchema: CONTINUE_PARAMS },
|
|
398
|
+
status: { name: 'codex_status', description: 'Read a live snapshot of what the local codex agent is currently doing. Returns one section per project (working directory): running count, the current step (the command/text being produced, from the --json event stream), the recent step trail (commands, file changes, messages, errors), and the last completed run status + thread id. Optional cwd filters to a single project. Call this to check on an in-flight codex_run/codex_continue without waiting for it to finish.', inputSchema: STATUS_PARAMS }
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
// codex_models:宿主有 spawn/fetch 能力时注册(preset 注入 deps.process / MCP 注入
|
|
402
|
+
// deps.nodeChild;dynamic 沙箱形态省略 —— 与 agy/opencode 同约定)。
|
|
403
|
+
extraTools: (deps.process || deps.nodeChild || typeof deps.fetch === 'function')
|
|
404
|
+
? [codexModelsTool({ env, deps })]
|
|
405
|
+
: [],
|
|
406
|
+
|
|
407
|
+
settings: {
|
|
408
|
+
keys: ['binPath', 'defaultModel', 'sandboxMode', 'approvalPolicy'],
|
|
409
|
+
defaults: () => ({ binPath: '', defaultModel: '', sandboxMode: '', approvalPolicy: '' }),
|
|
410
|
+
normalize: (raw) => {
|
|
411
|
+
const r = raw && typeof raw === 'object' ? raw : {}
|
|
412
|
+
const pick = (v) => (typeof v === 'string' ? String(v).trim() : '')
|
|
413
|
+
const sandbox = pick(r.sandboxMode)
|
|
414
|
+
const approval = pick(r.approvalPolicy)
|
|
415
|
+
return {
|
|
416
|
+
binPath: pick(r.binPath),
|
|
417
|
+
defaultModel: pick(r.defaultModel),
|
|
418
|
+
sandboxMode: SANDBOX_MODES.indexOf(sandbox) >= 0 ? sandbox : '',
|
|
419
|
+
approvalPolicy: APPROVAL_POLICIES.indexOf(approval) >= 0 ? approval : ''
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
// 面板是同步回调,且**不再自动探测**(契约 §3.2):defaultModel 的候选只来自
|
|
423
|
+
// 已缓存的 live 清单(codexCachedModelOptions 纯读,不 spawnSync、不发请求)。
|
|
424
|
+
// 探测由用户点「拉取可用模型」→ settings.refreshModels() 显式触发。
|
|
425
|
+
meta: (env2) => {
|
|
426
|
+
const e = env2 && typeof env2 === 'object' ? env2 : env
|
|
427
|
+
const opts = codexCachedModelOptions(e)
|
|
428
|
+
const exeCandidates = codexExeCandidates(e)
|
|
429
|
+
return {
|
|
430
|
+
titleSuffix: 'Codex',
|
|
431
|
+
fields: [
|
|
432
|
+
{
|
|
433
|
+
key: 'binPath', label: 'codex 可执行文件路径', type: 'select', allowCustom: true,
|
|
434
|
+
options: exeCandidates.map((p) => ({ value: p, label: p })),
|
|
435
|
+
placeholder: '留空 = CODEX_BIN / CODEX_CLI / PATH 的 codex',
|
|
436
|
+
note: '留空即按 env › PATH 顺序解析;填 .mjs/.js 会经 node 启动(测试夹具形态)。'
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
|
|
440
|
+
options: opts.options,
|
|
441
|
+
// 面板据此渲染「手动拉取」按钮(契约 §3.2);meta() 自身永不探测。
|
|
442
|
+
refresh: { label: '拉取可用模型' },
|
|
443
|
+
placeholder: '留空 = 由 ~/.codex/config.toml 的 model 决定',
|
|
444
|
+
note: opts.note
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
key: 'sandboxMode', label: '沙箱策略', type: 'select',
|
|
448
|
+
options: [
|
|
449
|
+
{ value: '', label: '(跟随 mode / codex 自身 config)' },
|
|
450
|
+
{ value: 'read-only', label: 'read-only(只读,plan 模式默认)' },
|
|
451
|
+
{ value: 'workspace-write', label: 'workspace-write(可写工作区,默认)' },
|
|
452
|
+
{ value: 'danger-full-access', label: 'danger-full-access(无沙箱,慎用)' }
|
|
453
|
+
],
|
|
454
|
+
note: '对应 codex 的 -s;续聊(resume)没有 -s,改经 -c sandbox_mode 生效。'
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
key: 'approvalPolicy', label: '审批策略', type: 'select',
|
|
458
|
+
options: [
|
|
459
|
+
{ value: '', label: '(不覆盖,用 codex 自身 config)' },
|
|
460
|
+
{ value: 'never', label: 'never(从不审批,推荐)' },
|
|
461
|
+
{ value: 'on-request', label: 'on-request' },
|
|
462
|
+
{ value: 'on-failure', label: 'on-failure' },
|
|
463
|
+
{ value: 'untrusted', label: 'untrusted' }
|
|
464
|
+
],
|
|
465
|
+
note: '经 -c approval_policy="…" 传入 —— codex exec 没有 -a 旗(实测报 unexpected argument)。非交互场景建议 never。'
|
|
466
|
+
}
|
|
467
|
+
]
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
/**
|
|
471
|
+
* 手动拉取(契约 §3.2):面板「拉取可用模型」按钮 → keystone
|
|
472
|
+
* POST /cli-bridge/settings/models → 这里。这是本 provider 唯一的面板探测入口。
|
|
473
|
+
*
|
|
474
|
+
* 复用 codexLiveModels(工具面同一个实现:GET {base_url}/models + Bearer),不写第二套;
|
|
475
|
+
* 成功时它已把 payload 写回 CODEX_MODEL_CACHE,随后 meta() 直接命中。
|
|
476
|
+
* 铁律:失败 = ok:false + 可操作错误 + 空 ids;绝不返回 {ok:true, ids:[]},
|
|
477
|
+
* 也绝不把 catalog/builtin(本地目录、非权威)混进这份「服务端可用清单」。
|
|
478
|
+
*/
|
|
479
|
+
async refreshModels(settings, env2) {
|
|
480
|
+
const e = (env2 && typeof env2 === 'object') ? env2 : env
|
|
481
|
+
const res = await codexLiveModels(e, {
|
|
482
|
+
refresh: true,
|
|
483
|
+
fetch: deps.fetch,
|
|
484
|
+
timeoutMs: REFRESH_TIMEOUT_MS
|
|
485
|
+
})
|
|
486
|
+
if (!res.ok) {
|
|
487
|
+
return { ok: false, ids: [], source: 'live', error: String(res.error || 'live model list unavailable') + ' ' + REFRESH_HINT }
|
|
488
|
+
}
|
|
489
|
+
const ids = []
|
|
490
|
+
const labels = {}
|
|
491
|
+
for (const m of res.models || []) {
|
|
492
|
+
if (!m || !m.id || ids.indexOf(m.id) >= 0) continue
|
|
493
|
+
ids.push(m.id)
|
|
494
|
+
labels[m.id] = m.free ? m.id + ' [free]' : m.id
|
|
495
|
+
}
|
|
496
|
+
if (!ids.length) {
|
|
497
|
+
// 拿到 2xx 却没有任何可用 id:显式失败,绝不把空清单当成功(本项目 0.2.6~0.2.9 的坑)。
|
|
498
|
+
return { ok: false, ids: [], source: 'live', error: '端点 ' + String(res.url || '') + ' 返回 0 个可用模型 id(响应已解析但没有可用项)。' + REFRESH_HINT }
|
|
499
|
+
}
|
|
500
|
+
return { ok: true, ids, labels, source: 'live' }
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
diagnose: (settings, env2) => {
|
|
504
|
+
const s = settings && typeof settings === 'object' ? settings : {}
|
|
505
|
+
const e = env2 && typeof env2 === 'object' ? env2 : env
|
|
506
|
+
const bits = []
|
|
507
|
+
bits.push(s.binPath ? 'bin=设置:' + s.binPath : 'bin=' + codexExeCandidates(e)[0])
|
|
508
|
+
bits.push(s.defaultModel ? 'model=' + s.defaultModel : 'model=(codex config 自决)')
|
|
509
|
+
const ep = codexModelsEndpoint(e)
|
|
510
|
+
if (ep.ok) {
|
|
511
|
+
const cached = codexCachedLiveModels(e)
|
|
512
|
+
bits.push('endpoint=' + endpointHost(ep.url) + (cached && cached.ok ? ' live=' + cached.count : ' live=未缓存'))
|
|
513
|
+
} else bits.push('endpoint=未配置')
|
|
514
|
+
bits.push('sandbox=' + (s.sandboxMode || 'mode 派生'))
|
|
515
|
+
bits.push('approval=' + (s.approvalPolicy || 'codex config'))
|
|
516
|
+
const cfg = codexReadConfig(e)
|
|
517
|
+
if (cfg.model) bits.push('cfg.model=' + cfg.model)
|
|
518
|
+
bits.push('home=' + (codexHomeDir(e) || '?'))
|
|
519
|
+
return bits.join(' · ')
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
mcp: { version: '0.2.8' }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 三形态共用的工具逻辑(kit 的 coreExecute 负责 jobs/超时/回退/瞬时重试)。
|
|
527
|
+
descriptor.runToolLogic = async function (args, exec) {
|
|
528
|
+
const a = args || {}
|
|
529
|
+
const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
|
|
530
|
+
return this.runner.coreExecute(normalized, exec)
|
|
531
|
+
}
|
|
532
|
+
descriptor.continueToolLogic = async function (args, exec) {
|
|
533
|
+
const a = args || {}
|
|
534
|
+
const mapped = {
|
|
535
|
+
prompt: a.prompt, model: a.model, mode: a.mode, sandboxMode: a.sandboxMode,
|
|
536
|
+
reasoningEffort: a.reasoningEffort, cwd: a.cwd, images: a.images,
|
|
537
|
+
timeoutSec: a.timeoutSec,
|
|
538
|
+
background: a.background === undefined ? true : a.background
|
|
539
|
+
}
|
|
540
|
+
if (a.sessionId) mapped.sessionId = String(a.sessionId)
|
|
541
|
+
else if (a.latest) mapped.continueLatest = true
|
|
542
|
+
return this.runner.coreExecute(mapped, exec)
|
|
543
|
+
}
|
|
544
|
+
descriptor.statusToolLogic = function (args) {
|
|
545
|
+
const a = args || {}
|
|
546
|
+
const snap = this.engine.statusSnapshot()
|
|
547
|
+
if (a.cwd) {
|
|
548
|
+
const key = String(a.cwd)
|
|
549
|
+
snap.projects = snap.projects.filter((p) => p.cwd === key)
|
|
550
|
+
const g = snap.projects[0]
|
|
551
|
+
if (g) {
|
|
552
|
+
Object.assign(snap, {
|
|
553
|
+
state: g.state, running: g.running, current: g.current, trail: g.trail,
|
|
554
|
+
lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId,
|
|
555
|
+
lastBackend: g.lastBackend, fallbackActive: g.fallbackActive, updatedAt: g.updatedAt
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return snap
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// MCP/测试形态复用同一套模型读取(与 extraTools 同源,避免两处实现漂移)。
|
|
563
|
+
descriptor.listModels = async function (args) {
|
|
564
|
+
return codexModelsTool({ env, deps }).logic(args || {})
|
|
565
|
+
}
|
|
566
|
+
descriptor.liveModels = codexLiveModels
|
|
567
|
+
descriptor.catalogModels = codexCatalogModels
|
|
568
|
+
descriptor.builtinModels = codexBuiltinModels
|
|
569
|
+
|
|
570
|
+
descriptor.attach = function (hostOpts) {
|
|
571
|
+
host = hostOpts || {}
|
|
572
|
+
codexSetHost(env, deps.nodeFs || null, deps.nodePath || null)
|
|
573
|
+
try {
|
|
574
|
+
if (typeof host.getSettings === 'function') {
|
|
575
|
+
const s = host.getSettings('codex')
|
|
576
|
+
if (s && typeof s === 'object') settingsRef = descriptor.settings.normalize(s)
|
|
577
|
+
}
|
|
578
|
+
} catch (e) { /* 设置缺省不影响装配 */ }
|
|
579
|
+
this.engine = createStatusEngine({ provider: this, publish: host.publish })
|
|
580
|
+
const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
|
|
581
|
+
this.runner = runner
|
|
582
|
+
return runner
|
|
583
|
+
}
|
|
584
|
+
return descriptor
|
|
585
|
+
}
|