@xyagent/cli 0.0.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/README.md +860 -0
- package/bin/agentlink +8585 -0
- package/bin/agentlink-agent +7 -0
- package/bin/agentlink-mcp-stdio +173 -0
- package/docs/LOGGING.md +159 -0
- package/package.json +38 -0
- package/src/core.mjs +320 -0
- package/src/tunnel.mjs +291 -0
- package/src/tunnel_proxy.mjs +913 -0
- package/src/tunnel_service.mjs +699 -0
- package/src/tunnel_state.mjs +301 -0
- package/src/unified_producer.mjs +259 -0
- package/src-ext/README.md +75 -0
- package/src-ext/bin.mjs +128 -0
- package/src-ext/commands/agent.mjs +66 -0
- package/src-ext/commands/help.mjs +31 -0
- package/src-ext/commands/pair.mjs +455 -0
- package/src-ext/commands/reload.mjs +78 -0
- package/src-ext/commands/service.mjs +64 -0
- package/src-ext/core/accountResolver.mjs +137 -0
- package/src-ext/core/activeRuns.mjs +76 -0
- package/src-ext/core/agentlinkToolsBootstrap.mjs +75 -0
- package/src-ext/core/agentlinkToolsRegistry.mjs +323 -0
- package/src-ext/core/agentlinkToolsServer.mjs +109 -0
- package/src-ext/core/approvalGateway.mjs +132 -0
- package/src-ext/core/attachAuditLog.mjs +79 -0
- package/src-ext/core/attachDenylist.mjs +128 -0
- package/src-ext/core/autoTunnelDetector.mjs +98 -0
- package/src-ext/core/autoTunnelGateway.mjs +393 -0
- package/src-ext/core/autoTunnelWiring.mjs +184 -0
- package/src-ext/core/boundedMap.mjs +98 -0
- package/src-ext/core/bridgeAttachAutoScanner.mjs +237 -0
- package/src-ext/core/bridgeHooks.mjs +168 -0
- package/src-ext/core/bridgeSelfUninstall.mjs +133 -0
- package/src-ext/core/claudeTrust.mjs +74 -0
- package/src-ext/core/codexTrust.mjs +77 -0
- package/src-ext/core/cosUploadClient.mjs +326 -0
- package/src-ext/core/createWorkspace.mjs +170 -0
- package/src-ext/core/daemonRestart.mjs +49 -0
- package/src-ext/core/defaultWorkspace.mjs +65 -0
- package/src-ext/core/ensureGitRepo.mjs +57 -0
- package/src-ext/core/hermesProfile.mjs +75 -0
- package/src-ext/core/interruptGateway.mjs +119 -0
- package/src-ext/core/interruptRunListener.mjs +106 -0
- package/src-ext/core/mcpRuntimeFanout.mjs +236 -0
- package/src-ext/core/mcpStdioClient.mjs +316 -0
- package/src-ext/core/openclawGatewayClient.mjs +230 -0
- package/src-ext/core/pairCodeClient.mjs +331 -0
- package/src-ext/core/pairInventory.mjs +59 -0
- package/src-ext/core/pathExpand.mjs +34 -0
- package/src-ext/core/preprocessAttachments.mjs +113 -0
- package/src-ext/core/relayWorker.mjs +284 -0
- package/src-ext/core/relayWorkerHttp.mjs +58 -0
- package/src-ext/core/scanWorkspaces.mjs +842 -0
- package/src-ext/core/sessionSlot.mjs +70 -0
- package/src-ext/core/stateMachine.mjs +85 -0
- package/src-ext/core/toolsServerLifecycle.mjs +169 -0
- package/src-ext/core/unifiedDispatchHandler.mjs +439 -0
- package/src-ext/core/usageReporter.mjs +207 -0
- package/src-ext/openclaw-plugin/envelope-builder.cjs +619 -0
- package/src-ext/openclaw-plugin/index.cjs +1919 -0
- package/src-ext/openclaw-plugin/openclaw.plugin.json +20 -0
- package/src-ext/openclaw-plugin/package.json +12 -0
- package/src-ext/openclaw-plugin/relay-defaults.json +3 -0
- package/src-ext/openclaw-plugin/todoTranslatorUtils.cjs +101 -0
- package/src-ext/runtime/_shared/cliExec.mjs +138 -0
- package/src-ext/runtime/_shared/cosTextRefs.mjs +120 -0
- package/src-ext/runtime/_shared/fetchAndBase64Encode.mjs +52 -0
- package/src-ext/runtime/_shared/relayObjectToBlock.mjs +307 -0
- package/src-ext/runtime/_shared/sessionPicker.mjs +418 -0
- package/src-ext/runtime/_shared/slashCommandRouter.mjs +1346 -0
- package/src-ext/runtime/_shared/todoTranslatorUtils.mjs +94 -0
- package/src-ext/runtime/claude/askUserQuestionTranslator.mjs +241 -0
- package/src-ext/runtime/claude/customCommandsCollector.mjs +132 -0
- package/src-ext/runtime/claude/handleRequest.mjs +1418 -0
- package/src-ext/runtime/claude/index.mjs +160 -0
- package/src-ext/runtime/claude/internalCommandNames.mjs +58 -0
- package/src-ext/runtime/claude/launcher.mjs +313 -0
- package/src-ext/runtime/claude/mcpConfigAdapter.mjs +174 -0
- package/src-ext/runtime/claude/mcpConfigWriter.mjs +57 -0
- package/src-ext/runtime/claude/modelContextWindow.mjs +30 -0
- package/src-ext/runtime/claude/modelScanner.mjs +257 -0
- package/src-ext/runtime/claude/permissionInterruptBlock.mjs +86 -0
- package/src-ext/runtime/claude/permissionPromptServer.mjs +170 -0
- package/src-ext/runtime/claude/permissionPromptServerWorker.mjs +214 -0
- package/src-ext/runtime/claude/preflight.mjs +62 -0
- package/src-ext/runtime/claude/stdoutParser.mjs +365 -0
- package/src-ext/runtime/claude/todoTranslator.mjs +66 -0
- package/src-ext/runtime/claude/usageProbe.mjs +576 -0
- package/src-ext/runtime/claude/usageProbeStrip.mjs +14 -0
- package/src-ext/runtime/codex/codexCustomCommandsCollector.mjs +87 -0
- package/src-ext/runtime/codex/elicitationHandler.mjs +91 -0
- package/src-ext/runtime/codex/eventMapper.mjs +154 -0
- package/src-ext/runtime/codex/handleRequest.mjs +1152 -0
- package/src-ext/runtime/codex/index.mjs +142 -0
- package/src-ext/runtime/codex/mcpConfigAdapter.mjs +155 -0
- package/src-ext/runtime/codex/modelScanner.mjs +305 -0
- package/src-ext/runtime/codex/planNotificationHandler.mjs +61 -0
- package/src-ext/runtime/codex/preflight.mjs +67 -0
- package/src-ext/runtime/hermes/commandBackend.mjs +117 -0
- package/src-ext/runtime/hermes/envSetup.mjs +229 -0
- package/src-ext/runtime/hermes/gatewayManager.mjs +625 -0
- package/src-ext/runtime/hermes/handleRequest.mjs +1257 -0
- package/src-ext/runtime/hermes/httpBackend.mjs +219 -0
- package/src-ext/runtime/hermes/index.mjs +196 -0
- package/src-ext/runtime/hermes/mcpConfigAdapter.mjs +189 -0
- package/src-ext/runtime/hermes/preflight.mjs +359 -0
- package/src-ext/runtime/hermes/responsesBackend.mjs +276 -0
- package/src-ext/runtime/hermes/sessionStore.mjs +78 -0
- package/src-ext/runtime/hermes/todoTranslator.mjs +90 -0
- package/src-ext/runtime/hermes/workspaceRpc.mjs +124 -0
- package/src-ext/runtime/openclaw/buildOpenclawDaemonInput.mjs +143 -0
- package/src-ext/runtime/openclaw/mcpConfigAdapter.mjs +91 -0
- package/src-ext/runtime/openclaw/openclawConfig.mjs +145 -0
- package/src-ext/runtime/picoclaw/constants.mjs +39 -0
- package/src-ext/runtime/picoclaw/handleRequest.mjs +289 -0
- package/src-ext/runtime/picoclaw/index.mjs +314 -0
- package/src-ext/runtime/picoclaw/pairFlow.mjs +224 -0
- package/src-ext/runtime/picoclaw/state.mjs +78 -0
- package/src-ext/runtime/picoclaw/todoTranslator.mjs +67 -0
- package/src-ext/runtime/picoclaw/translator.mjs +272 -0
- package/src-ext/runtime/picoclaw/wsClient.mjs +129 -0
- package/src-ext/service/serviceManager.mjs +521 -0
- package/src-shared/README.md +44 -0
- package/src-shared/chat_push_normalizer.mjs +113 -0
- package/src-shared/correlation.mjs +25 -0
- package/src-shared/envelope_builder.mjs +612 -0
- package/src-shared/logger.mjs +167 -0
- package/src-shared/relay-defaults.json +3 -0
- package/src-shared/relay_revoke.mjs +46 -0
- package/src-shared/vendor/README.md +60 -0
- package/src-shared/vendor/smol-toml/LICENSE +24 -0
- package/src-shared/vendor/smol-toml/README.md +236 -0
- package/src-shared/vendor/smol-toml/VENDORED.md +37 -0
- package/src-shared/vendor/smol-toml/dist/date.d.ts +41 -0
- package/src-shared/vendor/smol-toml/dist/date.js +127 -0
- package/src-shared/vendor/smol-toml/dist/error.d.ts +38 -0
- package/src-shared/vendor/smol-toml/dist/error.js +63 -0
- package/src-shared/vendor/smol-toml/dist/extract.d.ts +30 -0
- package/src-shared/vendor/smol-toml/dist/extract.js +109 -0
- package/src-shared/vendor/smol-toml/dist/index.cjs +900 -0
- package/src-shared/vendor/smol-toml/dist/index.d.ts +43 -0
- package/src-shared/vendor/smol-toml/dist/index.js +33 -0
- package/src-shared/vendor/smol-toml/dist/parse.d.ts +37 -0
- package/src-shared/vendor/smol-toml/dist/parse.js +148 -0
- package/src-shared/vendor/smol-toml/dist/primitive.d.ts +31 -0
- package/src-shared/vendor/smol-toml/dist/primitive.js +178 -0
- package/src-shared/vendor/smol-toml/dist/stringify.d.ts +31 -0
- package/src-shared/vendor/smol-toml/dist/stringify.js +163 -0
- package/src-shared/vendor/smol-toml/dist/struct.d.ts +32 -0
- package/src-shared/vendor/smol-toml/dist/struct.js +194 -0
- package/src-shared/vendor/smol-toml/dist/util.d.ts +42 -0
- package/src-shared/vendor/smol-toml/dist/util.js +100 -0
- package/src-shared/vendor/smol-toml/package.json +54 -0
- package/src-shared/vendor/yaml/LICENSE +13 -0
- package/src-shared/vendor/yaml/README.md +155 -0
- package/src-shared/vendor/yaml/VENDORED.md +29 -0
- package/src-shared/vendor/yaml/bin.mjs +11 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-collection.js +88 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-doc.js +42 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-node.js +92 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-scalar.js +80 -0
- package/src-shared/vendor/yaml/browser/dist/compose/composer.js +217 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-map.js +113 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-seq.js +47 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-end.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-collection.js +203 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-scalar.js +223 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-props.js +148 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-contains-newline.js +34 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-empty-scalar-position.js +27 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-map-includes.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/doc/Document.js +334 -0
- package/src-shared/vendor/yaml/browser/dist/doc/anchors.js +72 -0
- package/src-shared/vendor/yaml/browser/dist/doc/applyReviver.js +55 -0
- package/src-shared/vendor/yaml/browser/dist/doc/createNode.js +89 -0
- package/src-shared/vendor/yaml/browser/dist/doc/directives.js +176 -0
- package/src-shared/vendor/yaml/browser/dist/errors.js +57 -0
- package/src-shared/vendor/yaml/browser/dist/index.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/log.js +14 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Alias.js +101 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Collection.js +147 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Node.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Pair.js +36 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Scalar.js +24 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/YAMLMap.js +144 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/addPairToJSMap.js +104 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/identity.js +36 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/toJS.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-scalar.js +214 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-stringify.js +61 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-visit.js +97 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst.js +98 -0
- package/src-shared/vendor/yaml/browser/dist/parse/lexer.js +717 -0
- package/src-shared/vendor/yaml/browser/dist/parse/line-counter.js +39 -0
- package/src-shared/vendor/yaml/browser/dist/parse/parser.js +954 -0
- package/src-shared/vendor/yaml/browser/dist/public-api.js +99 -0
- package/src-shared/vendor/yaml/browser/dist/schema/Schema.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/map.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/null.js +15 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/seq.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/string.js +14 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/bool.js +19 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/float.js +43 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/int.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/schema.js +23 -0
- package/src-shared/vendor/yaml/browser/dist/schema/json/schema.js +62 -0
- package/src-shared/vendor/yaml/browser/dist/schema/tags.js +83 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/binary.js +66 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/schema.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringify.js +124 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyCollection.js +143 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyComment.js +20 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyNumber.js +24 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyPair.js +150 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyString.js +328 -0
- package/src-shared/vendor/yaml/browser/dist/util.js +11 -0
- package/src-shared/vendor/yaml/browser/dist/visit.js +233 -0
- package/src-shared/vendor/yaml/browser/index.js +5 -0
- package/src-shared/vendor/yaml/browser/package.json +3 -0
- package/src-shared/vendor/yaml/dist/cli.d.ts +8 -0
- package/src-shared/vendor/yaml/dist/cli.mjs +199 -0
- package/src-shared/vendor/yaml/dist/compose/compose-collection.d.ts +11 -0
- package/src-shared/vendor/yaml/dist/compose/compose-collection.js +90 -0
- package/src-shared/vendor/yaml/dist/compose/compose-doc.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/compose/compose-doc.js +44 -0
- package/src-shared/vendor/yaml/dist/compose/compose-node.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/compose/compose-node.js +95 -0
- package/src-shared/vendor/yaml/dist/compose/compose-scalar.d.ts +5 -0
- package/src-shared/vendor/yaml/dist/compose/compose-scalar.js +82 -0
- package/src-shared/vendor/yaml/dist/compose/composer.d.ts +62 -0
- package/src-shared/vendor/yaml/dist/compose/composer.js +221 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-map.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.js +200 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-end.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-end.js +39 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.js +205 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-props.d.ts +23 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-props.js +150 -0
- package/src-shared/vendor/yaml/dist/compose/util-contains-newline.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/compose/util-contains-newline.js +36 -0
- package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.js +29 -0
- package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.js +17 -0
- package/src-shared/vendor/yaml/dist/compose/util-map-includes.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/compose/util-map-includes.js +19 -0
- package/src-shared/vendor/yaml/dist/doc/Document.d.ts +141 -0
- package/src-shared/vendor/yaml/dist/doc/Document.js +336 -0
- package/src-shared/vendor/yaml/dist/doc/anchors.d.ts +24 -0
- package/src-shared/vendor/yaml/dist/doc/anchors.js +77 -0
- package/src-shared/vendor/yaml/dist/doc/applyReviver.d.ts +9 -0
- package/src-shared/vendor/yaml/dist/doc/applyReviver.js +57 -0
- package/src-shared/vendor/yaml/dist/doc/createNode.d.ts +17 -0
- package/src-shared/vendor/yaml/dist/doc/createNode.js +91 -0
- package/src-shared/vendor/yaml/dist/doc/directives.d.ts +49 -0
- package/src-shared/vendor/yaml/dist/doc/directives.js +178 -0
- package/src-shared/vendor/yaml/dist/errors.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/errors.js +62 -0
- package/src-shared/vendor/yaml/dist/index.d.ts +22 -0
- package/src-shared/vendor/yaml/dist/index.js +50 -0
- package/src-shared/vendor/yaml/dist/log.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/log.js +17 -0
- package/src-shared/vendor/yaml/dist/nodes/Alias.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/nodes/Alias.js +103 -0
- package/src-shared/vendor/yaml/dist/nodes/Collection.d.ts +73 -0
- package/src-shared/vendor/yaml/dist/nodes/Collection.js +151 -0
- package/src-shared/vendor/yaml/dist/nodes/Node.d.ts +47 -0
- package/src-shared/vendor/yaml/dist/nodes/Node.js +40 -0
- package/src-shared/vendor/yaml/dist/nodes/Pair.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/nodes/Pair.js +39 -0
- package/src-shared/vendor/yaml/dist/nodes/Scalar.d.ts +42 -0
- package/src-shared/vendor/yaml/dist/nodes/Scalar.js +27 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLMap.d.ts +53 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLMap.js +147 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.js +115 -0
- package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.js +106 -0
- package/src-shared/vendor/yaml/dist/nodes/identity.d.ts +23 -0
- package/src-shared/vendor/yaml/dist/nodes/identity.js +53 -0
- package/src-shared/vendor/yaml/dist/nodes/toJS.d.ts +27 -0
- package/src-shared/vendor/yaml/dist/nodes/toJS.js +39 -0
- package/src-shared/vendor/yaml/dist/options.d.ts +338 -0
- package/src-shared/vendor/yaml/dist/parse/cst-scalar.d.ts +64 -0
- package/src-shared/vendor/yaml/dist/parse/cst-scalar.js +218 -0
- package/src-shared/vendor/yaml/dist/parse/cst-stringify.d.ts +8 -0
- package/src-shared/vendor/yaml/dist/parse/cst-stringify.js +63 -0
- package/src-shared/vendor/yaml/dist/parse/cst-visit.d.ts +39 -0
- package/src-shared/vendor/yaml/dist/parse/cst-visit.js +99 -0
- package/src-shared/vendor/yaml/dist/parse/cst.d.ts +108 -0
- package/src-shared/vendor/yaml/dist/parse/cst.js +112 -0
- package/src-shared/vendor/yaml/dist/parse/lexer.d.ts +87 -0
- package/src-shared/vendor/yaml/dist/parse/lexer.js +719 -0
- package/src-shared/vendor/yaml/dist/parse/line-counter.d.ts +22 -0
- package/src-shared/vendor/yaml/dist/parse/line-counter.js +41 -0
- package/src-shared/vendor/yaml/dist/parse/parser.d.ts +84 -0
- package/src-shared/vendor/yaml/dist/parse/parser.js +958 -0
- package/src-shared/vendor/yaml/dist/public-api.d.ts +43 -0
- package/src-shared/vendor/yaml/dist/public-api.js +104 -0
- package/src-shared/vendor/yaml/dist/schema/Schema.d.ts +18 -0
- package/src-shared/vendor/yaml/dist/schema/Schema.js +40 -0
- package/src-shared/vendor/yaml/dist/schema/common/map.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/map.js +19 -0
- package/src-shared/vendor/yaml/dist/schema/common/null.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/common/null.js +17 -0
- package/src-shared/vendor/yaml/dist/schema/common/seq.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/seq.js +19 -0
- package/src-shared/vendor/yaml/dist/schema/common/string.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/string.js +16 -0
- package/src-shared/vendor/yaml/dist/schema/core/bool.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/bool.js +21 -0
- package/src-shared/vendor/yaml/dist/schema/core/float.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/float.js +47 -0
- package/src-shared/vendor/yaml/dist/schema/core/int.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/int.js +42 -0
- package/src-shared/vendor/yaml/dist/schema/core/schema.d.ts +1 -0
- package/src-shared/vendor/yaml/dist/schema/core/schema.js +25 -0
- package/src-shared/vendor/yaml/dist/schema/json/schema.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/json/schema.js +64 -0
- package/src-shared/vendor/yaml/dist/schema/json-schema.d.ts +69 -0
- package/src-shared/vendor/yaml/dist/schema/tags.d.ts +40 -0
- package/src-shared/vendor/yaml/dist/schema/tags.js +86 -0
- package/src-shared/vendor/yaml/dist/schema/types.d.ts +90 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.js +68 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.js +29 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.js +50 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.js +76 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.js +77 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.js +96 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
- package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
- package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.js +151 -0
- package/src-shared/vendor/yaml/dist/stringify/stringify.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/stringify/stringify.js +127 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.js +145 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyComment.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyComment.js +24 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.js +87 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.js +26 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyPair.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyPair.js +152 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyString.d.ts +9 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyString.js +330 -0
- package/src-shared/vendor/yaml/dist/test-events.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/test-events.js +134 -0
- package/src-shared/vendor/yaml/dist/util.d.ts +12 -0
- package/src-shared/vendor/yaml/dist/util.js +28 -0
- package/src-shared/vendor/yaml/dist/visit.d.ts +102 -0
- package/src-shared/vendor/yaml/dist/visit.js +236 -0
- package/src-shared/vendor/yaml/package.json +96 -0
- package/src-shared/vendor/yaml/util.js +2 -0
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handleRequest.mjs — codex runtime D-C2.2
|
|
3
|
+
*
|
|
4
|
+
* 把 codex handleRequest 逻辑提取为独立模块,以便单元测试。
|
|
5
|
+
*
|
|
6
|
+
* createCodexHandleRequest({ pre, cwd, log, usage, shuttingDownRef, dispatchHandlerOptions })
|
|
7
|
+
* → async (request, ctx) => void
|
|
8
|
+
*
|
|
9
|
+
* 路由:
|
|
10
|
+
* 1. request.__kind == "agent.run.dispatch" → unifiedDispatchHandler(返回 true)→ 结束
|
|
11
|
+
* 2. 否则 → 原有 MCP stdio 旧路径
|
|
12
|
+
*
|
|
13
|
+
* dispatchHandlerOptions:
|
|
14
|
+
* relayUrl string relay 服务地址
|
|
15
|
+
* bridgeToken string bridge 鉴权 token
|
|
16
|
+
* producerFactory? Function 注入 producer(测试用)
|
|
17
|
+
* openclawInvoke? Function 注入 openclawInvoke(测试用;若未注入则使用默认)
|
|
18
|
+
* _mcpClientFactory? Function 注入 MCP 客户端工厂(测试用,覆盖旧路径)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createUnifiedDispatchHandler } from "../../core/unifiedDispatchHandler.mjs";
|
|
22
|
+
import { collectCodexCustomCommands as _defaultCollectCodexCustomCommands } from "./codexCustomCommandsCollector.mjs";
|
|
23
|
+
import { scanWorkspacesByRuntime } from "../../core/scanWorkspaces.mjs";
|
|
24
|
+
import { createMcpStdioClient } from "../../core/mcpStdioClient.mjs";
|
|
25
|
+
import { createActiveRunRegistry } from "../../core/activeRuns.mjs";
|
|
26
|
+
import { BoundedMap } from "../../core/boundedMap.mjs";
|
|
27
|
+
import { expandHome } from "../../core/pathExpand.mjs";
|
|
28
|
+
import { createWorkspace as _defaultCreateWorkspace } from "../../core/createWorkspace.mjs";
|
|
29
|
+
import { ensureDefaultWorkspace } from "../../core/defaultWorkspace.mjs";
|
|
30
|
+
import { mapCodexMsg } from "./eventMapper.mjs";
|
|
31
|
+
import { createCodexPlanHandler } from "./planNotificationHandler.mjs";
|
|
32
|
+
import {
|
|
33
|
+
classifySlashCommand,
|
|
34
|
+
emitAssistantText,
|
|
35
|
+
} from "../_shared/slashCommandRouter.mjs";
|
|
36
|
+
import { createInterruptGateway } from "../../core/interruptGateway.mjs";
|
|
37
|
+
import { createApprovalGateway } from "../../core/approvalGateway.mjs";
|
|
38
|
+
import { attachElicitationHandler } from "./elicitationHandler.mjs";
|
|
39
|
+
import { writeCodexTrust } from "../../core/codexTrust.mjs";
|
|
40
|
+
import { waitForRunResumed } from "../../core/interruptRunListener.mjs";
|
|
41
|
+
import { listSessionsForRuntime, buildSessionLabel, buildSessionSubtitle } from "../_shared/sessionPicker.mjs";
|
|
42
|
+
import {
|
|
43
|
+
buildRunStarted,
|
|
44
|
+
buildRunCompleted,
|
|
45
|
+
buildRunPaused,
|
|
46
|
+
buildBlockStarted,
|
|
47
|
+
buildBlockCompleted,
|
|
48
|
+
newBlockId,
|
|
49
|
+
} from "../../../src-shared/envelope_builder.mjs";
|
|
50
|
+
|
|
51
|
+
const TURN_HARD_TIMEOUT_MS = 10 * 60_000;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* codex notification msg 翻译为 onChunk 期望格式。
|
|
55
|
+
* 翻译规则:
|
|
56
|
+
* agent_message_delta → response.output_text.delta
|
|
57
|
+
* agent_reasoning(_delta|_section_break|_raw_content)
|
|
58
|
+
* → response.reasoning_summary_text.delta
|
|
59
|
+
* exec_command_begin → response.tool_use.start + completed (一次性,因 codex
|
|
60
|
+
* 的 command 在 begin 事件就有完整 input)
|
|
61
|
+
* exec_command_end → response.tool_result
|
|
62
|
+
* task_complete/task_completed/... → response.completed
|
|
63
|
+
* error → response.failed
|
|
64
|
+
* 其他 → null (忽略)
|
|
65
|
+
*
|
|
66
|
+
* codex 没有 subagent 概念 (单一 agent + tool calls),所以无 parent_tool_use_id。
|
|
67
|
+
*
|
|
68
|
+
* 返回单个 chunk 或一组 chunk 数组——为支持 exec_command_begin 一次产 start+completed
|
|
69
|
+
* 两个事件,签名兼容数组返回。
|
|
70
|
+
*/
|
|
71
|
+
export function translateCodexMsgToChunk(msg) {
|
|
72
|
+
if (!msg || typeof msg.type !== "string") return null;
|
|
73
|
+
switch (msg.type) {
|
|
74
|
+
case "agent_message_content_delta":
|
|
75
|
+
// codex 0.130.0+: streaming text fragments arrive under msg.delta (single-token chars).
|
|
76
|
+
// 53 fragments per ~10s turn (实测), 必须接;否则用户看不到流式打字。
|
|
77
|
+
return { event: "response.output_text.delta", delta: String(msg.delta ?? "") };
|
|
78
|
+
case "agent_message_delta":
|
|
79
|
+
// Legacy event name (codex <0.130). msg.content 是完整文本。
|
|
80
|
+
return { event: "response.output_text.delta", delta: String(msg.content || "") };
|
|
81
|
+
case "agent_message":
|
|
82
|
+
// 完成态汇总 — content_delta 已流过整段, 此处不重复发, 否则 app 会看到双倍文本。
|
|
83
|
+
// 仅在 phase 标识为 "final" 时再考虑 (codex 默认 phase="commentary")
|
|
84
|
+
return null;
|
|
85
|
+
case "agent_reasoning":
|
|
86
|
+
case "agent_reasoning_delta":
|
|
87
|
+
case "agent_reasoning_section_break":
|
|
88
|
+
case "agent_reasoning_raw_content": {
|
|
89
|
+
// 字段在 codex 协议里可能是 msg.text 或 msg.content(参考 eventMapper.mjs:44-46)
|
|
90
|
+
const text = typeof msg.text === "string"
|
|
91
|
+
? msg.text
|
|
92
|
+
: (typeof msg.content === "string" ? msg.content : "");
|
|
93
|
+
if (!text) return null;
|
|
94
|
+
return { event: "response.reasoning_summary_text.delta", delta: text };
|
|
95
|
+
}
|
|
96
|
+
case "exec_command_begin": {
|
|
97
|
+
const callId = String(msg.call_id || "");
|
|
98
|
+
const input = {
|
|
99
|
+
command: msg.command || "",
|
|
100
|
+
...(msg.cwd ? { cwd: msg.cwd } : {}),
|
|
101
|
+
};
|
|
102
|
+
return [
|
|
103
|
+
{ event: "response.tool_use.start", call_id: callId, name: "bash", input },
|
|
104
|
+
{ event: "response.tool_use.completed", call_id: callId, name: "bash", input },
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
case "exec_command_end": {
|
|
108
|
+
const callId = String(msg.call_id || "");
|
|
109
|
+
const exitCode = typeof msg.exit_code === "number" ? msg.exit_code : null;
|
|
110
|
+
const stdout = typeof msg.stdout === "string" ? msg.stdout : "";
|
|
111
|
+
const stderr = typeof msg.stderr === "string" ? msg.stderr : "";
|
|
112
|
+
const isError = exitCode != null && exitCode !== 0;
|
|
113
|
+
const summaryParts = [];
|
|
114
|
+
if (stdout) summaryParts.push(stdout.slice(-1500));
|
|
115
|
+
if (stderr) summaryParts.push(`STDERR:\n${stderr.slice(-500)}`);
|
|
116
|
+
return {
|
|
117
|
+
event: "response.tool_result",
|
|
118
|
+
call_id: callId,
|
|
119
|
+
output: [{ type: "text", format: "plain", text: summaryParts.join("\n") }],
|
|
120
|
+
is_error: isError,
|
|
121
|
+
summary: exitCode != null ? `exit=${exitCode}` : undefined,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case "task_complete":
|
|
125
|
+
case "task_completed":
|
|
126
|
+
case "session_ended":
|
|
127
|
+
case "shutdown":
|
|
128
|
+
return { event: "response.completed" };
|
|
129
|
+
case "error":
|
|
130
|
+
return {
|
|
131
|
+
event: "response.failed",
|
|
132
|
+
error: { message: String(msg.message || msg.error || "codex error") },
|
|
133
|
+
};
|
|
134
|
+
default:
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {{
|
|
141
|
+
* pre: { ok: boolean, mcpSubcommand: string },
|
|
142
|
+
* cwd: string,
|
|
143
|
+
* log: { info, warn, error, child? },
|
|
144
|
+
* usage: { addCost, schedulePublish },
|
|
145
|
+
* shuttingDownRef: { current: boolean },
|
|
146
|
+
* dispatchHandlerOptions: {
|
|
147
|
+
* relayUrl: string,
|
|
148
|
+
* bridgeToken: string,
|
|
149
|
+
* producerFactory?: Function,
|
|
150
|
+
* openclawInvoke?: Function,
|
|
151
|
+
* _mcpClientFactory?: Function, // 仅测试用
|
|
152
|
+
* },
|
|
153
|
+
* }} opts
|
|
154
|
+
* @returns {(request: any, ctx: any) => Promise<void>}
|
|
155
|
+
*/
|
|
156
|
+
export function createCodexHandleRequest({ pre, cwd, log, usage, shuttingDownRef, bridgeCtx, dispatchHandlerOptions, scanner, _createWorkspace, _collectCodexCustomCommands }) {
|
|
157
|
+
const {
|
|
158
|
+
relayUrl,
|
|
159
|
+
bridgeToken,
|
|
160
|
+
producerFactory,
|
|
161
|
+
openclawInvoke: injectedOpenclawInvoke,
|
|
162
|
+
_mcpClientFactory,
|
|
163
|
+
} = dispatchHandlerOptions ?? {};
|
|
164
|
+
// bridge 状态(codex CLI 版本 / gw_id / model), 供 slash 路由器 bridge-synth
|
|
165
|
+
// 分支引用(/help /status /cost /model — codex MCP 全不识别 slash, 必须 bridge 合成)。
|
|
166
|
+
const codexCliVersion = bridgeCtx?.version || pre?.version || null;
|
|
167
|
+
const codexGwId = bridgeCtx?.gwId || null;
|
|
168
|
+
const codexModel = bridgeCtx?.model || null;
|
|
169
|
+
|
|
170
|
+
// MCP 客户端工厂(可被测试注入)
|
|
171
|
+
const mkMcpClient = _mcpClientFactory ?? (({ command, args, cwd: mcpCwd, log: mcpLog }) =>
|
|
172
|
+
createMcpStdioClient({ command, args, cwd: mcpCwd, log: mcpLog })
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// agentlink thread_id → codex internal threadId 缓存(结构化输出抽出来的)。
|
|
176
|
+
// Codex MCP 暴露两个 tool:
|
|
177
|
+
// - `codex {prompt}` 开新会话,返回 `{threadId, content}`
|
|
178
|
+
// - `codex-reply {threadId, prompt}` 用现成的 threadId 续接 —— **但 codex
|
|
179
|
+
// MCP server 要求 session 还在同一个 MCP 进程的内存里**(实测:从另一个
|
|
180
|
+
// MCP 进程发 codex-reply 直接返回 isError + "Session not found")。
|
|
181
|
+
// 所以续接必须配合 **long-lived MCP per agentlink-thread**:第一次给某
|
|
182
|
+
// thread dispatch 时 spawn MCP + 调 codex 拿 threadId 缓存;后续同 thread
|
|
183
|
+
// 复用这个 MCP client 调 codex-reply。
|
|
184
|
+
const threadCodexIdMap = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 }); // audit: LRU+TTL
|
|
185
|
+
// interrupt 闸门 — /resume picker 挂起/恢复
|
|
186
|
+
const interruptGateway = createInterruptGateway({ log });
|
|
187
|
+
// 审批闸门 — codex elicitation/create(工具/命令需用户确认)经此收敛到 App
|
|
188
|
+
// (兼容无 child 方法的注入 log,如部分测试桩)
|
|
189
|
+
const approvalGateway = createApprovalGateway({
|
|
190
|
+
log: typeof log?.child === "function" ? log : { ...log, child: () => log },
|
|
191
|
+
});
|
|
192
|
+
// activeRuns 注册表:per-turn 登记 in-flight requestId,供 sessions.abort 精确 cancel。
|
|
193
|
+
// kill 语义 = 发 notifications/cancelled,不杀 MCP 子进程(会话保留)。
|
|
194
|
+
const activeRuns = createActiveRunRegistry({ log });
|
|
195
|
+
|
|
196
|
+
// agentlink thread_id → 长寿命 MCP client(per-thread 复用),死了从 map 里清掉。
|
|
197
|
+
const threadMcpMap = new Map();
|
|
198
|
+
// agentlink thread_id → 最近一次 session_configured payload(model / permission_profile /
|
|
199
|
+
// approval_policy / mcp_servers / cwd / sandbox / reasoning_effort 等)。供 /permissions
|
|
200
|
+
// /approvals /mcp /status 等 slash 合成响应时引用真实状态(而不是空模板)。
|
|
201
|
+
// 在 MCP onNotification("codex/event") 里捕获(msg.type === "session_configured")。
|
|
202
|
+
const threadSessionConfigured = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 }); // audit: LRU+TTL
|
|
203
|
+
// agentlink thread_id → 最近一次 token_count.rate_limits payload。Codex MCP 每轮
|
|
204
|
+
// 跑完会发 `event_msg.token_count` 携带 5h(primary) + weekly(secondary) 额度,
|
|
205
|
+
// 结构跟 claude 的 rate_limits 一致 — primary.{used_percent, window_minutes:300,
|
|
206
|
+
// resets_at(unix秒)} / secondary.{used_percent, window_minutes:10080, resets_at} /
|
|
207
|
+
// credits / plan_type。/usage 合成时用这份, 同时也写盘到本地最近一次 jsonl
|
|
208
|
+
// (~/.codex/sessions/...)做 fallback(bridge 重启后还能读到上次结果)。
|
|
209
|
+
const threadRateLimits = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 }); // audit: LRU+TTL
|
|
210
|
+
|
|
211
|
+
function evictMcp(threadId, mcpRef, reason) {
|
|
212
|
+
const cur = threadMcpMap.get(threadId);
|
|
213
|
+
if (cur === mcpRef) {
|
|
214
|
+
// audit 修复:仅删 map 引用时旧 mcp 对象的 pending/notifHandlers 仍被
|
|
215
|
+
// 闭包持有;显式 close 释放(进程已退场景 close 幂等无害)。
|
|
216
|
+
try { mcpRef.close?.(reason); } catch { /* already closed */ }
|
|
217
|
+
threadMcpMap.delete(threadId);
|
|
218
|
+
threadCodexIdMap.delete(threadId);
|
|
219
|
+
threadSessionConfigured.delete(threadId);
|
|
220
|
+
threadRateLimits.delete(threadId);
|
|
221
|
+
log.warn?.("codex.mcp.evicted", "long-lived MCP removed from cache", {
|
|
222
|
+
thread_id: threadId, reason,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 默认 openclawInvoke:起 MCP stdio 子进程,监听 codex/event notification
|
|
228
|
+
// Task 5.1: 加入 blocks 参数(默认 []),支持 image + PDF attachment inline URL
|
|
229
|
+
// align-runtime-default-workspace: 加入 workspace_path 参数,解析链同 claude
|
|
230
|
+
const defaultOpenclawInvoke = async ({ thread_id, run_id, input, blocks = [], workspace_path: reqWorkspacePath, onChunk: _rawOnChunk }) => {
|
|
231
|
+
// per-request cwd 解析(align-runtime-default-workspace):
|
|
232
|
+
// 1. workspace_path 非空 → expandHome + existsSync → 用它
|
|
233
|
+
// 2. workspace_path 给定但不存在 → fallback 到 defaultWorkspace,emit cwd_fallback
|
|
234
|
+
// 3. workspace_path 未给 → 直接用 defaultWorkspace(修 service 模式 "/" bug)
|
|
235
|
+
let spawnCwd;
|
|
236
|
+
let cwdFallback = null;
|
|
237
|
+
|
|
238
|
+
const { existsSync: fsExistsSync } = await import("node:fs");
|
|
239
|
+
|
|
240
|
+
if (reqWorkspacePath && typeof reqWorkspacePath === "string" && reqWorkspacePath.trim()) {
|
|
241
|
+
const resolved = expandHome(reqWorkspacePath.trim());
|
|
242
|
+
if (fsExistsSync(resolved)) {
|
|
243
|
+
spawnCwd = resolved;
|
|
244
|
+
log.info?.("codex.spawn.cwd.workspace", "using workspace_path as cwd", {
|
|
245
|
+
cwd: resolved, source: "workspace_path",
|
|
246
|
+
});
|
|
247
|
+
} else {
|
|
248
|
+
spawnCwd = await ensureDefaultWorkspace("codex");
|
|
249
|
+
cwdFallback = {
|
|
250
|
+
actual_cwd: spawnCwd,
|
|
251
|
+
requested_cwd: reqWorkspacePath,
|
|
252
|
+
reason: `workspace_path '${reqWorkspacePath}' does not exist after expand, falling back to default workspace`,
|
|
253
|
+
};
|
|
254
|
+
log.warn?.("codex.spawn.cwd.not_found", "workspace_path does not exist, falling back to default workspace", {
|
|
255
|
+
workspace_path: reqWorkspacePath, fallback_cwd: spawnCwd,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
spawnCwd = await ensureDefaultWorkspace("codex");
|
|
260
|
+
log.info?.("codex.spawn.cwd.default", "no workspace_path, using default workspace", {
|
|
261
|
+
cwd: spawnCwd, source: "default_workspace",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 自动 attach 扫描器: 拦 response.output_text.delta 喂 accumulator, fire-and-forget。
|
|
266
|
+
// codex 是 single-token streaming, accumulator 按 \n 切完整行触发 scan, 路径
|
|
267
|
+
// 自然落在完整行里被识别。scanner 由工厂注入。
|
|
268
|
+
const _acc = scanner?.createTurnAccumulator({ requestId: run_id });
|
|
269
|
+
const onChunk = _acc
|
|
270
|
+
? async (event) => {
|
|
271
|
+
if (event && event.event === "response.output_text.delta" && typeof event.delta === "string") {
|
|
272
|
+
try { _acc.append(event.delta); } catch { /* swallow */ }
|
|
273
|
+
}
|
|
274
|
+
return _rawOnChunk(event);
|
|
275
|
+
}
|
|
276
|
+
: _rawOnChunk;
|
|
277
|
+
|
|
278
|
+
// fallback 可观测:workspace_path 给定但不存在时,emit run.started 含 cwd_fallback
|
|
279
|
+
if (cwdFallback) {
|
|
280
|
+
await onChunk({
|
|
281
|
+
event: "__envelopes",
|
|
282
|
+
envelopes: [buildRunStarted(run_id, { options: { cwd_fallback: cwdFallback } })],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// audit 修复:unsubEvent/mcp 提升声明——turn 中任何异常此前会跳过
|
|
287
|
+
// try 尾部的清理(listener 累积 → 下一轮事件翻倍;__currentRun 悬空 →
|
|
288
|
+
// elicitation 发往旧 run)。清理统一挪进 finally。
|
|
289
|
+
let mcp = null;
|
|
290
|
+
let unsubEvent = null;
|
|
291
|
+
try {
|
|
292
|
+
// ── Slash command 拦截(bridge 层)─────────────────────────────────────
|
|
293
|
+
// codex MCP `tools/call codex {prompt:"/help"}` **不识别任何 slash 命令**——
|
|
294
|
+
// 全部当 prompt 喂给 LLM,LLM 用自然语言"假装"完成命令(最危险的是
|
|
295
|
+
// `/clear` 答"已清除上下文"但 codex 内部 threadId 一字未动,用户被误导)。
|
|
296
|
+
//
|
|
297
|
+
// 因此 codex 这边 bridge 必须截下所有 slash:
|
|
298
|
+
// - /clear → 删 threadCodexIdMap + 关 MCP 进程, 下一条消息 fresh session
|
|
299
|
+
// - /help /status /cost /model /usage /version → bridge 用已有信息合成
|
|
300
|
+
// 等价输出(不调 MCP, 避免 LLM 介入)
|
|
301
|
+
// - 其它 unknown slash → 友好提示
|
|
302
|
+
const cachedSessionCfg = thread_id ? threadSessionConfigured.get(thread_id) || null : null;
|
|
303
|
+
const slash = await classifySlashCommand(input, "codex", {
|
|
304
|
+
version: codexCliVersion,
|
|
305
|
+
cwd,
|
|
306
|
+
gwId: codexGwId,
|
|
307
|
+
threadId: thread_id || null,
|
|
308
|
+
sessionId: thread_id ? threadCodexIdMap.get(thread_id) || null : null,
|
|
309
|
+
// 优先用 session_configured 里的真实 model(codex MCP 启动时返回),
|
|
310
|
+
// 退回到 bridgeCtx 里的 fallback model(目前是 null)。
|
|
311
|
+
model: cachedSessionCfg?.model || codexModel,
|
|
312
|
+
// 整段 session_configured payload, 给 /permissions /approvals /mcp 用
|
|
313
|
+
sessionConfigured: cachedSessionCfg,
|
|
314
|
+
// 最近一次 token_count.rate_limits, 给 /usage 用
|
|
315
|
+
rateLimits: thread_id ? threadRateLimits.get(thread_id) || null : null,
|
|
316
|
+
});
|
|
317
|
+
if (slash.kind === "client-reset") {
|
|
318
|
+
if (thread_id) {
|
|
319
|
+
threadCodexIdMap.delete(thread_id);
|
|
320
|
+
const mcpRef = threadMcpMap.get(thread_id);
|
|
321
|
+
if (mcpRef) {
|
|
322
|
+
// 关掉当前 MCP 进程,下一轮 spawn 新的 = 新 codex session,
|
|
323
|
+
// 跟用户对"清除上下文"的期望一致。
|
|
324
|
+
try { mcpRef.close?.("slash-clear"); } catch {}
|
|
325
|
+
evictMcp(thread_id, mcpRef, "slash-clear");
|
|
326
|
+
}
|
|
327
|
+
log.info?.("codex.slash.clear",
|
|
328
|
+
"client-reset slash command, dropped codex threadId + closed MCP",
|
|
329
|
+
{ thread_id, command: slash.command });
|
|
330
|
+
}
|
|
331
|
+
await emitAssistantText(onChunk, slash.ackText);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (slash.kind === "bridge-synth") {
|
|
335
|
+
log.info?.("codex.slash.synth",
|
|
336
|
+
"bridge-synthesized slash response (codex MCP can't dispatch slash)",
|
|
337
|
+
{ thread_id, command: slash.command });
|
|
338
|
+
await emitAssistantText(onChunk, slash.responseText);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── /resume interrupt session picker (codex) ────────────────────────────
|
|
343
|
+
if (slash.kind === "interrupt-session-pick") {
|
|
344
|
+
log.info?.("codex.slash.resume_picker", "/resume: starting interrupt session picker", { thread_id, run_id });
|
|
345
|
+
// 默认只列「当前工作区」(spawnCwd) 下的历史会话;跨项目会话不再混入。
|
|
346
|
+
const sessions = await listSessionsForRuntime("codex", { filterCwd: spawnCwd });
|
|
347
|
+
if (sessions.length === 0) {
|
|
348
|
+
log.info?.("codex.slash.resume_picker.empty", "/resume: no sessions in current workspace", { thread_id, cwd: spawnCwd });
|
|
349
|
+
await emitAssistantText(onChunk, "本工作区暂无历史会话,直接开始新对话吧");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const blockId = newBlockId();
|
|
353
|
+
const options = sessions.map((s) => ({ id: s.session_id, label: buildSessionLabel(s), description: buildSessionSubtitle(s) }));
|
|
354
|
+
await onChunk({
|
|
355
|
+
event: "__envelopes",
|
|
356
|
+
envelopes: [
|
|
357
|
+
buildRunStarted(run_id),
|
|
358
|
+
buildBlockStarted(run_id, {
|
|
359
|
+
id: blockId, type: "interrupt", subtype: "question",
|
|
360
|
+
prompt: "选择要续接的历史会话", options, selection_mode: "single",
|
|
361
|
+
}),
|
|
362
|
+
buildRunPaused(run_id, [blockId]),
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
// 非阻塞:发完 interrupt + run.paused 立即 return 释放 worker;
|
|
366
|
+
// resume 由 relay stream 监听器异步处理(见 claude handleRequest 同款)。
|
|
367
|
+
if (relayUrl && bridgeToken && thread_id) {
|
|
368
|
+
waitForRunResumed({ relayUrl, bridgeToken, threadId: thread_id, runId: run_id, interruptId: blockId, log })
|
|
369
|
+
.then(async ({ value }) => {
|
|
370
|
+
const decisionValue = value;
|
|
371
|
+
await onChunk({
|
|
372
|
+
event: "__envelopes",
|
|
373
|
+
envelopes: [
|
|
374
|
+
buildBlockCompleted(run_id, { id: blockId, type: "interrupt", subtype: "question", prompt: "选择要续接的历史会话", status: "completed", value: decisionValue }),
|
|
375
|
+
buildRunCompleted(run_id, "end_turn"),
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
// codex 的 session resume 与 claude 不同(走 codex-reply threadId),
|
|
379
|
+
// 磁盘 session resume 暂无等价路径,仅记录供后续集成。
|
|
380
|
+
const selectedSessionId = decisionValue?.option_id ?? null;
|
|
381
|
+
if (selectedSessionId) {
|
|
382
|
+
log.info?.("codex.slash.resume_picker.done", "codex session selected (pending resume integration)", {
|
|
383
|
+
thread_id, session_id: selectedSessionId,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
.catch((err) => log.warn?.("codex.slash.resume_picker.listener_err", err?.message));
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (slash.kind === "unsupported") {
|
|
393
|
+
log.info?.("codex.slash.unsupported",
|
|
394
|
+
"unsupported slash command, replying with hint",
|
|
395
|
+
{ thread_id, command: slash.command });
|
|
396
|
+
await emitAssistantText(onChunk, slash.hintText);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// not-slash:继续走 MCP 路径
|
|
400
|
+
|
|
401
|
+
// Task 5.3: 使用 buildCodexPromptWithAttachments 构造包含 image/PDF 引导语的 prompt
|
|
402
|
+
// P0#29: codex MCP 工具只收纯文本 prompt,远程 URL 模型"看"不到(fetch 拿到的
|
|
403
|
+
// 是二进制,进不了多模态上下文)→ 先把 image URL 下载成本地临时文件,
|
|
404
|
+
// prompt 引导 codex 用 view_image 工具查看本地路径。下载失败回退 URL 行为。
|
|
405
|
+
const localizedBlocks = await localizeImageBlocksForCodex(blocks, { log });
|
|
406
|
+
const promptWithAttachments = buildCodexPromptWithAttachments(input, localizedBlocks);
|
|
407
|
+
let priorCodexThreadId = thread_id ? threadCodexIdMap.get(thread_id) : null;
|
|
408
|
+
mcp = thread_id ? threadMcpMap.get(thread_id) : null;
|
|
409
|
+
let mcpIsNew = false;
|
|
410
|
+
if (!mcp) {
|
|
411
|
+
// P0#27: codex 按 ~/.codex/config.toml 的 projects 信任决定沙箱模式;
|
|
412
|
+
// 默认工作区(ensureDefaultWorkspace 建出)从未写过信任 → untrusted →
|
|
413
|
+
// read-only 沙箱 + 命令被拒。配对进来的工作区都是用户显式导入/创建的,
|
|
414
|
+
// spawn 前幂等补写 trust_level=trusted。
|
|
415
|
+
// 测试注入 _mcpClientFactory 时跳过(不碰真实 ~/.codex/config.toml)。
|
|
416
|
+
try {
|
|
417
|
+
if (!_mcpClientFactory) await writeCodexTrust(spawnCwd);
|
|
418
|
+
} catch (trustErr) {
|
|
419
|
+
log.warn?.("codex.trust.write_failed", "writeCodexTrust failed (continuing)", {
|
|
420
|
+
cwd: spawnCwd, err: trustErr?.message || String(trustErr),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
mcp = mkMcpClient({
|
|
424
|
+
command: "codex",
|
|
425
|
+
args: [pre.mcpSubcommand],
|
|
426
|
+
cwd: spawnCwd, // align-runtime-default-workspace: 用解析后的 spawnCwd
|
|
427
|
+
log,
|
|
428
|
+
});
|
|
429
|
+
mcpIsNew = true;
|
|
430
|
+
if (thread_id) {
|
|
431
|
+
threadMcpMap.set(thread_id, mcp);
|
|
432
|
+
// MCP 进程意外死亡 → 从 cache 清掉,下次 dispatch 会重 spawn
|
|
433
|
+
try {
|
|
434
|
+
mcp.setExitHandler(({ code, signal }) => {
|
|
435
|
+
evictMcp(thread_id, mcp, `mcp.exit code=${code} signal=${signal}`);
|
|
436
|
+
});
|
|
437
|
+
} catch { /* setExitHandler 不存在的话忽略 */ }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let turnComplete = false;
|
|
442
|
+
let lastChunkError = null;
|
|
443
|
+
|
|
444
|
+
// plan notification handler:处理 turn/plan/updated → todo_list block 翻译
|
|
445
|
+
// 每次 openclawInvoke 独立实例(每个 run 独立),符合 D3 的 block.id 复用策略
|
|
446
|
+
const planHandler = createCodexPlanHandler({
|
|
447
|
+
runId: run_id,
|
|
448
|
+
onEnvelopes: async (envs) => {
|
|
449
|
+
try { await onChunk({ event: "__envelopes", envelopes: envs }); } catch { /* ignore */ }
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// 每轮重新订阅 codex/event —— 闭包里捕获本轮的 turnComplete/onChunk。
|
|
454
|
+
// 关键:turn 结束时 unsubscribe 这个 listener,否则下一轮 listener 会叠加,
|
|
455
|
+
// 一个事件触发多次 chunk → App 端文本翻倍。
|
|
456
|
+
unsubEvent = mcp.onNotification("codex/event", async (params) => {
|
|
457
|
+
const msg = params?.msg;
|
|
458
|
+
if (!msg || typeof msg !== "object") return;
|
|
459
|
+
|
|
460
|
+
// 捕获 session_configured: 含 model / permission_profile / approval_policy /
|
|
461
|
+
// mcp_servers / sandbox / reasoning_effort / cwd / session_id, 后续 slash
|
|
462
|
+
// 合成(/permissions /approvals /mcp /sandbox 等)从这里读真实状态。
|
|
463
|
+
// 捕获 token_count → rate_limits(5h + weekly 配额),供 /usage 用
|
|
464
|
+
if (msg.type === "token_count" && msg.rate_limits && thread_id) {
|
|
465
|
+
threadRateLimits.set(thread_id, {
|
|
466
|
+
...msg.rate_limits,
|
|
467
|
+
captured_at: new Date().toISOString(),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (msg.type === "session_configured" && thread_id) {
|
|
472
|
+
threadSessionConfigured.set(thread_id, {
|
|
473
|
+
session_id: msg.session_id || null,
|
|
474
|
+
thread_id_codex: msg.thread_id || null,
|
|
475
|
+
model: msg.model || null,
|
|
476
|
+
model_provider_id: msg.model_provider_id || null,
|
|
477
|
+
approval_policy: msg.approval_policy || null,
|
|
478
|
+
approvals_reviewer: msg.approvals_reviewer || null,
|
|
479
|
+
permission_profile: msg.permission_profile || null,
|
|
480
|
+
active_permission_profile: msg.active_permission_profile || null,
|
|
481
|
+
cwd: msg.cwd || null,
|
|
482
|
+
reasoning_effort: msg.reasoning_effort || null,
|
|
483
|
+
mcp_servers: msg.mcp_servers || null,
|
|
484
|
+
rollout_path: msg.rollout_path || null,
|
|
485
|
+
captured_at: new Date().toISOString(),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 先检测 turn 结束信号
|
|
490
|
+
if (msg.type === "task_complete" || msg.type === "task_completed" ||
|
|
491
|
+
msg.type === "session_ended" || msg.type === "shutdown") {
|
|
492
|
+
turnComplete = true;
|
|
493
|
+
}
|
|
494
|
+
// turn_aborted: codex 收到 notifications/cancelled 后发此事件,表示 turn 已真停。
|
|
495
|
+
// 置 turnComplete=true 使主循环退出;同时配合下方 `if (toolError && !turnComplete)`
|
|
496
|
+
// 护栏,确保被 cancel 的 tools/call 迟到 reject 不触发 response.failed。
|
|
497
|
+
if (msg.type === "turn_aborted") {
|
|
498
|
+
turnComplete = true;
|
|
499
|
+
abortedByTurnAborted = true;
|
|
500
|
+
}
|
|
501
|
+
if (msg.type === "error") {
|
|
502
|
+
lastChunkError = String(msg.message || msg.error || "codex error");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// turn/plan/updated 通知:交给 planHandler 处理(不透传为普通 chunk)
|
|
506
|
+
if (msg.type === "turn/plan/updated") {
|
|
507
|
+
const mapped = mapCodexMsg(msg);
|
|
508
|
+
if (mapped?.kind === "plan_updated") {
|
|
509
|
+
planHandler.handlePlanUpdated(mapped.payload);
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const chunk = translateCodexMsgToChunk(msg);
|
|
515
|
+
if (chunk) {
|
|
516
|
+
const chunks = Array.isArray(chunk) ? chunk : [chunk];
|
|
517
|
+
for (const c of chunks) {
|
|
518
|
+
try { await onChunk(c); } catch { /* ignore */ }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// long-lived MCP:只在第一次 spawn 时 initialize,后续 turn 复用已经
|
|
524
|
+
// initialized 的 client。mcp.initialize 自己有 `if (initialized) return`
|
|
525
|
+
// 兜底,这里加 if(mcpIsNew) 主要是为了不在 reuse 路径上多打 init log。
|
|
526
|
+
if (mcpIsNew) {
|
|
527
|
+
try {
|
|
528
|
+
await mcp.initialize({ capabilities: {} });
|
|
529
|
+
} catch (err) {
|
|
530
|
+
const msg = err?.message || String(err);
|
|
531
|
+
log.error?.("mcp.initialize.failed", "handshake failed", { err: msg });
|
|
532
|
+
await onChunk({ event: "response.failed", error: { message: `mcp init failed: ${msg}` } });
|
|
533
|
+
try { unsubEvent(); } catch {}
|
|
534
|
+
try { mcp.close("init-failed"); } catch {}
|
|
535
|
+
if (thread_id) evictMcp(thread_id, mcp, "init-failed");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// P0#27/#16: codex elicitation/create(命令/工具需用户确认)→ App 审批弹窗。
|
|
541
|
+
// 此前 handler 从未注册,codex 的确认请求悬空超时 → 工具直接失败。
|
|
542
|
+
// 编排与 /resume picker 一致:interrupt(approval) block + run.paused →
|
|
543
|
+
// 等 relay run.resumed(App 决策)→ blockCompleted + settle 闸门。
|
|
544
|
+
if (mcpIsNew) {
|
|
545
|
+
try {
|
|
546
|
+
attachElicitationHandler({
|
|
547
|
+
mcp,
|
|
548
|
+
approvalGateway,
|
|
549
|
+
log,
|
|
550
|
+
onApprovalRequest: (info) => {
|
|
551
|
+
const cur = mcp.__currentRun;
|
|
552
|
+
if (!cur || !relayUrl || !bridgeToken || !cur.thread_id) {
|
|
553
|
+
log.warn?.("codex.approval.no_context",
|
|
554
|
+
"elicitation arrived without run context; auto-reject", {
|
|
555
|
+
request_id: info?.request_id || "",
|
|
556
|
+
});
|
|
557
|
+
approvalGateway.settle(info.request_id, "reject", "no-run-context");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const blockId = newBlockId();
|
|
561
|
+
const message =
|
|
562
|
+
(typeof info?.tool_input?.message === "string" && info.tool_input.message.trim())
|
|
563
|
+
? info.tool_input.message.trim()
|
|
564
|
+
: "Codex 请求执行需要确认的操作";
|
|
565
|
+
void cur.onChunk({
|
|
566
|
+
event: "__envelopes",
|
|
567
|
+
envelopes: [
|
|
568
|
+
buildBlockStarted(cur.run_id, {
|
|
569
|
+
id: blockId,
|
|
570
|
+
type: "interrupt",
|
|
571
|
+
subtype: "approval",
|
|
572
|
+
prompt: message,
|
|
573
|
+
}),
|
|
574
|
+
buildRunPaused(cur.run_id, [blockId]),
|
|
575
|
+
],
|
|
576
|
+
});
|
|
577
|
+
waitForRunResumed({
|
|
578
|
+
relayUrl,
|
|
579
|
+
bridgeToken,
|
|
580
|
+
threadId: cur.thread_id,
|
|
581
|
+
runId: cur.run_id,
|
|
582
|
+
interruptId: blockId,
|
|
583
|
+
log,
|
|
584
|
+
}).then(async ({ value }) => {
|
|
585
|
+
const approved =
|
|
586
|
+
value?.approved === true ||
|
|
587
|
+
(typeof value?.option_id === "string" &&
|
|
588
|
+
value.option_id !== "" && value.option_id !== "reject");
|
|
589
|
+
await cur.onChunk({
|
|
590
|
+
event: "__envelopes",
|
|
591
|
+
envelopes: [
|
|
592
|
+
buildBlockCompleted(cur.run_id, {
|
|
593
|
+
id: blockId,
|
|
594
|
+
type: "interrupt",
|
|
595
|
+
subtype: "approval",
|
|
596
|
+
prompt: message,
|
|
597
|
+
status: "completed",
|
|
598
|
+
value,
|
|
599
|
+
}),
|
|
600
|
+
],
|
|
601
|
+
});
|
|
602
|
+
approvalGateway.settle(
|
|
603
|
+
info.request_id,
|
|
604
|
+
approved ? "approve" : "reject",
|
|
605
|
+
typeof value?.reason === "string" ? value.reason : "",
|
|
606
|
+
);
|
|
607
|
+
}).catch((err) => {
|
|
608
|
+
log.warn?.("codex.approval.listener_err", "run.resumed listener error", {
|
|
609
|
+
request_id: info?.request_id || "",
|
|
610
|
+
err: err?.message || String(err),
|
|
611
|
+
});
|
|
612
|
+
approvalGateway.settle(info.request_id, "reject", "listener-error");
|
|
613
|
+
});
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
} catch (attachErr) {
|
|
617
|
+
log.warn?.("codex.approval.attach_failed", "attachElicitationHandler failed", {
|
|
618
|
+
err: attachErr?.message || String(attachErr),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let toolError = null;
|
|
624
|
+
let toolResult = null;
|
|
625
|
+
// turn_aborted 标志:收到 codex/event turn_aborted 时置 true,用于 turn 结束后
|
|
626
|
+
// emit「已停止本次回复」文案(类比 claude runtime 的 abortedByRpc 收尾行为)。
|
|
627
|
+
let abortedByTurnAborted = false;
|
|
628
|
+
// 续接:缓存命中 → codex-reply(带 threadId 续会话),否则 codex(开新会话)。
|
|
629
|
+
// 第一次会 cache codex 返回的 threadId;同一 MCP 进程后续轮直接 reply。
|
|
630
|
+
let callName = priorCodexThreadId ? "codex-reply" : "codex";
|
|
631
|
+
let callArgs = priorCodexThreadId
|
|
632
|
+
? { threadId: priorCodexThreadId, prompt: promptWithAttachments }
|
|
633
|
+
: { prompt: promptWithAttachments };
|
|
634
|
+
if (priorCodexThreadId) {
|
|
635
|
+
log.info?.("codex.session.resume", "resuming codex thread", {
|
|
636
|
+
agentlink_thread: thread_id, codex_thread: priorCodexThreadId,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// executeCall 包装:codex-reply 在 MCP server 还活着但 session 失效时
|
|
641
|
+
// (理论上不该发生,但兜底)返回 isError + "Session not found",
|
|
642
|
+
// 自动回退到全新 `codex` 调用。
|
|
643
|
+
//
|
|
644
|
+
// 改用 callWithId 以持有 requestId,供 activeRuns.register 的 kill 函数
|
|
645
|
+
// 调用 mcp.cancel(requestId) 发 notifications/cancelled 真停当前 turn。
|
|
646
|
+
async function executeCall() {
|
|
647
|
+
let lastErr = null;
|
|
648
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
649
|
+
let res;
|
|
650
|
+
try {
|
|
651
|
+
// callWithId 暴露 requestId,供 cancel(requestId) 精确取消当前 turn。
|
|
652
|
+
// mcp 由 _mcpClientFactory 注入,真实路径已是 mcpStdioClient(有 callWithId);
|
|
653
|
+
// 若注入的 mock 未实现 callWithId,降级到 call() 并注册空 kill(无 cancel 能力)。
|
|
654
|
+
let promise;
|
|
655
|
+
let unregisterRun;
|
|
656
|
+
if (typeof mcp.callWithId === "function") {
|
|
657
|
+
const callResult = mcp.callWithId("tools/call", { name: callName, arguments: callArgs });
|
|
658
|
+
promise = callResult.promise;
|
|
659
|
+
const requestId = callResult.requestId;
|
|
660
|
+
// turn 开始前登记 activeRun:
|
|
661
|
+
// kill = () => mcp.cancel(requestId)(发 cancelled,不杀子进程)
|
|
662
|
+
// turn 结束(turnComplete 或 turn_aborted)时 unregister。
|
|
663
|
+
unregisterRun = activeRuns.register({
|
|
664
|
+
sessionKey: thread_id,
|
|
665
|
+
threadId: thread_id,
|
|
666
|
+
runId: run_id,
|
|
667
|
+
kill: () => mcp.cancel(requestId),
|
|
668
|
+
});
|
|
669
|
+
} else {
|
|
670
|
+
// 降级路径:旧 mock 或不支持 callWithId 的客户端
|
|
671
|
+
promise = mcp.call("tools/call", { name: callName, arguments: callArgs });
|
|
672
|
+
unregisterRun = activeRuns.register({
|
|
673
|
+
sessionKey: thread_id,
|
|
674
|
+
threadId: thread_id,
|
|
675
|
+
runId: run_id,
|
|
676
|
+
kill: () => { /* no cancel capability in fallback mode */ },
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
res = await promise;
|
|
681
|
+
} catch (err) {
|
|
682
|
+
lastErr = err;
|
|
683
|
+
unregisterRun();
|
|
684
|
+
return { res: null, err };
|
|
685
|
+
}
|
|
686
|
+
unregisterRun();
|
|
687
|
+
} catch (err) {
|
|
688
|
+
lastErr = err;
|
|
689
|
+
return { res: null, err };
|
|
690
|
+
}
|
|
691
|
+
if (res?.isError === true) {
|
|
692
|
+
const txt = Array.isArray(res.content)
|
|
693
|
+
? res.content.filter((b) => b?.type === "text").map((b) => b.text).join("\n")
|
|
694
|
+
: "tool returned isError";
|
|
695
|
+
// Session not found → 清缓存,重试 codex(开新会话)
|
|
696
|
+
if (callName === "codex-reply" && /session not found/i.test(txt) && attempt === 0) {
|
|
697
|
+
log.warn?.("codex.session.invalid",
|
|
698
|
+
"codex-reply rejected with Session not found; falling back to fresh codex",
|
|
699
|
+
{ agentlink_thread: thread_id, codex_thread: callArgs.threadId, err: txt });
|
|
700
|
+
if (thread_id) threadCodexIdMap.delete(thread_id);
|
|
701
|
+
callName = "codex";
|
|
702
|
+
callArgs = { prompt: promptWithAttachments };
|
|
703
|
+
priorCodexThreadId = null;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
return { res, err: new Error(`codex tool error: ${txt}`) };
|
|
707
|
+
}
|
|
708
|
+
return { res, err: null };
|
|
709
|
+
}
|
|
710
|
+
return { res: null, err: lastErr || new Error("codex retry exhausted") };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// elicitation handler 是 per-MCP 一次性挂载的,审批要回到「当前 turn」的
|
|
714
|
+
// run 上下文 —— 每轮 tools/call 前登记,turn 结束清除。
|
|
715
|
+
mcp.__currentRun = { thread_id, run_id, onChunk };
|
|
716
|
+
const callPromise = executeCall()
|
|
717
|
+
.then(({ res, err }) => {
|
|
718
|
+
toolResult = res;
|
|
719
|
+
if (err) toolError = err;
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// 等 turn 完成(task_complete notification / tools/call 返回 / 硬超时 / shutdown)
|
|
723
|
+
const startedAt = Date.now();
|
|
724
|
+
while (!turnComplete && !shuttingDownRef?.current && !toolError) {
|
|
725
|
+
if (Date.now() - startedAt > TURN_HARD_TIMEOUT_MS) {
|
|
726
|
+
log.warn?.("codex.turn.hard_timeout", "turn exceeded hard timeout", {
|
|
727
|
+
elapsed_ms: Date.now() - startedAt,
|
|
728
|
+
});
|
|
729
|
+
await onChunk({ event: "response.failed", error: { message: "codex turn hard timeout" } });
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
await sleep(200);
|
|
733
|
+
}
|
|
734
|
+
await callPromise;
|
|
735
|
+
|
|
736
|
+
if (toolError && !turnComplete) {
|
|
737
|
+
const errMsg = toolError?.message || String(toolError);
|
|
738
|
+
await onChunk({ event: "response.failed", error: { message: `tools/call failed: ${errMsg}` } });
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// turn_aborted 收尾:emit 「已停止本次回复」 assistant text chunk
|
|
742
|
+
// 类比 claude runtime 的 abortedByRpc 收尾(让 App 看到明确的停止文案)。
|
|
743
|
+
if (abortedByTurnAborted) {
|
|
744
|
+
await onChunk({ event: "response.output_text.delta", delta: "已停止本次回复" });
|
|
745
|
+
log.info?.("codex.abort.finalized", "turn aborted by cancel, emitted stop notice", {
|
|
746
|
+
thread_id, run_id,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 从 tool result 抽取 codex threadId 缓存起来,下次同 agentlink thread
|
|
751
|
+
// 来 dispatch 时走 codex-reply。结构化输出按 MCP outputSchema 形状走
|
|
752
|
+
// structuredContent;老版本 codex 可能放在 content[].text 的 JSON 里,
|
|
753
|
+
// 这里两种都兜一下。已有缓存(同 MCP 复用)时跳过,避免每轮覆盖。
|
|
754
|
+
if (thread_id && toolResult && !toolError && !threadCodexIdMap.has(thread_id)) {
|
|
755
|
+
const sc = toolResult.structuredContent;
|
|
756
|
+
let codexThreadId = sc && typeof sc.threadId === "string" ? sc.threadId : null;
|
|
757
|
+
if (!codexThreadId && Array.isArray(toolResult.content)) {
|
|
758
|
+
for (const item of toolResult.content) {
|
|
759
|
+
if (item?.type === "text" && typeof item.text === "string") {
|
|
760
|
+
try {
|
|
761
|
+
const parsed = JSON.parse(item.text);
|
|
762
|
+
if (parsed && typeof parsed.threadId === "string") {
|
|
763
|
+
codexThreadId = parsed.threadId;
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
} catch { /* not JSON, ignore */ }
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (codexThreadId) {
|
|
771
|
+
threadCodexIdMap.set(thread_id, codexThreadId);
|
|
772
|
+
log.info?.("codex.session.captured",
|
|
773
|
+
"captured codex threadId for resume", {
|
|
774
|
+
agentlink_thread: thread_id, codex_thread: codexThreadId,
|
|
775
|
+
});
|
|
776
|
+
} else {
|
|
777
|
+
log.warn?.("codex.session.no_thread_id",
|
|
778
|
+
"tools/call returned no threadId — next turn will start a new session",
|
|
779
|
+
{ agentlink_thread: thread_id });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// plan handler complete():发 content_block.completed(若有未关闭的 plan block)
|
|
784
|
+
// 在 response.completed 之前调用(unifiedDispatchHandler 会在我们返回后追加 run.completed)
|
|
785
|
+
planHandler.complete();
|
|
786
|
+
|
|
787
|
+
// **关键**:long-lived MCP 模式下 turn 结束 **只 unsubscribe listener**,
|
|
788
|
+
// 不能 close MCP —— 否则同 agentlink thread 下一轮还得重 spawn,
|
|
789
|
+
// codex 内存 session 又丢了,跟没续接一样。
|
|
790
|
+
// MCP 子进程的真正 cleanup 走 setExitHandler / bridge shutdown。
|
|
791
|
+
} finally {
|
|
792
|
+
try { _acc?.flush(); } catch { /* swallow */ }
|
|
793
|
+
// 清理统一在 finally:异常路径同样生效
|
|
794
|
+
if (mcp?.__currentRun?.run_id === run_id) mcp.__currentRun = null;
|
|
795
|
+
try { unsubEvent?.(); } catch { /* already unsubscribed */ }
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// 注册 unifiedDispatchHandler
|
|
800
|
+
const unifiedHandler = createUnifiedDispatchHandler({
|
|
801
|
+
relayUrl,
|
|
802
|
+
bridgeToken,
|
|
803
|
+
log,
|
|
804
|
+
producerFactory,
|
|
805
|
+
openclawInvoke: injectedOpenclawInvoke ?? defaultOpenclawInvoke,
|
|
806
|
+
gatewayId: codexGwId, // 正文 agentlink-cos:// 图片引用解析(sign-get)
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// workspaces.create 实现(可注入用于测试)
|
|
810
|
+
const createWorkspaceImpl = _createWorkspace ?? _defaultCreateWorkspace;
|
|
811
|
+
// codex 自定义命令收集器(可注入用于测试)
|
|
812
|
+
const collectCodexCustomCommandsImpl = _collectCodexCustomCommands ?? _defaultCollectCodexCustomCommands;
|
|
813
|
+
|
|
814
|
+
return async function handleRequest(request, ctx) {
|
|
815
|
+
// 步骤 0(最优先):relay-rpc 分支。
|
|
816
|
+
if (request?.request?.__relayKind === "rpc") {
|
|
817
|
+
const method = request.request.method;
|
|
818
|
+
const params = request.request.params ?? {};
|
|
819
|
+
let result;
|
|
820
|
+
try {
|
|
821
|
+
if (method === "__runtime.customCommands") {
|
|
822
|
+
// codex 自定义命令来自 ~/.codex/prompts/*.md,无需 probe
|
|
823
|
+
const commands = await collectCodexCustomCommandsImpl();
|
|
824
|
+
result = { commands };
|
|
825
|
+
} else if (method === "workspaces.scan") {
|
|
826
|
+
// workspaces.scan 走通用 scanWorkspacesByRuntime(codex runtime 读
|
|
827
|
+
// ~/.codex/sessions/*.jsonl 首行 cwd field,dedupe 后返回)。
|
|
828
|
+
result = await scanWorkspacesByRuntime({
|
|
829
|
+
runtimeKind: "codex",
|
|
830
|
+
gatewayId: codexGwId,
|
|
831
|
+
params,
|
|
832
|
+
});
|
|
833
|
+
} else if (method === "workspaces.create") {
|
|
834
|
+
result = await createWorkspaceImpl({
|
|
835
|
+
runtime: "codex",
|
|
836
|
+
name: params.name,
|
|
837
|
+
});
|
|
838
|
+
} else if (method === "sessions.abort" || method === "chat.abort") {
|
|
839
|
+
// codex abort 真实实现(codex-abort-via-cancelled):
|
|
840
|
+
// 1. activeRuns.abort 找到匹配的 in-flight run,调用 kill()
|
|
841
|
+
// 2. kill() = mcp.cancel(requestId),发 notifications/cancelled 给 codex MCP server
|
|
842
|
+
// 3. codex 发 turn_aborted 事件,主循环 turnComplete 置位,turn 自然收尾
|
|
843
|
+
// 4. MCP 子进程不被 kill,threadCodexIdMap 保留,下条消息可续接
|
|
844
|
+
// aborted=0 时返回 no-active-run(不报错,方便 App 幂等重试)。
|
|
845
|
+
const { aborted, matched } = activeRuns.abort({
|
|
846
|
+
key: params.key ?? params.sessionKey ?? "",
|
|
847
|
+
runId: params.runId ?? "",
|
|
848
|
+
});
|
|
849
|
+
if (aborted >= 1) {
|
|
850
|
+
result = { ok: true, aborted, matched };
|
|
851
|
+
} else {
|
|
852
|
+
result = { ok: true, aborted: 0, matched: [], status: "no-active-run" };
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
result = { ok: false, error: `unknown rpc method: ${method}` };
|
|
856
|
+
}
|
|
857
|
+
} catch (err) {
|
|
858
|
+
result = { ok: false, error: err?.message ?? String(err) };
|
|
859
|
+
}
|
|
860
|
+
await ctx.publishEvent({
|
|
861
|
+
event: "response",
|
|
862
|
+
statusCode: 200,
|
|
863
|
+
contentType: "application/json; charset=utf-8",
|
|
864
|
+
body: JSON.stringify(result),
|
|
865
|
+
});
|
|
866
|
+
await ctx.publishEvent({ event: "end" });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// 步骤 1:优先走 unified dispatch
|
|
871
|
+
const handled = await unifiedHandler(request, ctx);
|
|
872
|
+
if (handled) return;
|
|
873
|
+
|
|
874
|
+
// 步骤 2:旧路径 — 与 index.mjs 原有逻辑一致
|
|
875
|
+
const userText = extractUserText(request);
|
|
876
|
+
if (!userText) {
|
|
877
|
+
log.warn?.("turn.skip", "no user text in request", { requestId: ctx.requestId });
|
|
878
|
+
await ctx.publishEvent({ event: "error", error: "empty user input" });
|
|
879
|
+
await ctx.publishEvent({ event: "end" });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
await ctx.publishEvent({ event: "start" });
|
|
884
|
+
|
|
885
|
+
const mcp = mkMcpClient({
|
|
886
|
+
command: "codex",
|
|
887
|
+
args: [pre.mcpSubcommand],
|
|
888
|
+
cwd,
|
|
889
|
+
log,
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
let turnComplete = false;
|
|
893
|
+
let lastError = null;
|
|
894
|
+
|
|
895
|
+
mcp.onNotification("codex/event", (params) => {
|
|
896
|
+
const msg = params?.msg;
|
|
897
|
+
if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
|
|
898
|
+
try {
|
|
899
|
+
const data = JSON.stringify(msg);
|
|
900
|
+
void ctx.publishEvent({ event: "chunk", data });
|
|
901
|
+
} catch (err) {
|
|
902
|
+
log.warn?.("codex.notify.serialize", "failed to stringify msg", {
|
|
903
|
+
err: err?.message || String(err), type: msg.type,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
if (msg.type === "task_complete" || msg.type === "task_completed" ||
|
|
907
|
+
msg.type === "session_ended" || msg.type === "shutdown") {
|
|
908
|
+
turnComplete = true;
|
|
909
|
+
}
|
|
910
|
+
if (msg.type === "error") {
|
|
911
|
+
lastError = String(msg.message || msg.error || "codex error");
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
try {
|
|
916
|
+
await mcp.initialize({ capabilities: {} });
|
|
917
|
+
} catch (err) {
|
|
918
|
+
const msg = err?.message || String(err);
|
|
919
|
+
log.error?.("mcp.initialize.failed", "handshake failed", { err: msg });
|
|
920
|
+
await ctx.publishEvent({ event: "error", error: `mcp init failed: ${msg}` });
|
|
921
|
+
await ctx.publishEvent({ event: "end" });
|
|
922
|
+
try { mcp.close("init-failed"); } catch {}
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
let toolError = null;
|
|
927
|
+
const callPromise = mcp
|
|
928
|
+
.call("tools/call", { name: "codex", arguments: { prompt: userText } })
|
|
929
|
+
.catch((err) => { toolError = err; });
|
|
930
|
+
|
|
931
|
+
const startedAt = Date.now();
|
|
932
|
+
while (!turnComplete && !shuttingDownRef?.current && !toolError) {
|
|
933
|
+
if (Date.now() - startedAt > TURN_HARD_TIMEOUT_MS) {
|
|
934
|
+
log.warn?.("codex.turn.hard_timeout", "turn exceeded hard timeout", {
|
|
935
|
+
requestId: ctx.requestId, elapsed_ms: Date.now() - startedAt,
|
|
936
|
+
});
|
|
937
|
+
lastError = "turn hard timeout";
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
await sleep(200);
|
|
941
|
+
}
|
|
942
|
+
await callPromise;
|
|
943
|
+
|
|
944
|
+
if (toolError) {
|
|
945
|
+
const msg = toolError?.message || String(toolError);
|
|
946
|
+
log.warn?.("codex.tool.fail", "tools/call codex errored", { err: msg });
|
|
947
|
+
await ctx.publishEvent({ event: "error", error: `tools/call failed: ${msg}` });
|
|
948
|
+
} else if (lastError) {
|
|
949
|
+
await ctx.publishEvent({ event: "error", error: lastError });
|
|
950
|
+
}
|
|
951
|
+
await ctx.publishEvent({ event: "end" });
|
|
952
|
+
|
|
953
|
+
try { mcp.close("turn-end"); } catch {}
|
|
954
|
+
|
|
955
|
+
usage.addCost({ turns: 1, duration_ms: Date.now() - startedAt });
|
|
956
|
+
usage.schedulePublish("codex.turn_end");
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
961
|
+
|
|
962
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* buildCodexPromptWithAttachments — Phase 5 (multimodal-attachment-llm-dispatch)
|
|
966
|
+
*
|
|
967
|
+
* codex MCP prompt 参数为字符串类型(无多模态 content array)。
|
|
968
|
+
* N/A for base64 fetch path — codex uses string prompt only; attachment content
|
|
969
|
+
* is represented as URL references or descriptions inline in the prompt text.
|
|
970
|
+
*
|
|
971
|
+
* attachment_kind ベースの4種 dispatch(relayObjectToBlock 相当のロジックを string に inline):
|
|
972
|
+
* - image → [image] <URL>(base64 fetch は不可。codex は fetch tool で取得する)
|
|
973
|
+
* - document → [file: <name>] <URL>(PDF: URL inline)
|
|
974
|
+
* - text → [file: <name>] <URL>(text-like: URL inline)
|
|
975
|
+
* - binary → [file: <name>] (type: <mime>, size: <size>) — binary content
|
|
976
|
+
*
|
|
977
|
+
* 後方互換:attachment_kind がない場合は block.type + mime から推断。
|
|
978
|
+
*
|
|
979
|
+
* @param {string} input - 用户原始文字 prompt
|
|
980
|
+
* @param {any[]} blocks - dispatch payload blocks 数组
|
|
981
|
+
* @returns {string} 构造后的 prompt 字符串
|
|
982
|
+
*/
|
|
983
|
+
// P0#29: image URL block → 本地临时文件。codex 的 view_image 工具只认本地路径,
|
|
984
|
+
// 这是 codex 路径上让模型真正"看到"图片的唯一通道(MCP prompt 无多模态入参)。
|
|
985
|
+
// 单图上限 15MB、15s 超时;任何失败都不阻塞——保留原 block,prompt 回退 URL 行。
|
|
986
|
+
const CODEX_IMG_MAX_BYTES = 15 * 1024 * 1024;
|
|
987
|
+
export async function localizeImageBlocksForCodex(blocks = [], { log, _fetchImpl } = {}) {
|
|
988
|
+
const { mkdtemp, writeFile } = await import("node:fs/promises");
|
|
989
|
+
const { tmpdir } = await import("node:os");
|
|
990
|
+
const path = await import("node:path");
|
|
991
|
+
const doFetch = _fetchImpl || fetch;
|
|
992
|
+
|
|
993
|
+
const imageIdx = (blocks ?? [])
|
|
994
|
+
.map((b, i) => ({ b, i }))
|
|
995
|
+
.filter(({ b }) => b?.type === "image" && b?.source?.kind === "url" && b?.source?.url);
|
|
996
|
+
if (imageIdx.length === 0) return blocks;
|
|
997
|
+
|
|
998
|
+
let dir = null;
|
|
999
|
+
const out = [...blocks];
|
|
1000
|
+
for (const { b, i } of imageIdx) {
|
|
1001
|
+
try {
|
|
1002
|
+
const controller = new AbortController();
|
|
1003
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
1004
|
+
const resp = await doFetch(b.source.url, { signal: controller.signal });
|
|
1005
|
+
clearTimeout(timer);
|
|
1006
|
+
if (!resp.ok) throw new Error(`http_${resp.status}`);
|
|
1007
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
1008
|
+
if (buf.length > CODEX_IMG_MAX_BYTES) throw new Error(`too_large_${buf.length}`);
|
|
1009
|
+
if (!dir) dir = await mkdtemp(path.join(tmpdir(), "agentlink-codex-img-"));
|
|
1010
|
+
const mime = String(b.source?.media_type || "").toLowerCase();
|
|
1011
|
+
const ext = mime.includes("jpeg") || mime.includes("jpg") ? ".jpg"
|
|
1012
|
+
: mime.includes("webp") ? ".webp"
|
|
1013
|
+
: mime.includes("gif") ? ".gif"
|
|
1014
|
+
: ".png";
|
|
1015
|
+
const file = path.join(dir, `img-${i}${ext}`);
|
|
1016
|
+
await writeFile(file, buf);
|
|
1017
|
+
out[i] = { ...b, source: { ...b.source, local_path: file } };
|
|
1018
|
+
log?.info?.("codex.image.localized", "image downloaded for view_image", {
|
|
1019
|
+
index: i, bytes: buf.length, path: file,
|
|
1020
|
+
});
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
log?.warn?.("codex.image.localize_failed", "image download failed; falling back to URL line", {
|
|
1023
|
+
index: i, err: err?.message || String(err),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return out;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export function buildCodexPromptWithAttachments(input, blocks = []) {
|
|
1031
|
+
const relayBlocks = (blocks ?? []).filter(
|
|
1032
|
+
(b) =>
|
|
1033
|
+
(b?.type === "image" || b?.type === "attachment") &&
|
|
1034
|
+
b?.source?.kind === "url" &&
|
|
1035
|
+
b?.source?.url,
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
if (relayBlocks.length === 0) return input;
|
|
1039
|
+
|
|
1040
|
+
const imageParts = [];
|
|
1041
|
+
const fileParts = [];
|
|
1042
|
+
|
|
1043
|
+
for (const block of relayBlocks) {
|
|
1044
|
+
const kind = _resolveAttachmentKind(block);
|
|
1045
|
+
const url = block.source.url;
|
|
1046
|
+
const name = block.name ?? "file";
|
|
1047
|
+
const mime = block.source?.media_type ?? block.mime ?? "application/octet-stream";
|
|
1048
|
+
const size = block.size ?? null;
|
|
1049
|
+
|
|
1050
|
+
if (kind === "image") {
|
|
1051
|
+
// P0#29: 本地化成功 → 给本地路径(配合 view_image 引导语);失败回退 URL。
|
|
1052
|
+
const localPath = block.source?.local_path;
|
|
1053
|
+
imageParts.push(`[image] ${localPath || url}`);
|
|
1054
|
+
} else if (kind === "binary") {
|
|
1055
|
+
const sizeDesc = size != null ? `${size} bytes` : "unknown size";
|
|
1056
|
+
fileParts.push(`[file: ${name}] (type: ${mime}, size: ${sizeDesc}) — binary content`);
|
|
1057
|
+
} else {
|
|
1058
|
+
// document / text → URL inline
|
|
1059
|
+
fileParts.push(`[file: ${name}] ${url}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const parts = [];
|
|
1064
|
+
|
|
1065
|
+
// 引导语。图片若已本地化(local_path),必须明确指示用 view_image 查看——
|
|
1066
|
+
// 这是 codex 把图片载入多模态上下文的唯一方式;不引导的话模型会拿 shell
|
|
1067
|
+
// 工具去 cat/curl 二进制然后开始幻觉 (P0#29)。
|
|
1068
|
+
const hasLocalImage = relayBlocks.some(
|
|
1069
|
+
(b) => _resolveAttachmentKind(b) === "image" && b.source?.local_path,
|
|
1070
|
+
);
|
|
1071
|
+
if (imageParts.length > 0 && fileParts.length > 0) {
|
|
1072
|
+
parts.push(hasLocalImage
|
|
1073
|
+
? "你收到了以下图片和文件。图片是本地文件路径,请先用 view_image 工具逐张查看,再结合内容回答:"
|
|
1074
|
+
: "你收到了以下图片和文件,请结合内容回答:");
|
|
1075
|
+
} else if (imageParts.length > 0) {
|
|
1076
|
+
parts.push(hasLocalImage
|
|
1077
|
+
? "你收到了以下图片(本地文件路径)。请先用 view_image 工具逐张查看,再结合图片内容回答:"
|
|
1078
|
+
: "你收到了以下图片,请结合图片内容回答:");
|
|
1079
|
+
} else {
|
|
1080
|
+
parts.push("你收到了以下文件,请结合内容回答:");
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
parts.push(...imageParts, ...fileParts);
|
|
1084
|
+
|
|
1085
|
+
if (input) {
|
|
1086
|
+
parts.push("---");
|
|
1087
|
+
parts.push(input);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return parts.join("\n");
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* attachment_kind を推断(relayObjectToBlock.mjs の resolveAttachmentKind と同じロジック)。
|
|
1095
|
+
* codex は relayObjectToBlock を async でインポートせず同期で動く必要があるため、
|
|
1096
|
+
* ここでは同期コピーを保持する。
|
|
1097
|
+
*/
|
|
1098
|
+
// keep in sync with relayObjectToBlock.mjs:resolveAttachmentKind (lines 50-65)
|
|
1099
|
+
function _resolveAttachmentKind(block) {
|
|
1100
|
+
const kind = block?.source?.attachment_kind;
|
|
1101
|
+
if (kind && kind !== "file") return kind;
|
|
1102
|
+
|
|
1103
|
+
const mime = block?.source?.media_type ?? block?.mime ?? "";
|
|
1104
|
+
if (block?.type === "image" || mime.startsWith("image/")) return "image";
|
|
1105
|
+
if (mime === "application/pdf") return "document";
|
|
1106
|
+
if (
|
|
1107
|
+
mime.startsWith("text/") ||
|
|
1108
|
+
mime === "application/json" ||
|
|
1109
|
+
mime === "application/yaml" ||
|
|
1110
|
+
mime === "application/x-yaml" || // CR-005: align with relayObjectToBlock.mjs
|
|
1111
|
+
mime === "application/xml" ||
|
|
1112
|
+
mime.startsWith("application/typescript") ||
|
|
1113
|
+
/^text\/x-/.test(mime)
|
|
1114
|
+
) return "text";
|
|
1115
|
+
return "binary";
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function extractUserText(request) {
|
|
1119
|
+
const input = request?.request?.input;
|
|
1120
|
+
if (typeof input === "string") return input.trim();
|
|
1121
|
+
if (Array.isArray(input)) return collectTextFromItems(input).trim();
|
|
1122
|
+
if (input && typeof input === "object") {
|
|
1123
|
+
if (typeof input.text === "string") return input.text.trim();
|
|
1124
|
+
if (input.content !== undefined) return collectTextFromAny(input.content).trim();
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof request?.clientPreviewText === "string") {
|
|
1127
|
+
return request.clientPreviewText.trim();
|
|
1128
|
+
}
|
|
1129
|
+
return "";
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function collectTextFromAny(value) {
|
|
1133
|
+
if (typeof value === "string") return value;
|
|
1134
|
+
if (Array.isArray(value)) return collectTextFromItems(value);
|
|
1135
|
+
if (value && typeof value === "object" && typeof value.text === "string") return value.text;
|
|
1136
|
+
return "";
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function collectTextFromItems(items) {
|
|
1140
|
+
const parts = [];
|
|
1141
|
+
for (const item of items) {
|
|
1142
|
+
if (typeof item === "string") { parts.push(item); continue; }
|
|
1143
|
+
if (!item || typeof item !== "object") continue;
|
|
1144
|
+
if (item.role && String(item.role).toLowerCase() !== "user") continue;
|
|
1145
|
+
if (typeof item.text === "string") { parts.push(item.text); continue; }
|
|
1146
|
+
if (item.content !== undefined) {
|
|
1147
|
+
const t = collectTextFromAny(item.content);
|
|
1148
|
+
if (t) parts.push(t);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return parts.join("\n\n");
|
|
1152
|
+
}
|