@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,1418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handleRequest.mjs — claude runtime D-C2.3
|
|
3
|
+
*
|
|
4
|
+
* 把 claude handleRequest 逻辑提取为独立模块,以便单元测试。
|
|
5
|
+
*
|
|
6
|
+
* createClaudeHandleRequest({ cwd, log, usage, dispatchHandlerOptions })
|
|
7
|
+
* → async (request, ctx) => void
|
|
8
|
+
*
|
|
9
|
+
* 路由:
|
|
10
|
+
* 1. request.__kind == "agent.run.dispatch" → unifiedDispatchHandler(返回 true)→ 结束
|
|
11
|
+
* 2. 否则 → 原有 spawn 旧路径(含 usage.addCost)
|
|
12
|
+
*
|
|
13
|
+
* dispatchHandlerOptions:
|
|
14
|
+
* relayUrl string relay 服务地址
|
|
15
|
+
* bridgeToken string bridge 鉴权 token
|
|
16
|
+
* producerFactory? Function 注入 producer(测试用)
|
|
17
|
+
* openclawInvoke? Function 注入 openclawInvoke(测试用;若未注入则使用默认)
|
|
18
|
+
* _spawnFactory? Function 注入 spawnClaude(测试用,覆盖旧路径)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawnClaude, writeUserMessage, writeToolResult } from "./launcher.mjs";
|
|
22
|
+
import { collectCustomCommands as _defaultCollectCustomCommands } from "./customCommandsCollector.mjs";
|
|
23
|
+
import { probeClaudeSystemInit } from "../_shared/cliExec.mjs";
|
|
24
|
+
import { normalizeClaudeLine, createLineStreamParser } from "./stdoutParser.mjs";
|
|
25
|
+
import { createUnifiedDispatchHandler } from "../../core/unifiedDispatchHandler.mjs";
|
|
26
|
+
import { scanWorkspacesByRuntime } from "../../core/scanWorkspaces.mjs";
|
|
27
|
+
import { expandHome } from "../../core/pathExpand.mjs";
|
|
28
|
+
import { ensureDefaultWorkspace, defaultWorkspacePath } from "../../core/defaultWorkspace.mjs";
|
|
29
|
+
import { createWorkspace as _defaultCreateWorkspace } from "../../core/createWorkspace.mjs";
|
|
30
|
+
import { createClaudeTodoTranslator } from "./todoTranslator.mjs";
|
|
31
|
+
import { createAskUserQuestionTranslator, aggregateAnswers, buildOptionMaps } from "./askUserQuestionTranslator.mjs";
|
|
32
|
+
import {
|
|
33
|
+
classifySlashCommand,
|
|
34
|
+
emitAssistantText,
|
|
35
|
+
} from "../_shared/slashCommandRouter.mjs";
|
|
36
|
+
import { createInterruptGateway } from "../../core/interruptGateway.mjs";
|
|
37
|
+
import { createActiveRunRegistry } from "../../core/activeRuns.mjs";
|
|
38
|
+
import { BoundedMap } from "../../core/boundedMap.mjs";
|
|
39
|
+
import { waitForRunResumed } from "../../core/interruptRunListener.mjs";
|
|
40
|
+
import { listSessionsForRuntime, buildSessionLabel, buildSessionSubtitle } from "../_shared/sessionPicker.mjs";
|
|
41
|
+
import {
|
|
42
|
+
buildRunStarted,
|
|
43
|
+
buildRunCompleted,
|
|
44
|
+
buildRunPaused,
|
|
45
|
+
buildBlockStarted,
|
|
46
|
+
buildBlockCompleted,
|
|
47
|
+
newBlockId,
|
|
48
|
+
newRunId,
|
|
49
|
+
translateOpenClawChunkToEnvelope,
|
|
50
|
+
newBlockState,
|
|
51
|
+
} from "../../../src-shared/envelope_builder.mjs";
|
|
52
|
+
import { createPermissionPromptServer as _defaultPermissionPromptServerFactory } from "./permissionPromptServer.mjs";
|
|
53
|
+
import { writeMcpConfig as _defaultWriteMcpConfig, deleteMcpConfig as _defaultDeleteMcpConfig } from "./mcpConfigWriter.mjs";
|
|
54
|
+
import { buildPermissionInterruptBlock } from "./permissionInterruptBlock.mjs";
|
|
55
|
+
import { fileURLToPath } from "node:url";
|
|
56
|
+
import path from "node:path";
|
|
57
|
+
|
|
58
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
const PERMISSION_PROMPT_WORKER_SCRIPT = path.join(__dirname, "permissionPromptServerWorker.mjs");
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 把已解析的 normalizedEvent(来自 stdoutParser.mjs)翻译为 onChunk 期望格式。
|
|
63
|
+
* 翻译规则:
|
|
64
|
+
* assistant_text (非 _marker) → response.output_text.delta
|
|
65
|
+
* assistant_text with stop_reason → response.completed (message_stop 信号)
|
|
66
|
+
* thinking → response.reasoning_summary_text.delta
|
|
67
|
+
* tool_use → response.tool_use.start + response.tool_use.completed (input already complete from claude stream-json)
|
|
68
|
+
* tool_use_end → response.tool_result
|
|
69
|
+
* error → response.failed
|
|
70
|
+
* 其他 → null (忽略)
|
|
71
|
+
*
|
|
72
|
+
* **subagent**: claude stdoutParser 在 assistant_text / tool_use / tool_use_end
|
|
73
|
+
* 的 payload 上携带 parent_tool_use_id(非 null 表示子 agent 输出);翻译时
|
|
74
|
+
* 透传该字段到 chunk,envelope_builder 把它放到 envelope.extensions。
|
|
75
|
+
*
|
|
76
|
+
* 返回单个 chunk 或一组 chunk 数组——为支持 tool_use 一次产 start+completed
|
|
77
|
+
* 两个事件,签名兼容数组返回。调用方需 for-of 而非直接 await。
|
|
78
|
+
*/
|
|
79
|
+
export function translateClaudeEventToChunk(ev) {
|
|
80
|
+
if (!ev) return null;
|
|
81
|
+
const parentRef = ev.payload?.parent_tool_use_id || null;
|
|
82
|
+
const withParent = (chunk) => parentRef ? { ...chunk, parent_tool_use_id: parentRef } : chunk;
|
|
83
|
+
switch (ev.kind) {
|
|
84
|
+
case "assistant_text": {
|
|
85
|
+
if (ev.payload?._marker === "turn_end" || ev.payload?.stop_reason) {
|
|
86
|
+
return { event: "response.completed" };
|
|
87
|
+
}
|
|
88
|
+
const text = ev.payload?.text || "";
|
|
89
|
+
if (!text) return null;
|
|
90
|
+
return withParent({ event: "response.output_text.delta", delta: text });
|
|
91
|
+
}
|
|
92
|
+
case "thinking": {
|
|
93
|
+
const text = String(ev.payload?.text || "");
|
|
94
|
+
if (!text) return null;
|
|
95
|
+
return withParent({ event: "response.reasoning_summary_text.delta", delta: text });
|
|
96
|
+
}
|
|
97
|
+
case "tool_use": {
|
|
98
|
+
// claude stream-json 一次性 emit 完整 tool_use block(含 input),所以
|
|
99
|
+
// 我们 emit start + completed 一对,input_complete=true。
|
|
100
|
+
const callId = String(ev.payload?.tool_id || ev.payload?.id || "");
|
|
101
|
+
const name = String(ev.payload?.name || "unknown");
|
|
102
|
+
const input = ev.payload?.input && typeof ev.payload.input === "object"
|
|
103
|
+
? ev.payload.input : {};
|
|
104
|
+
return [
|
|
105
|
+
withParent({ event: "response.tool_use.start", call_id: callId, name, input }),
|
|
106
|
+
withParent({ event: "response.tool_use.completed", call_id: callId, name, input }),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
case "tool_use_end": {
|
|
110
|
+
// claude stream-json 的 tool_result 块挂在 user 消息上, stdoutParser
|
|
111
|
+
// 已经把 block.content 提取为 `result` 文本 (见 stdoutParser.mjs:70)。
|
|
112
|
+
const callId = String(ev.payload?.tool_id || "");
|
|
113
|
+
// stdoutParser uses `result` (string); fallback to `content` for forward-compat.
|
|
114
|
+
const resultText = typeof ev.payload?.result === "string"
|
|
115
|
+
? ev.payload.result
|
|
116
|
+
: (typeof ev.payload?.content === "string" ? ev.payload.content : "");
|
|
117
|
+
const isError = !!ev.payload?.is_error;
|
|
118
|
+
const output = resultText
|
|
119
|
+
? [{ type: "text", format: "plain", text: resultText }]
|
|
120
|
+
: [];
|
|
121
|
+
return withParent({
|
|
122
|
+
event: "response.tool_result",
|
|
123
|
+
call_id: callId,
|
|
124
|
+
output,
|
|
125
|
+
is_error: isError,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
case "error":
|
|
129
|
+
return {
|
|
130
|
+
event: "response.failed",
|
|
131
|
+
error: { message: String(ev.payload?.err || ev.payload?.message || "claude error") },
|
|
132
|
+
};
|
|
133
|
+
default:
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 构造 --resume 注入时的答案文本(传给 claude 的 user message)。
|
|
140
|
+
*
|
|
141
|
+
* 规格(spec design.md §2.3):
|
|
142
|
+
* - 单问:`[你提问了:{question}]\n我的选择是:{answer}`
|
|
143
|
+
* - 多问:`[你提问了以下问题,以下是我的回答:]\n1. {Q1}:{A1}\n2. {Q2}:{A2}...`
|
|
144
|
+
*
|
|
145
|
+
* answer 格式化:
|
|
146
|
+
* - free_text_response → 直接使用
|
|
147
|
+
* - selected_ids / selected_option_ids → 反查 label,顿号拼接
|
|
148
|
+
* - option_id → 反查 label(fallback 到 option_id 本身)
|
|
149
|
+
*
|
|
150
|
+
* @param {Array<{question: string, options?: Array<{label: string}>}>} questions
|
|
151
|
+
* @param {Array<object>} resumeValues 每个 block 的 resume value,与 questions 一一对应
|
|
152
|
+
* @param {Array<Record<string, string>>} optionMaps 每题的 option_id→label 映射
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
export function buildAnswerInjectionText(questions, resumeValues, optionMaps) {
|
|
156
|
+
const formatAnswer = (value, map) => {
|
|
157
|
+
const v = value ?? {};
|
|
158
|
+
// free_text_response 优先
|
|
159
|
+
if (typeof v.free_text_response === "string" && v.free_text_response.trim()) {
|
|
160
|
+
return v.free_text_response.trim();
|
|
161
|
+
}
|
|
162
|
+
// 多选
|
|
163
|
+
if (Array.isArray(v.selected_ids) && v.selected_ids.length > 0) {
|
|
164
|
+
return v.selected_ids.map((id) => (map?.[id] ?? id)).join("、");
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(v.selected_option_ids) && v.selected_option_ids.length > 0) {
|
|
167
|
+
return v.selected_option_ids.map((id) => (map?.[id] ?? id)).join("、");
|
|
168
|
+
}
|
|
169
|
+
// 单选
|
|
170
|
+
if (typeof v.option_id === "string" && v.option_id) {
|
|
171
|
+
return map?.[v.option_id] ?? v.option_id;
|
|
172
|
+
}
|
|
173
|
+
return "";
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (questions.length === 1) {
|
|
177
|
+
const q = questions[0].question ?? "";
|
|
178
|
+
const a = formatAnswer(resumeValues[0], optionMaps?.[0] ?? {});
|
|
179
|
+
return `[你提问了:${q}]\n我的选择是:${a}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 多问场景
|
|
183
|
+
const lines = ["[你提问了以下问题,以下是我的回答:]"];
|
|
184
|
+
for (let i = 0; i < questions.length; i++) {
|
|
185
|
+
const q = questions[i].question ?? "";
|
|
186
|
+
const a = formatAnswer(resumeValues[i], optionMaps?.[i] ?? {});
|
|
187
|
+
lines.push(`${i + 1}. ${q}:${a}`);
|
|
188
|
+
}
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {{
|
|
194
|
+
* cwd: string,
|
|
195
|
+
* log: { info, warn, error, child? },
|
|
196
|
+
* usage: { addCost },
|
|
197
|
+
* dispatchHandlerOptions: {
|
|
198
|
+
* relayUrl: string,
|
|
199
|
+
* bridgeToken: string,
|
|
200
|
+
* producerFactory?: Function,
|
|
201
|
+
* openclawInvoke?: Function,
|
|
202
|
+
* _spawnFactory?: Function, // 仅测试用
|
|
203
|
+
* },
|
|
204
|
+
* }} opts
|
|
205
|
+
* @returns {(request: any, ctx: any) => Promise<void>}
|
|
206
|
+
*/
|
|
207
|
+
export function createClaudeHandleRequest({ cwd, log, usage, bridgeCtx, dispatchHandlerOptions, scanner, _createWorkspace, _collectCustomCommands }) {
|
|
208
|
+
const {
|
|
209
|
+
relayUrl,
|
|
210
|
+
bridgeToken,
|
|
211
|
+
producerFactory,
|
|
212
|
+
openclawInvoke: injectedOpenclawInvoke,
|
|
213
|
+
_spawnFactory,
|
|
214
|
+
// approval 模式注入点(测试用)
|
|
215
|
+
approvalGateway: injectedApprovalGateway,
|
|
216
|
+
permissionMode: injectedPermissionMode,
|
|
217
|
+
_permissionPromptServerFactory,
|
|
218
|
+
_writeMcpConfig,
|
|
219
|
+
_deleteMcpConfig,
|
|
220
|
+
// waitForRunResumed 注入点(测试用;生产走顶层 import)
|
|
221
|
+
_waitForRunResumed: injectedWaitForRunResumed,
|
|
222
|
+
} = dispatchHandlerOptions ?? {};
|
|
223
|
+
|
|
224
|
+
// waitForRunResumed 可注入(测试用),生产走 module-level import
|
|
225
|
+
const mkWaitForRunResumed = injectedWaitForRunResumed ?? waitForRunResumed;
|
|
226
|
+
|
|
227
|
+
// approval 功能依赖注入(允许测试注入,生产走默认)
|
|
228
|
+
const mkPermissionPromptServer = _permissionPromptServerFactory ?? _defaultPermissionPromptServerFactory;
|
|
229
|
+
const doWriteMcpConfig = _writeMcpConfig ?? _defaultWriteMcpConfig;
|
|
230
|
+
const doDeleteMcpConfig = _deleteMcpConfig ?? _defaultDeleteMcpConfig;
|
|
231
|
+
// bridge 状态(claude CLI 版本 / gw_id / model), 供 slash 路由器在 bridge-synth
|
|
232
|
+
// 分支(/help /status /model 等 CLI 不支持的)合成响应文本时引用。
|
|
233
|
+
const claudeCliVersion = bridgeCtx?.version || null;
|
|
234
|
+
const claudeGwId = bridgeCtx?.gwId || null;
|
|
235
|
+
const claudeModel = bridgeCtx?.model || null;
|
|
236
|
+
|
|
237
|
+
// spawn 工厂(可被测试注入)
|
|
238
|
+
const mkSpawn = _spawnFactory ?? ((opts) => spawnClaude(opts));
|
|
239
|
+
|
|
240
|
+
// thread_id → 上次 claude 给的 session_id 缓存。每条 dispatch 用 thread_id
|
|
241
|
+
// 查 map,命中就 `claude --resume <sid>` 续上下文,没命中就开新 session
|
|
242
|
+
// 并在 session_ready 事件里捕获 session_id 回写。
|
|
243
|
+
// 进程级 in-memory 即可——bridge 重启会丢,但前端再发一条会自然 reseed;
|
|
244
|
+
// 持久化方案待后续与 nacos / 本地 ~/.agentlink/sessions.json 整合。
|
|
245
|
+
// audit 修复:thread 数随用户/会话增长,进程级 Map 改 LRU+TTL
|
|
246
|
+
//(7 天未访问的 thread 续接缓存丢弃——丢了也只是开新 session,可接受)。
|
|
247
|
+
const threadSessionMap = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 });
|
|
248
|
+
// thread_id → 选中历史会话的项目 cwd。claude session 按项目目录隔离,
|
|
249
|
+
// /resume 选中的会话属于其原始项目;下次 spawn 必须在该 cwd 下才能 --resume 成功。
|
|
250
|
+
const threadSessionCwdMap = new BoundedMap({ maxSize: 5_000, ttlMs: 7 * 24 * 60 * 60 * 1000 });
|
|
251
|
+
|
|
252
|
+
// interrupt 闸门 — 跨请求共享,用于 /resume picker 挂起/恢复。
|
|
253
|
+
const interruptGateway = createInterruptGateway({ log });
|
|
254
|
+
|
|
255
|
+
// 活跃 run 登记表 — App 停止按钮 (sessions.abort RPC) 据此 kill 子进程。
|
|
256
|
+
const activeRuns = createActiveRunRegistry({ log });
|
|
257
|
+
|
|
258
|
+
// 默认 openclawInvoke:spawn claude --print 子进程,解析 stdout,翻译事件
|
|
259
|
+
// Task 3.5: 加入 blocks 参数(来自 dispatch payload),透传给 writeUserMessage 以支持多模态
|
|
260
|
+
// workspace-aware cwd: 若 workspace_path 非空且目录存在,spawn 用它作 cwd;
|
|
261
|
+
// 否则 fallback 到 defaultWorkspace(不再用工厂 cwd,修 service 模式 "/" bug)。
|
|
262
|
+
const defaultOpenclawInvoke = async ({ thread_id, run_id, input, blocks = [], workspace_path: reqWorkspacePath, session_key = null, onChunk: _rawOnChunk }) => {
|
|
263
|
+
// per-request cwd 解析(align-runtime-default-workspace):
|
|
264
|
+
// 1. workspace_path 非空 → expandHome + existsSync → 用它
|
|
265
|
+
// 2. workspace_path 给定但不存在 → fallback 到 defaultWorkspace,emit cwd_fallback 可观测
|
|
266
|
+
// 3. workspace_path 未给 → 直接用 defaultWorkspace
|
|
267
|
+
// 注:factory cwd 不再作为兜底(保留作显式 --cwd 入口)
|
|
268
|
+
let spawnCwd;
|
|
269
|
+
let cwdFallback = null;
|
|
270
|
+
|
|
271
|
+
const { existsSync } = await import("node:fs");
|
|
272
|
+
|
|
273
|
+
if (reqWorkspacePath && typeof reqWorkspacePath === "string" && reqWorkspacePath.trim()) {
|
|
274
|
+
const resolved = expandHome(reqWorkspacePath.trim());
|
|
275
|
+
if (existsSync(resolved)) {
|
|
276
|
+
spawnCwd = resolved;
|
|
277
|
+
log.info?.("claude.spawn.cwd.workspace", "using workspace_path as cwd", {
|
|
278
|
+
cwd: resolved, source: "workspace_path",
|
|
279
|
+
});
|
|
280
|
+
} else {
|
|
281
|
+
// workspace_path 给定但不存在 → fallback 到默认工作区,fallback 可观测
|
|
282
|
+
spawnCwd = await ensureDefaultWorkspace("claude");
|
|
283
|
+
cwdFallback = {
|
|
284
|
+
actual_cwd: spawnCwd,
|
|
285
|
+
requested_cwd: reqWorkspacePath,
|
|
286
|
+
reason: `workspace_path '${reqWorkspacePath}' does not exist after expand, falling back to default workspace`,
|
|
287
|
+
};
|
|
288
|
+
log.warn?.("claude.spawn.cwd.not_found", "workspace_path does not exist, falling back to default workspace", {
|
|
289
|
+
workspace_path: reqWorkspacePath, fallback_cwd: spawnCwd,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
// 未给 workspace_path → 用默认工作区(lazy ensure,兜底 service 模式 "/" bug)
|
|
294
|
+
spawnCwd = await ensureDefaultWorkspace("claude");
|
|
295
|
+
log.info?.("claude.spawn.cwd.default", "no workspace_path, using default workspace", {
|
|
296
|
+
cwd: spawnCwd, source: "default_workspace",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// /resume picker 选中的历史会话属于其原始项目目录;claude session 按 cwd 隔离,
|
|
300
|
+
// 必须在该会话的项目 cwd 下才能 `--resume <id>` 命中。覆盖 spawnCwd。
|
|
301
|
+
if (thread_id && threadSessionCwdMap.has(thread_id)) {
|
|
302
|
+
const sessCwd = threadSessionCwdMap.get(thread_id);
|
|
303
|
+
if (sessCwd && typeof sessCwd === "string") {
|
|
304
|
+
if (existsSync(sessCwd)) {
|
|
305
|
+
spawnCwd = sessCwd;
|
|
306
|
+
log.info?.("claude.spawn.cwd.resume_session",
|
|
307
|
+
"using picked session project cwd for --resume", { cwd: sessCwd });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// 自动 attach 扫描器: 拦 response.output_text.delta 喂 accumulator, fire-and-forget
|
|
312
|
+
// 不阻塞流式输出。scanner 由工厂注入(bootstrapAgentlinkTools.scanner)。
|
|
313
|
+
const _acc = scanner?.createTurnAccumulator({ requestId: run_id });
|
|
314
|
+
const onChunk = _acc
|
|
315
|
+
? async (event) => {
|
|
316
|
+
if (event && event.event === "response.output_text.delta" && typeof event.delta === "string") {
|
|
317
|
+
try { _acc.append(event.delta); } catch { /* swallow */ }
|
|
318
|
+
}
|
|
319
|
+
return _rawOnChunk(event);
|
|
320
|
+
}
|
|
321
|
+
: _rawOnChunk;
|
|
322
|
+
|
|
323
|
+
// fallback 可观测:workspace_path 给定但不存在时,emit run.started 含 cwd_fallback
|
|
324
|
+
if (cwdFallback) {
|
|
325
|
+
await onChunk({
|
|
326
|
+
event: "__envelopes",
|
|
327
|
+
envelopes: [buildRunStarted(run_id, { options: { cwd_fallback: cwdFallback } })],
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// approval 模式资源(在 try 块外声明,以便 finally 块访问)
|
|
332
|
+
let _approvalPermServer = null;
|
|
333
|
+
let _approvalMcpConfigPath = null;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// ── Slash command 拦截(bridge 层)─────────────────────────────────────
|
|
337
|
+
// 设计原则(2026-05-20 重写):
|
|
338
|
+
// claude CLI 在 `--print --input-format stream-json` 模式下对 slash 命令
|
|
339
|
+
// 有两类反应:
|
|
340
|
+
// A) CLI 真支持(/cost /usage /context /init /review 等): 发 synthetic
|
|
341
|
+
// assistant 消息(model="<synthetic>"), 内容是真实结果。stdoutParser
|
|
342
|
+
// 的 isSyntheticSlashResponse 分支会把文本 emit 给 bridge。
|
|
343
|
+
// B) CLI 不支持(/help /status /model /agents /skills /memory 等):
|
|
344
|
+
// 同样发 synthetic 消息但内容是 "<cmd> isn't available in this
|
|
345
|
+
// environment.", 给用户看一句"不支持"对远程会话体验不友好。
|
|
346
|
+
//
|
|
347
|
+
// 所以 bridge 端策略:
|
|
348
|
+
// - /clear → client-reset(必须清 thread→session map)
|
|
349
|
+
// - /compact → passthrough(CLI 真跑 compaction)
|
|
350
|
+
// - B 类(CLI 拒绝响应的)→ bridge-synth, 用已有 bridge 状态合成等价输出
|
|
351
|
+
// - 其它(/cost /usage /context ...) → not-slash, 让 CLI 自己说话
|
|
352
|
+
const slash = await classifySlashCommand(input, "claude", {
|
|
353
|
+
version: claudeCliVersion,
|
|
354
|
+
// 必须用本次请求解析出的 spawnCwd(工作区目录),不能用 factory cwd:
|
|
355
|
+
// init 探活的 slash_commands 清单含工作区级自定义命令(.claude/commands/),
|
|
356
|
+
// 用错目录会把非默认工作区的自定义命令全部误判「不支持」(P0#25)。
|
|
357
|
+
cwd: spawnCwd,
|
|
358
|
+
gwId: claudeGwId,
|
|
359
|
+
threadId: thread_id || null,
|
|
360
|
+
sessionId: thread_id ? threadSessionMap.get(thread_id) || null : null,
|
|
361
|
+
model: claudeModel,
|
|
362
|
+
});
|
|
363
|
+
if (slash.kind === "client-reset") {
|
|
364
|
+
if (thread_id) {
|
|
365
|
+
threadSessionMap.delete(thread_id);
|
|
366
|
+
threadSessionCwdMap.delete(thread_id);
|
|
367
|
+
log.info?.("claude.slash.clear",
|
|
368
|
+
"client-reset slash command, dropped thread→session mapping",
|
|
369
|
+
{ thread_id, command: slash.command });
|
|
370
|
+
}
|
|
371
|
+
await emitAssistantText(onChunk, slash.ackText);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (slash.kind === "bridge-synth") {
|
|
375
|
+
log.info?.("claude.slash.synth",
|
|
376
|
+
"bridge-synthesized slash response (CLI rejects in --print mode)",
|
|
377
|
+
{ thread_id, command: slash.command });
|
|
378
|
+
await emitAssistantText(onChunk, slash.responseText);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── /resume interrupt session picker ────────────────────────────────────
|
|
383
|
+
if (slash.kind === "interrupt-session-pick") {
|
|
384
|
+
log.info?.("claude.slash.resume_picker", "/resume: starting interrupt session picker", { thread_id, run_id });
|
|
385
|
+
|
|
386
|
+
// 默认只列「当前工作区」(spawnCwd) 下的历史会话;跨项目会话不再混入。
|
|
387
|
+
const sessions = await listSessionsForRuntime("claude", { filterCwd: spawnCwd });
|
|
388
|
+
|
|
389
|
+
if (sessions.length === 0) {
|
|
390
|
+
// 本工作区无可恢复会话 → 文本提示 + run.completed(无 run.paused)
|
|
391
|
+
log.info?.("claude.slash.resume_picker.empty", "/resume: no sessions in current workspace, sending text fallback", { thread_id, cwd: spawnCwd });
|
|
392
|
+
await emitAssistantText(onChunk, "本工作区暂无历史会话,直接开始新对话吧");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 构造 interrupt block options
|
|
397
|
+
const blockId = newBlockId();
|
|
398
|
+
const options = sessions.map((s) => ({
|
|
399
|
+
id: s.session_id,
|
|
400
|
+
label: buildSessionLabel(s),
|
|
401
|
+
description: buildSessionSubtitle(s),
|
|
402
|
+
}));
|
|
403
|
+
|
|
404
|
+
// 发 run.started → content_block.started(interrupt) → run.paused
|
|
405
|
+
await onChunk({
|
|
406
|
+
event: "__envelopes",
|
|
407
|
+
envelopes: [
|
|
408
|
+
buildRunStarted(run_id),
|
|
409
|
+
buildBlockStarted(run_id, {
|
|
410
|
+
id: blockId,
|
|
411
|
+
type: "interrupt",
|
|
412
|
+
subtype: "question",
|
|
413
|
+
prompt: "选择要续接的历史会话",
|
|
414
|
+
options,
|
|
415
|
+
selection_mode: "single",
|
|
416
|
+
}),
|
|
417
|
+
buildRunPaused(run_id, [blockId]),
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// 非阻塞:发完 interrupt + run.paused 后**立即返回**释放 long-poll worker,
|
|
422
|
+
// 否则单 worker 会卡在 await 上,导致其它会话的 /resume 全部排队 hang。
|
|
423
|
+
// resume 由 relay stream 监听器异步处理:收到 run.resumed → 发收尾 + 记 session。
|
|
424
|
+
if (relayUrl && bridgeToken && thread_id) {
|
|
425
|
+
waitForRunResumed({
|
|
426
|
+
relayUrl,
|
|
427
|
+
bridgeToken,
|
|
428
|
+
threadId: thread_id,
|
|
429
|
+
runId: run_id,
|
|
430
|
+
interruptId: blockId,
|
|
431
|
+
log,
|
|
432
|
+
}).then(async ({ value }) => {
|
|
433
|
+
const decisionValue = value;
|
|
434
|
+
// 发 content_block.completed + run.completed(收尾,独立 POST /events)
|
|
435
|
+
await onChunk({
|
|
436
|
+
event: "__envelopes",
|
|
437
|
+
envelopes: [
|
|
438
|
+
buildBlockCompleted(run_id, {
|
|
439
|
+
id: blockId,
|
|
440
|
+
type: "interrupt",
|
|
441
|
+
// schema 要求 interrupt 块带 type/subtype/prompt(completed 也校验),
|
|
442
|
+
// 否则 oneOf 退化到 text 分支报 missing format/text。
|
|
443
|
+
subtype: "question",
|
|
444
|
+
prompt: "选择要续接的历史会话",
|
|
445
|
+
status: "completed",
|
|
446
|
+
value: decisionValue,
|
|
447
|
+
}),
|
|
448
|
+
// stop_reason 必须是协议枚举值;控制型 run 正常结束用 end_turn。
|
|
449
|
+
buildRunCompleted(run_id, "end_turn"),
|
|
450
|
+
],
|
|
451
|
+
});
|
|
452
|
+
// 写入 threadSessionMap(下次消息走 --resume <session_id>)+ 记下该会话项目 cwd
|
|
453
|
+
const selectedSessionId = decisionValue?.option_id ?? null;
|
|
454
|
+
if (selectedSessionId) {
|
|
455
|
+
threadSessionMap.set(thread_id, selectedSessionId);
|
|
456
|
+
const picked = sessions.find((s) => s.session_id === selectedSessionId);
|
|
457
|
+
if (picked?.cwd) {
|
|
458
|
+
threadSessionCwdMap.set(thread_id, picked.cwd);
|
|
459
|
+
}
|
|
460
|
+
log.info?.("claude.slash.resume_picker.done", "session selected for next run", {
|
|
461
|
+
thread_id, session_id: selectedSessionId, session_cwd: picked?.cwd || null,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}).catch((err) => {
|
|
465
|
+
log.warn?.("claude.slash.resume_picker.listener_err", "run.resumed listener error", {
|
|
466
|
+
run_id, err: err?.message || String(err),
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
log.warn?.("claude.slash.resume_picker.no_listener",
|
|
471
|
+
"missing relay/token/thread; cannot await resume", { run_id });
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// not-slash / passthrough(/compact): 继续走 spawn, claude CLI 自己应答
|
|
477
|
+
const isPassthroughSlash = slash.kind === "passthrough" || slash.kind === "prompt-rewrite";
|
|
478
|
+
// prompt-rewrite(P1#22): /diff /recap 等 TUI 命令改写成等效自然语言真跑
|
|
479
|
+
const effectiveInput = slash.kind === "prompt-rewrite" ? slash.rewrittenInput : input;
|
|
480
|
+
if (slash.kind === "prompt-rewrite") {
|
|
481
|
+
log.info?.("claude.slash.rewrite", "slash rewritten to natural-language prompt",
|
|
482
|
+
{ thread_id, command: slash.command });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── approval 模式 ─────────────────────────────────────────────────────────
|
|
486
|
+
// 读取权限模式:injectedPermissionMode(测试注入)> env var > bypass 默认
|
|
487
|
+
const effectivePermissionMode = injectedPermissionMode
|
|
488
|
+
|| process.env.AGENTLINK_CLAUDE_PERMISSION_MODE
|
|
489
|
+
|| "bypassPermissions";
|
|
490
|
+
const isApprovalMode = effectivePermissionMode === "approval" || effectivePermissionMode === "default";
|
|
491
|
+
|
|
492
|
+
if (isApprovalMode && injectedApprovalGateway) {
|
|
493
|
+
// 写 MCP config 文件
|
|
494
|
+
try {
|
|
495
|
+
_approvalMcpConfigPath = await doWriteMcpConfig({
|
|
496
|
+
runId: run_id,
|
|
497
|
+
serverScriptPath: PERMISSION_PROMPT_WORKER_SCRIPT,
|
|
498
|
+
});
|
|
499
|
+
log.info?.("claude.approval.mcp_config_written", "MCP config written", {
|
|
500
|
+
run_id, path: _approvalMcpConfigPath,
|
|
501
|
+
});
|
|
502
|
+
} catch (err) {
|
|
503
|
+
log.warn?.("claude.approval.mcp_config_failed", "failed to write MCP config, falling back to bypass", {
|
|
504
|
+
err: err?.message || String(err),
|
|
505
|
+
});
|
|
506
|
+
_approvalMcpConfigPath = null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (_approvalMcpConfigPath) {
|
|
510
|
+
try {
|
|
511
|
+
_approvalPermServer = mkPermissionPromptServer({ approvalGateway: injectedApprovalGateway, log });
|
|
512
|
+
await _approvalPermServer.start();
|
|
513
|
+
log.info?.("claude.approval.perm_server_started", "permission prompt server started", { run_id });
|
|
514
|
+
|
|
515
|
+
// pull 模式:监听 approvalGateway 上的 openRequest,发 interrupt block + run.paused
|
|
516
|
+
// 通过轮询 getLastRequestId 并监听 promise 来实现 pull
|
|
517
|
+
// 注:实际上 approvalGateway.openRequest 是同步注册的;permServer 的 IPC 消息
|
|
518
|
+
// 会触发 approvalGateway.openRequest,然后由 permServer 内部管理 promise。
|
|
519
|
+
// handleRequest 通过 onRequest 监听器(间隔轮询 lastRequestId)来发 interrupt block。
|
|
520
|
+
|
|
521
|
+
// 启动 approval 监听器(fire-and-forget,直到 run 结束)
|
|
522
|
+
const approvalListener = (async () => {
|
|
523
|
+
let lastSeenId = null;
|
|
524
|
+
// 轮询直到 permServer 被 stop(child=null)或 run 结束
|
|
525
|
+
while (_approvalPermServer) {
|
|
526
|
+
const currentId = _approvalPermServer.getLastRequestId();
|
|
527
|
+
if (currentId && currentId !== lastSeenId) {
|
|
528
|
+
lastSeenId = currentId;
|
|
529
|
+
// 找到新的 openRequest,发 interrupt block
|
|
530
|
+
try {
|
|
531
|
+
// 从 approvalGateway 内部取 meta(tool_name, tool_input)
|
|
532
|
+
// approvalGateway 没有直接暴露 meta,但 permServer 的 worker 已经把
|
|
533
|
+
// meta 传给 approvalGateway.openRequest;我们通过 approvalGateway.openRequest
|
|
534
|
+
// 调用时传入的 meta 来构造 block。
|
|
535
|
+
// 为了取到 meta,我们在 permServer IPC 收到 openRequest 时也通知 handleRequest。
|
|
536
|
+
// 简单方案:permServer 额外暴露 getLastMeta()。
|
|
537
|
+
const meta = _approvalPermServer.getLastMeta ? _approvalPermServer.getLastMeta() : {};
|
|
538
|
+
const blockId = newBlockId();
|
|
539
|
+
const block = buildPermissionInterruptBlock({
|
|
540
|
+
toolName: meta.tool_name || "unknown",
|
|
541
|
+
toolInput: meta.tool_input || {},
|
|
542
|
+
blockId,
|
|
543
|
+
runId: run_id,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// 发 content_block.started + run.paused
|
|
547
|
+
await onChunk({
|
|
548
|
+
event: "__envelopes",
|
|
549
|
+
envelopes: [
|
|
550
|
+
buildBlockStarted(run_id, block),
|
|
551
|
+
buildRunPaused(run_id, [blockId]),
|
|
552
|
+
],
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
log.info?.("claude.approval.interrupt_sent", "approval interrupt block sent", {
|
|
556
|
+
run_id, block_id: blockId, tool: meta.tool_name || "",
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// waitForRunResumed:等待用户决策
|
|
560
|
+
if (relayUrl && bridgeToken && thread_id) {
|
|
561
|
+
waitForRunResumed({
|
|
562
|
+
relayUrl,
|
|
563
|
+
bridgeToken,
|
|
564
|
+
threadId: thread_id,
|
|
565
|
+
runId: run_id,
|
|
566
|
+
interruptId: blockId,
|
|
567
|
+
log,
|
|
568
|
+
}).then(({ value }) => {
|
|
569
|
+
const behavior = value?.behavior || "deny";
|
|
570
|
+
const decision = behavior === "allow" ? "approve" : "reject";
|
|
571
|
+
injectedApprovalGateway.settle(currentId, decision, `user-${behavior}`);
|
|
572
|
+
log.info?.("claude.approval.settled", "approval settled from run.resumed", {
|
|
573
|
+
run_id, request_id: currentId, decision,
|
|
574
|
+
});
|
|
575
|
+
}).catch((err) => {
|
|
576
|
+
log.warn?.("claude.approval.listener_err", "waitForRunResumed error, auto-reject", {
|
|
577
|
+
run_id, err: err?.message || String(err),
|
|
578
|
+
});
|
|
579
|
+
injectedApprovalGateway.settle(currentId, "reject", "listener-error");
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
} catch (err) {
|
|
583
|
+
log.warn?.("claude.approval.block_err", "failed to send approval interrupt block", {
|
|
584
|
+
err: err?.message || String(err),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
589
|
+
}
|
|
590
|
+
})();
|
|
591
|
+
// 不 await,fire-and-forget
|
|
592
|
+
} catch (err) {
|
|
593
|
+
log.warn?.("claude.approval.perm_server_failed", "permission prompt server failed to start, falling back", {
|
|
594
|
+
err: err?.message || String(err),
|
|
595
|
+
});
|
|
596
|
+
_approvalPermServer = null;
|
|
597
|
+
await doDeleteMcpConfig(_approvalMcpConfigPath).catch(() => {});
|
|
598
|
+
_approvalMcpConfigPath = null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 包成 attemptOnce,便于 --resume 失败时清缓存后重试一次无 resume 路径
|
|
604
|
+
async function attemptOnce({ resumeSessionId, isRetry }) {
|
|
605
|
+
let handle;
|
|
606
|
+
try {
|
|
607
|
+
const cwdSource = spawnCwd !== cwd ? "workspace_path" : "default";
|
|
608
|
+
// approval 模式:注入 extraArgs
|
|
609
|
+
const approvalExtraArgs = (isApprovalMode && _approvalMcpConfigPath) ? [
|
|
610
|
+
"--permission-mode", "default",
|
|
611
|
+
"--permission-prompt-tool", "mcp__agentlink-permission__agentlink_permission_prompt",
|
|
612
|
+
"--setting-sources", "project",
|
|
613
|
+
"--mcp-config", _approvalMcpConfigPath,
|
|
614
|
+
] : [];
|
|
615
|
+
handle = mkSpawn({ cwd: spawnCwd, log, resumeSessionId: resumeSessionId || undefined, cwdSource, extraArgs: approvalExtraArgs });
|
|
616
|
+
} catch (err) {
|
|
617
|
+
const msg = err?.message || String(err);
|
|
618
|
+
log.error?.("child.spawn.failed", "spawn claude failed", { err: msg, isRetry: !!isRetry });
|
|
619
|
+
return { spawnError: msg };
|
|
620
|
+
}
|
|
621
|
+
// P0 停止按钮:登记活跃 run,App 的 sessions.abort RPC 据此 kill 子进程
|
|
622
|
+
let abortedByRpc = false;
|
|
623
|
+
const unregister = activeRuns.register({
|
|
624
|
+
sessionKey: session_key,
|
|
625
|
+
threadId: thread_id,
|
|
626
|
+
runId: run_id,
|
|
627
|
+
kill: (reason) => {
|
|
628
|
+
abortedByRpc = true;
|
|
629
|
+
try { handle.kill(reason); } catch { /* already dead */ }
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
if (resumeSessionId) {
|
|
633
|
+
log.info?.("claude.session.resume", "resuming claude session for thread", {
|
|
634
|
+
thread_id, session_id: resumeSessionId,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// 写 user 消息进 stdin(含 multimodal blocks)
|
|
639
|
+
try {
|
|
640
|
+
await writeUserMessage(handle.child, effectiveInput, blocks);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
const msg = err?.message || String(err);
|
|
643
|
+
log.error?.("child.stdin.failed", "write user message failed", { err: msg });
|
|
644
|
+
try { handle.kill("stdin-failed"); } catch {}
|
|
645
|
+
unregister();
|
|
646
|
+
return { stdinError: msg };
|
|
647
|
+
}
|
|
648
|
+
// 关 stdin 发 EOF —— claude --print --input-format stream-json 在没收到
|
|
649
|
+
// EOF 时会等下一条 user 消息,导致响应憋着不退、App 端永远 loading。
|
|
650
|
+
try { handle.child.stdin.end(); } catch {}
|
|
651
|
+
|
|
652
|
+
// 跟踪本轮:assistant 是否产出过 chunk + 抓到的 session_id(还没决定要不要
|
|
653
|
+
// 落 threadSessionMap,因为 slash 命令 / 失败情况下 claude 没写 jsonl 文件,
|
|
654
|
+
// 落了下一轮 --resume 必败)。
|
|
655
|
+
let sawAssistantOutput = false;
|
|
656
|
+
let sawNonSyntheticOutput = false; // 区分 synthetic slash 响应 vs 真 LLM turn
|
|
657
|
+
let pendingSessionId = null;
|
|
658
|
+
|
|
659
|
+
const todoTranslator = createClaudeTodoTranslator(run_id);
|
|
660
|
+
const askTranslator = createAskUserQuestionTranslator({ log, runId: run_id });
|
|
661
|
+
// 未决 AskUserQuestion 批次登记。真 CLI headless 下 AskUserQuestion 由
|
|
662
|
+
// CLI 自答 is_error tool_result 自取消、turn 直接结束(实测 2.1.173,
|
|
663
|
+
// 即使 stdin 保持打开也不等外部回填)——child 退出时必须 reconcile,
|
|
664
|
+
// 否则 run 永远停在 paused:relay 行卡 running、App 转圈不复位。
|
|
665
|
+
const pendingAskBatches = [];
|
|
666
|
+
let sawResponseCompleted = false;
|
|
667
|
+
let childExited = false;
|
|
668
|
+
let askTerminalEmitted = false;
|
|
669
|
+
// run 因 ask 暂停后的终态兜底:dispatcher 在 paused 时跳过合成收尾,
|
|
670
|
+
// 这里补一条 response.completed 让它闭合文本块 + 发 run.completed。
|
|
671
|
+
// sawResponseCompleted / askTerminalEmitted 双闸防止双终态。
|
|
672
|
+
const ensureAskTerminal = async () => {
|
|
673
|
+
if (askTerminalEmitted || sawResponseCompleted) return;
|
|
674
|
+
askTerminalEmitted = true;
|
|
675
|
+
try { await onChunk({ event: "response.completed" }); } catch { /* ignore */ }
|
|
676
|
+
};
|
|
677
|
+
const parser = createLineStreamParser({
|
|
678
|
+
onObject: async (obj) => {
|
|
679
|
+
const events = normalizeClaudeLine(obj);
|
|
680
|
+
for (const ev of events) {
|
|
681
|
+
if (ev.kind === "session_ready" && thread_id) {
|
|
682
|
+
const sid = ev.payload?.session_id;
|
|
683
|
+
if (typeof sid === "string" && sid) {
|
|
684
|
+
// 不直接写 map;等 exit code=0 且 sawNonSyntheticOutput=true 才确认
|
|
685
|
+
pendingSessionId = sid;
|
|
686
|
+
}
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
// 标记真有 assistant 内容(include text/thinking/tool_use;不算
|
|
690
|
+
// response.completed 这种生命周期事件)
|
|
691
|
+
if (ev.kind === "assistant_text" && !ev.payload?._marker && (ev.payload?.text || "").length > 0) {
|
|
692
|
+
sawAssistantOutput = true;
|
|
693
|
+
// synthetic slash 响应不算"真 LLM turn"——claude 没写 jsonl,
|
|
694
|
+
// session_id 不能续 resume。
|
|
695
|
+
if (!ev.payload?.synthetic) {
|
|
696
|
+
sawNonSyntheticOutput = true;
|
|
697
|
+
}
|
|
698
|
+
} else if (ev.kind === "thinking" || ev.kind === "tool_use" || ev.kind === "tool_use_end") {
|
|
699
|
+
sawAssistantOutput = true;
|
|
700
|
+
sawNonSyntheticOutput = true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ── AskUserQuestion 拦截(todoTranslator 之后) ─────────────────
|
|
704
|
+
const askResult = askTranslator.intercept(ev);
|
|
705
|
+
if (askResult !== null) {
|
|
706
|
+
if (askResult.consumed) {
|
|
707
|
+
// tool_use_end 被吞噬,跳过
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
// AskUserQuestion 被拦截:发 N 个 block.started + run.paused,异步等待
|
|
711
|
+
const { toolId, questions, batchGroupId, blocks } = askResult;
|
|
712
|
+
const batchBlockIds = blocks.map((b) => b.id);
|
|
713
|
+
const optionMaps = buildOptionMaps(blocks);
|
|
714
|
+
const askBatch = {
|
|
715
|
+
toolId,
|
|
716
|
+
blocks,
|
|
717
|
+
finalized: false,
|
|
718
|
+
abortCtrl: new AbortController(),
|
|
719
|
+
};
|
|
720
|
+
pendingAskBatches.push(askBatch);
|
|
721
|
+
|
|
722
|
+
// 发 N 个 buildBlockStarted(interrupt, awaiting) + 1 个 buildRunPaused
|
|
723
|
+
const envelopes = [
|
|
724
|
+
...blocks.map((b) => buildBlockStarted(run_id, b)),
|
|
725
|
+
buildRunPaused(run_id, batchBlockIds),
|
|
726
|
+
];
|
|
727
|
+
try {
|
|
728
|
+
await onChunk({ event: "__envelopes", envelopes });
|
|
729
|
+
} catch { /* ignore */ }
|
|
730
|
+
|
|
731
|
+
// 非阻塞等待:发完 run.paused 后立即继续,等待在 .then() 里
|
|
732
|
+
if (relayUrl && bridgeToken && thread_id) {
|
|
733
|
+
// Promise.all:集齐全部 block 的 resume 后聚合
|
|
734
|
+
const waitPromises = batchBlockIds.map((blockId, idx) =>
|
|
735
|
+
mkWaitForRunResumed({
|
|
736
|
+
relayUrl,
|
|
737
|
+
bridgeToken,
|
|
738
|
+
threadId: thread_id,
|
|
739
|
+
runId: run_id,
|
|
740
|
+
interruptId: blockId,
|
|
741
|
+
signal: askBatch.abortCtrl.signal,
|
|
742
|
+
log,
|
|
743
|
+
}).then(({ value }) => ({ blockId, idx, value, ok: true }))
|
|
744
|
+
.catch((err) => ({ blockId, idx, value: null, ok: false, err })),
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
Promise.all(waitPromises).then(async (results) => {
|
|
748
|
+
// child-exit reconcile 已收尾 → 本批次作废,避免双终态
|
|
749
|
+
if (askBatch.finalized) return;
|
|
750
|
+
askBatch.finalized = true;
|
|
751
|
+
// 检查是否有取消(reject/cancel_run 或超时)
|
|
752
|
+
const hasCanceled = results.some((r) => {
|
|
753
|
+
if (!r.ok) return true; // 超时或监听失败
|
|
754
|
+
const v = r.value ?? {};
|
|
755
|
+
return v.decision === "reject" || v.decision === "cancel_run";
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
if (hasCanceled) {
|
|
759
|
+
// 取消语义:全部 block 发 canceled,不写 stdin(claude 进程已退出)
|
|
760
|
+
const canceledEnvelopes = blocks.map((b) =>
|
|
761
|
+
buildBlockCompleted(run_id, {
|
|
762
|
+
id: b.id,
|
|
763
|
+
type: "interrupt",
|
|
764
|
+
subtype: b.subtype,
|
|
765
|
+
prompt: b.prompt,
|
|
766
|
+
status: "canceled",
|
|
767
|
+
}),
|
|
768
|
+
);
|
|
769
|
+
try { await onChunk({ event: "__envelopes", envelopes: canceledEnvelopes }); } catch {}
|
|
770
|
+
// child 已死(headless AskUserQuestion 场景),原 run 终态由
|
|
771
|
+
// reconcileAskOnExit 或本路径的 ensureAskTerminal 保证。
|
|
772
|
+
if (childExited) await ensureAskTerminal();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// 全部答案收集完毕 ── --resume 注入路径
|
|
777
|
+
const resumeValues = results.map((r) => r.value ?? {});
|
|
778
|
+
const retainedSid = askBatch.retainedSessionId ?? null;
|
|
779
|
+
|
|
780
|
+
if (!retainedSid) {
|
|
781
|
+
// 降级路径:session_id 不可用,无法续接
|
|
782
|
+
log?.warn?.("claude.ask_user_question.no_session_id_for_resume",
|
|
783
|
+
"decision arrived but retainedSessionId is null; falling back to cancel",
|
|
784
|
+
{ run_id });
|
|
785
|
+
const canceledEnvelopes = blocks.map((b) =>
|
|
786
|
+
buildBlockCompleted(run_id, {
|
|
787
|
+
id: b.id,
|
|
788
|
+
type: "interrupt",
|
|
789
|
+
subtype: b.subtype,
|
|
790
|
+
prompt: b.prompt,
|
|
791
|
+
status: "canceled",
|
|
792
|
+
}),
|
|
793
|
+
);
|
|
794
|
+
try { await onChunk({ event: "__envelopes", envelopes: canceledEnvelopes }); } catch {}
|
|
795
|
+
// 补一条友好提示(新 run 信封)——本地 translate 保证 run_id 正确
|
|
796
|
+
const fallbackRunId = newRunId();
|
|
797
|
+
try {
|
|
798
|
+
await onChunk({
|
|
799
|
+
event: "__envelopes",
|
|
800
|
+
envelopes: [buildRunStarted(fallbackRunId)],
|
|
801
|
+
});
|
|
802
|
+
} catch {}
|
|
803
|
+
{
|
|
804
|
+
let fbState = newBlockState();
|
|
805
|
+
const fbDelta = translateOpenClawChunkToEnvelope(
|
|
806
|
+
{ event: "response.output_text.delta", delta: "暂无可续接的会话,请重新提问。" },
|
|
807
|
+
fallbackRunId, fbState,
|
|
808
|
+
);
|
|
809
|
+
fbState = fbDelta.blockState;
|
|
810
|
+
if (fbDelta.envelopes.length > 0) {
|
|
811
|
+
try { await onChunk({ event: "__envelopes", envelopes: fbDelta.envelopes }); } catch {}
|
|
812
|
+
}
|
|
813
|
+
const fbCompleted = translateOpenClawChunkToEnvelope(
|
|
814
|
+
{ event: "response.completed" },
|
|
815
|
+
fallbackRunId, fbState,
|
|
816
|
+
);
|
|
817
|
+
if (fbCompleted.envelopes.length > 0) {
|
|
818
|
+
try { await onChunk({ event: "__envelopes", envelopes: fbCompleted.envelopes }); } catch {}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// § spec 2.4 时序:
|
|
825
|
+
// 1. 发原 batch 所有 block 的 blockCompleted(status=completed)
|
|
826
|
+
// 2. 生成新 run_id_resume
|
|
827
|
+
// 3. 构造 answerText
|
|
828
|
+
// 4. 起新 child(--resume <session_id>,answerText 经 stdin 写 user message)
|
|
829
|
+
// 5. 流式透传,新 session_id → threadSessionMap
|
|
830
|
+
// 6. run.completed(R2)
|
|
831
|
+
|
|
832
|
+
// Step 1: block completed(原 run R1 信封下)
|
|
833
|
+
const completedEnvelopes = results.map((r) =>
|
|
834
|
+
buildBlockCompleted(run_id, {
|
|
835
|
+
id: r.blockId,
|
|
836
|
+
type: "interrupt",
|
|
837
|
+
subtype: blocks[r.idx].subtype,
|
|
838
|
+
prompt: blocks[r.idx].prompt,
|
|
839
|
+
status: "completed",
|
|
840
|
+
value: r.value,
|
|
841
|
+
}),
|
|
842
|
+
);
|
|
843
|
+
try { await onChunk({ event: "__envelopes", envelopes: completedEnvelopes }); } catch {}
|
|
844
|
+
|
|
845
|
+
// Step 2 & 3
|
|
846
|
+
const runIdResume = newRunId();
|
|
847
|
+
const answerText = buildAnswerInjectionText(questions, resumeValues, optionMaps);
|
|
848
|
+
|
|
849
|
+
log?.info?.("claude.ask_user_question.resume_inject",
|
|
850
|
+
"starting --resume child for AskUserQuestion answer injection",
|
|
851
|
+
{ run_id, run_id_resume: runIdResume, session_id: retainedSid });
|
|
852
|
+
|
|
853
|
+
// Step 4: 起新 child(独立 run 信封)。dispatcher 的 translate 只认
|
|
854
|
+
// 原 R1 run_id,所以 R2 的全部信封都在本地铸(translate + 本地
|
|
855
|
+
// blockState),经 __envelopes 直通下发,run_id 天然是 runIdResume。
|
|
856
|
+
|
|
857
|
+
// emit run.started(R2)
|
|
858
|
+
try {
|
|
859
|
+
await onChunk({
|
|
860
|
+
event: "__envelopes",
|
|
861
|
+
envelopes: [buildRunStarted(runIdResume)],
|
|
862
|
+
});
|
|
863
|
+
} catch {}
|
|
864
|
+
|
|
865
|
+
// Step 5: spawn --resume child
|
|
866
|
+
let resumeHandle;
|
|
867
|
+
try {
|
|
868
|
+
resumeHandle = mkSpawn({
|
|
869
|
+
cwd: spawnCwd,
|
|
870
|
+
log,
|
|
871
|
+
resumeSessionId: retainedSid,
|
|
872
|
+
cwdSource: "ask_resume",
|
|
873
|
+
});
|
|
874
|
+
} catch (spawnErr) {
|
|
875
|
+
log?.warn?.("claude.ask_user_question.resume_spawn_failed",
|
|
876
|
+
"failed to spawn --resume child",
|
|
877
|
+
{ run_id_resume: runIdResume, err: spawnErr?.message });
|
|
878
|
+
{
|
|
879
|
+
let spawnFbState = newBlockState();
|
|
880
|
+
const spawnFbDelta = translateOpenClawChunkToEnvelope(
|
|
881
|
+
{ event: "response.output_text.delta", delta: "续接会话失败,请重新提问。" },
|
|
882
|
+
runIdResume, spawnFbState,
|
|
883
|
+
);
|
|
884
|
+
spawnFbState = spawnFbDelta.blockState;
|
|
885
|
+
if (spawnFbDelta.envelopes.length > 0) {
|
|
886
|
+
try { await onChunk({ event: "__envelopes", envelopes: spawnFbDelta.envelopes }); } catch {}
|
|
887
|
+
}
|
|
888
|
+
const spawnFbCompleted = translateOpenClawChunkToEnvelope(
|
|
889
|
+
{ event: "response.completed" },
|
|
890
|
+
runIdResume, spawnFbState,
|
|
891
|
+
);
|
|
892
|
+
if (spawnFbCompleted.envelopes.length > 0) {
|
|
893
|
+
try { await onChunk({ event: "__envelopes", envelopes: spawnFbCompleted.envelopes }); } catch {}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// 写 user message(答案注入文本)
|
|
900
|
+
try {
|
|
901
|
+
await writeUserMessage(resumeHandle.child, answerText, []);
|
|
902
|
+
} catch (stdinErr) {
|
|
903
|
+
log?.warn?.("claude.ask_user_question.resume_stdin_failed",
|
|
904
|
+
"failed to write answer to --resume child stdin",
|
|
905
|
+
{ err: stdinErr?.message });
|
|
906
|
+
try { resumeHandle.kill("stdin-failed"); } catch {}
|
|
907
|
+
{
|
|
908
|
+
let stdinFbState = newBlockState();
|
|
909
|
+
const stdinFbDelta = translateOpenClawChunkToEnvelope(
|
|
910
|
+
{ event: "response.output_text.delta", delta: "续接会话失败,请重新提问。" },
|
|
911
|
+
runIdResume, stdinFbState,
|
|
912
|
+
);
|
|
913
|
+
stdinFbState = stdinFbDelta.blockState;
|
|
914
|
+
if (stdinFbDelta.envelopes.length > 0) {
|
|
915
|
+
try { await onChunk({ event: "__envelopes", envelopes: stdinFbDelta.envelopes }); } catch {}
|
|
916
|
+
}
|
|
917
|
+
const stdinFbCompleted = translateOpenClawChunkToEnvelope(
|
|
918
|
+
{ event: "response.completed" },
|
|
919
|
+
runIdResume, stdinFbState,
|
|
920
|
+
);
|
|
921
|
+
if (stdinFbCompleted.envelopes.length > 0) {
|
|
922
|
+
try { await onChunk({ event: "__envelopes", envelopes: stdinFbCompleted.envelopes }); } catch {}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
try { resumeHandle.child.stdin.end(); } catch {}
|
|
928
|
+
|
|
929
|
+
// 流式透传 R2 输出
|
|
930
|
+
// 本地持有 blockState,对每个 chunk 调用 translateOpenClawChunkToEnvelope,
|
|
931
|
+
// 产出的 envelopes 以 __envelopes 直通发出,run_id 天然等于 runIdResume。
|
|
932
|
+
let resumePendingSessionId = null;
|
|
933
|
+
let resumeSawNonSynthetic = false;
|
|
934
|
+
let resumeBlockState = newBlockState();
|
|
935
|
+
let resumeSawResponseCompleted = false;
|
|
936
|
+
const resumeParser = createLineStreamParser({
|
|
937
|
+
onObject: async (obj) => {
|
|
938
|
+
const events = normalizeClaudeLine(obj);
|
|
939
|
+
for (const ev of events) {
|
|
940
|
+
if (ev.kind === "session_ready") {
|
|
941
|
+
const sid = ev.payload?.session_id;
|
|
942
|
+
if (typeof sid === "string" && sid) {
|
|
943
|
+
resumePendingSessionId = sid;
|
|
944
|
+
}
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (ev.kind === "assistant_text" && !ev.payload?._marker && (ev.payload?.text || "").length > 0) {
|
|
948
|
+
if (!ev.payload?.synthetic) resumeSawNonSynthetic = true;
|
|
949
|
+
} else if (ev.kind === "thinking" || ev.kind === "tool_use" || ev.kind === "tool_use_end") {
|
|
950
|
+
resumeSawNonSynthetic = true;
|
|
951
|
+
}
|
|
952
|
+
const chunk = translateClaudeEventToChunk(ev);
|
|
953
|
+
if (!chunk) continue;
|
|
954
|
+
const chunks = Array.isArray(chunk) ? chunk : [chunk];
|
|
955
|
+
for (const c of chunks) {
|
|
956
|
+
const cEvent = c.event || c.type;
|
|
957
|
+
if (cEvent === "response.completed" || cEvent === "response.failed" || cEvent === "end") {
|
|
958
|
+
resumeSawResponseCompleted = true;
|
|
959
|
+
}
|
|
960
|
+
const result = translateOpenClawChunkToEnvelope(c, runIdResume, resumeBlockState);
|
|
961
|
+
resumeBlockState = result.blockState;
|
|
962
|
+
if (result.envelopes.length > 0) {
|
|
963
|
+
try { await onChunk({ event: "__envelopes", envelopes: result.envelopes }); } catch {}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
onInvalid: () => {},
|
|
969
|
+
log,
|
|
970
|
+
});
|
|
971
|
+
resumeHandle.child.stdout.on("data", (buf) => resumeParser.push(buf));
|
|
972
|
+
|
|
973
|
+
const resumeCode = await new Promise((resolve) => {
|
|
974
|
+
resumeHandle.child.once("exit", (c) => {
|
|
975
|
+
resumeParser.flush();
|
|
976
|
+
resolve(c == null ? -1 : c);
|
|
977
|
+
});
|
|
978
|
+
resumeHandle.child.once("error", () => resolve(-1));
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// 新 session_id 写入 threadSessionMap
|
|
982
|
+
if (resumeCode === 0 && resumeSawNonSynthetic && resumePendingSessionId && thread_id) {
|
|
983
|
+
threadSessionMap.set(thread_id, resumePendingSessionId);
|
|
984
|
+
log?.info?.("claude.ask_user_question.resume_session_updated",
|
|
985
|
+
"threadSessionMap updated after --resume child",
|
|
986
|
+
{ thread_id, new_session_id: resumePendingSessionId });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// 若 child 未自发 response.completed,本地合成一次以关闭未闭合块并产出 run.completed(R2)。
|
|
990
|
+
// 若已发过(resumeSawResponseCompleted=true),translate 已产出 run.completed,不重复发。
|
|
991
|
+
if (!resumeSawResponseCompleted) {
|
|
992
|
+
const syntheticResult = translateOpenClawChunkToEnvelope(
|
|
993
|
+
{ event: "response.completed" },
|
|
994
|
+
runIdResume, resumeBlockState,
|
|
995
|
+
);
|
|
996
|
+
if (syntheticResult.envelopes.length > 0) {
|
|
997
|
+
try { await onChunk({ event: "__envelopes", envelopes: syntheticResult.envelopes }); } catch {}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}).catch((err) => {
|
|
1001
|
+
log?.warn?.("claude.ask_user_question.batch_wait_error",
|
|
1002
|
+
"batch wait Promise.all error", { err: err?.message || String(err) });
|
|
1003
|
+
});
|
|
1004
|
+
} else {
|
|
1005
|
+
log?.warn?.("claude.ask_user_question.no_listener",
|
|
1006
|
+
"missing relay/token/thread; cannot await AskUserQuestion resume", { run_id });
|
|
1007
|
+
}
|
|
1008
|
+
continue; // ev 已被消费,不走 todoTranslator
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const intercepted = todoTranslator.intercept(ev);
|
|
1012
|
+
if (intercepted.length === 0) continue;
|
|
1013
|
+
if (intercepted !== null && intercepted[0] !== ev) {
|
|
1014
|
+
try { await onChunk({ event: "__envelopes", envelopes: intercepted }); } catch { /* ignore */ }
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const chunk = translateClaudeEventToChunk(ev);
|
|
1018
|
+
if (!chunk) continue;
|
|
1019
|
+
const chunks = Array.isArray(chunk) ? chunk : [chunk];
|
|
1020
|
+
for (const c of chunks) {
|
|
1021
|
+
if (c?.event === "response.completed") sawResponseCompleted = true;
|
|
1022
|
+
try { await onChunk(c); } catch { /* ignore */ }
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
onInvalid: () => {},
|
|
1027
|
+
log,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
handle.child.stdout.on("data", (buf) => parser.push(buf));
|
|
1031
|
+
|
|
1032
|
+
const code = await new Promise((resolve) => {
|
|
1033
|
+
handle.child.once("exit", (c) => {
|
|
1034
|
+
parser.flush();
|
|
1035
|
+
resolve(c == null ? -1 : c);
|
|
1036
|
+
});
|
|
1037
|
+
handle.child.once("error", () => resolve(-1));
|
|
1038
|
+
});
|
|
1039
|
+
childExited = true;
|
|
1040
|
+
unregister();
|
|
1041
|
+
|
|
1042
|
+
const stderrTail = handle.getStderrTail ? handle.getStderrTail() : "";
|
|
1043
|
+
return {
|
|
1044
|
+
code, stderrTail, sawAssistantOutput, sawNonSyntheticOutput,
|
|
1045
|
+
pendingSessionId, abortedByRpc,
|
|
1046
|
+
pendingAskBatches, ensureAskTerminal,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// 第一次尝试:用现有 session_id(如果有)
|
|
1051
|
+
let resumeSessionId = thread_id ? threadSessionMap.get(thread_id) : null;
|
|
1052
|
+
let result = await attemptOnce({ resumeSessionId, isRetry: false });
|
|
1053
|
+
|
|
1054
|
+
// 兜底 1:spawn 失败直接报。
|
|
1055
|
+
if (result.spawnError) {
|
|
1056
|
+
await onChunk({ event: "response.failed", error: { message: `spawn failed: ${result.spawnError}` } });
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (result.stdinError) {
|
|
1060
|
+
await onChunk({ event: "response.failed", error: { message: `stdin write failed: ${result.stdinError}` } });
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Bug-B 修复:--resume <id> 失败 + stderr 含 "No conversation found"
|
|
1065
|
+
// → 清掉 map 里那条幻影 session_id,retry 一次无 resume 路径。
|
|
1066
|
+
if (
|
|
1067
|
+
result.code === 1 &&
|
|
1068
|
+
resumeSessionId &&
|
|
1069
|
+
typeof result.stderrTail === "string" &&
|
|
1070
|
+
/no conversation found/i.test(result.stderrTail)
|
|
1071
|
+
) {
|
|
1072
|
+
log.warn?.("claude.session.invalid",
|
|
1073
|
+
"claude --resume rejected (session not found); clearing cache and retrying without resume",
|
|
1074
|
+
{ thread_id, stale_session_id: resumeSessionId });
|
|
1075
|
+
if (thread_id) threadSessionMap.delete(thread_id);
|
|
1076
|
+
result = await attemptOnce({ resumeSessionId: null, isRetry: true });
|
|
1077
|
+
if (result.spawnError) {
|
|
1078
|
+
await onChunk({ event: "response.failed", error: { message: `spawn failed (retry): ${result.spawnError}` } });
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (result.stdinError) {
|
|
1082
|
+
await onChunk({ event: "response.failed", error: { message: `stdin write failed (retry): ${result.stdinError}` } });
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Session-id 写入策略:只有当 claude 本轮真的跑了 LLM turn(产出过非
|
|
1088
|
+
// synthetic 的 assistant 内容)时才信任这个 session_id —— 说明会话 jsonl
|
|
1089
|
+
// 已写盘。synthetic slash 响应(/help /status /cost 等)和"无任何 assistant
|
|
1090
|
+
// 内容"都不能信, 否则下一轮 --resume <sid> 报 "No conversation found"。
|
|
1091
|
+
//
|
|
1092
|
+
// 覆盖的场景:
|
|
1093
|
+
// - 普通对话:sawNonSyntheticOutput=true → 落盘 ✓
|
|
1094
|
+
// - /compact passthrough(claude 真跑 LLM compaction):sawNonSyntheticOutput=true → 落盘 ✓
|
|
1095
|
+
// - /help /status 等被 claude 以 synthetic 应答:sawNonSyntheticOutput=false → 丢
|
|
1096
|
+
// - claude 异常无 assistant:sawAssistantOutput=false → 丢
|
|
1097
|
+
if (
|
|
1098
|
+
thread_id &&
|
|
1099
|
+
result.code === 0 &&
|
|
1100
|
+
result.sawNonSyntheticOutput &&
|
|
1101
|
+
result.pendingSessionId
|
|
1102
|
+
) {
|
|
1103
|
+
threadSessionMap.set(thread_id, result.pendingSessionId);
|
|
1104
|
+
} else if (
|
|
1105
|
+
thread_id &&
|
|
1106
|
+
result.pendingSessionId &&
|
|
1107
|
+
!result.sawNonSyntheticOutput
|
|
1108
|
+
) {
|
|
1109
|
+
log.info?.("claude.session.discarded",
|
|
1110
|
+
"claude emitted session_id but no real LLM turn (synthetic slash response or no output); discarding to avoid stale resume",
|
|
1111
|
+
{ thread_id, discarded_session_id: result.pendingSessionId,
|
|
1112
|
+
is_passthrough_slash: isPassthroughSlash,
|
|
1113
|
+
saw_synthetic_only: result.sawAssistantOutput && !result.sawNonSyntheticOutput,
|
|
1114
|
+
exit_code: result.code });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// child 已退出但 AskUserQuestion 还挂着 → reconcile。
|
|
1118
|
+
//
|
|
1119
|
+
// fix-claude-ask-resume-injection 新行为:
|
|
1120
|
+
// - session_id 可用(exit_code=0 && sawNonSyntheticOutput && pendingSessionId):
|
|
1121
|
+
// 不发 blockCompleted(canceled);让 interrupt block 保持 awaiting;
|
|
1122
|
+
// 锁定 retainedSessionId 供后续异步等待链起 --resume child。
|
|
1123
|
+
// - session_id 不可用:退化为原止血路径(cancel + log warn)。
|
|
1124
|
+
//
|
|
1125
|
+
// 无论哪条路径,补 run.completed 让 dispatcher 正常收尾(dispatch 层因
|
|
1126
|
+
// run.paused 跳过合成收尾,必须这里收)。
|
|
1127
|
+
const reconcileAskOnExit = async ({ emitTerminal }) => {
|
|
1128
|
+
const all = result.pendingAskBatches || [];
|
|
1129
|
+
if (all.length === 0) return;
|
|
1130
|
+
const stale = all.filter((b) => !b.finalized);
|
|
1131
|
+
|
|
1132
|
+
// 判断是否有可用 session_id
|
|
1133
|
+
const hasRetainableSession = (
|
|
1134
|
+
result.code === 0 &&
|
|
1135
|
+
result.sawNonSyntheticOutput &&
|
|
1136
|
+
result.pendingSessionId != null
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
for (const b of stale) {
|
|
1140
|
+
if (hasRetainableSession) {
|
|
1141
|
+
// 留存路径:不 finalize(让异步 waitPromises.then() 继续处理),
|
|
1142
|
+
// 锁定 session_id,不发 canceled。
|
|
1143
|
+
// 注意:不设 b.finalized = true,让 waitPromises.then() 可以继续执行。
|
|
1144
|
+
b.retainedSessionId = result.pendingSessionId;
|
|
1145
|
+
log?.info?.("claude.ask_user_question.session_retained",
|
|
1146
|
+
"session_id retained for --resume injection; interrupt block stays awaiting",
|
|
1147
|
+
{ run_id, session_id: result.pendingSessionId });
|
|
1148
|
+
} else {
|
|
1149
|
+
// 止血路径:cancel + warn
|
|
1150
|
+
b.finalized = true;
|
|
1151
|
+
try { b.abortCtrl.abort(); } catch { /* ignore */ }
|
|
1152
|
+
log?.warn?.("claude.ask_user_question.no_session_id_for_resume",
|
|
1153
|
+
"child exited without usable session_id; falling back to cancel interrupt blocks",
|
|
1154
|
+
{ run_id, exit_code: result.code,
|
|
1155
|
+
saw_non_synthetic: result.sawNonSyntheticOutput,
|
|
1156
|
+
pending_session_id: result.pendingSessionId });
|
|
1157
|
+
const canceledEnvelopes = b.blocks.map((blk) =>
|
|
1158
|
+
buildBlockCompleted(run_id, {
|
|
1159
|
+
id: blk.id,
|
|
1160
|
+
type: "interrupt",
|
|
1161
|
+
subtype: blk.subtype,
|
|
1162
|
+
prompt: blk.prompt,
|
|
1163
|
+
status: "canceled",
|
|
1164
|
+
}),
|
|
1165
|
+
);
|
|
1166
|
+
try { await onChunk({ event: "__envelopes", envelopes: canceledEnvelopes }); } catch { /* ignore */ }
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const staleCanceled = stale.filter((b) => b.finalized);
|
|
1170
|
+
if (staleCanceled.length > 0) {
|
|
1171
|
+
log.info?.("claude.ask_user_question.reconciled_on_exit",
|
|
1172
|
+
"claude exited with AskUserQuestion pending; canceled interrupt blocks (no session_id)",
|
|
1173
|
+
{ run_id, batch_count: staleCanceled.length });
|
|
1174
|
+
}
|
|
1175
|
+
if (emitTerminal && typeof result.ensureAskTerminal === "function") {
|
|
1176
|
+
await result.ensureAskTerminal();
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// 用户主动停止(sessions.abort RPC kill)→ 优雅收尾,不报失败
|
|
1181
|
+
if (result.abortedByRpc) {
|
|
1182
|
+
log.info?.("claude.run.aborted", "run killed by sessions.abort rpc",
|
|
1183
|
+
{ thread_id, run_id, code: result.code });
|
|
1184
|
+
await emitAssistantText(onChunk, "已停止本次回复。");
|
|
1185
|
+
await reconcileAskOnExit({ emitTerminal: true });
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (result.code !== 0) {
|
|
1190
|
+
log.warn?.("child.nonzero_exit", "claude exited non-zero",
|
|
1191
|
+
{ code: result.code, stderrTail: result.stderrTail });
|
|
1192
|
+
// 卡片先置 canceled;终态交给下面的 response.failed(run.failed)
|
|
1193
|
+
await reconcileAskOnExit({ emitTerminal: false });
|
|
1194
|
+
await onChunk({
|
|
1195
|
+
event: "response.failed",
|
|
1196
|
+
error: { message: `claude exited code=${result.code} stderr=${(result.stderrTail || "").slice(-200)}` },
|
|
1197
|
+
});
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// 兜底 2:slash 命令短路退出,0 assistant chunk —— 给一句"命令已发往
|
|
1202
|
+
// Claude Code,无可显示输出"提示,免得 App 看起来卡住。
|
|
1203
|
+
if (isPassthroughSlash && !result.sawAssistantOutput) {
|
|
1204
|
+
await emitAssistantText(
|
|
1205
|
+
onChunk,
|
|
1206
|
+
`「${slash.command}」已发给 Claude Code 处理,但 CLI 在远程模式下无可显示输出。\n` +
|
|
1207
|
+
`如果是 /compact,可以在下一条消息观察上下文是否变短。`,
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
// 正常退出且 ask 还挂着 → 卡片 canceled + 补终态(dispatch 层因
|
|
1211
|
+
// run.paused 不会自动追加 run.completed,必须这里收)。
|
|
1212
|
+
await reconcileAskOnExit({ emitTerminal: true });
|
|
1213
|
+
// 若 stdout 没有 message_stop(response.completed),unifiedDispatchHandler 会自动追加 run.completed
|
|
1214
|
+
} finally {
|
|
1215
|
+
try { _acc?.flush(); } catch { /* swallow */ }
|
|
1216
|
+
// approval 模式清理:stop permServer + 删 MCP config 临时文件
|
|
1217
|
+
if (_approvalPermServer) {
|
|
1218
|
+
try { await _approvalPermServer.stop(); } catch { /* swallow */ }
|
|
1219
|
+
_approvalPermServer = null;
|
|
1220
|
+
}
|
|
1221
|
+
if (_approvalMcpConfigPath) {
|
|
1222
|
+
try { await doDeleteMcpConfig(_approvalMcpConfigPath); } catch { /* swallow */ }
|
|
1223
|
+
_approvalMcpConfigPath = null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
// 注册 unifiedDispatchHandler
|
|
1229
|
+
const unifiedHandler = createUnifiedDispatchHandler({
|
|
1230
|
+
relayUrl,
|
|
1231
|
+
bridgeToken,
|
|
1232
|
+
log,
|
|
1233
|
+
producerFactory,
|
|
1234
|
+
openclawInvoke: injectedOpenclawInvoke ?? defaultOpenclawInvoke,
|
|
1235
|
+
gatewayId: claudeGwId, // 正文 agentlink-cos:// 图片引用解析(sign-get)
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// workspaces.create 实现(可注入用于测试)
|
|
1239
|
+
const createWorkspaceImpl = _createWorkspace ?? _defaultCreateWorkspace;
|
|
1240
|
+
// 自定义命令收集器(可注入用于测试)
|
|
1241
|
+
const collectCustomCommandsImpl = _collectCustomCommands ?? _defaultCollectCustomCommands;
|
|
1242
|
+
|
|
1243
|
+
return async function handleRequest(request, ctx) {
|
|
1244
|
+
// 步骤 0(最优先):relay-rpc 分支。
|
|
1245
|
+
if (request?.request?.__relayKind === "rpc") {
|
|
1246
|
+
const method = request.request.method;
|
|
1247
|
+
const params = request.request.params ?? {};
|
|
1248
|
+
let result;
|
|
1249
|
+
try {
|
|
1250
|
+
if (method === "__runtime.customCommands") {
|
|
1251
|
+
// 获取当前工作区的自定义命令清单。
|
|
1252
|
+
// workspacePath 由 App 传入;probe claude system/init 获取活体 slash_commands。
|
|
1253
|
+
const workspacePath = typeof params.workspacePath === "string" && params.workspacePath.trim()
|
|
1254
|
+
? params.workspacePath.trim()
|
|
1255
|
+
: cwd;
|
|
1256
|
+
let init = null;
|
|
1257
|
+
try {
|
|
1258
|
+
init = await probeClaudeSystemInit({ cwd: workspacePath, timeoutMs: 8000 });
|
|
1259
|
+
} catch {
|
|
1260
|
+
log.warn?.("claude.rpc.customCommands.probe_failed", "probeClaudeSystemInit failed", { workspacePath });
|
|
1261
|
+
}
|
|
1262
|
+
const commands = await collectCustomCommandsImpl({ init, cwd: workspacePath });
|
|
1263
|
+
result = { commands };
|
|
1264
|
+
} else if (method === "workspaces.scan") {
|
|
1265
|
+
// workspaces.scan 走通用 scanWorkspacesByRuntime(claude runtime 读
|
|
1266
|
+
// ~/.claude.json projects trust list)。
|
|
1267
|
+
result = await scanWorkspacesByRuntime({
|
|
1268
|
+
runtimeKind: "claude",
|
|
1269
|
+
gatewayId: claudeGwId,
|
|
1270
|
+
params,
|
|
1271
|
+
});
|
|
1272
|
+
} else if (method === "workspaces.create") {
|
|
1273
|
+
result = await createWorkspaceImpl({
|
|
1274
|
+
runtime: "claude",
|
|
1275
|
+
name: params.name,
|
|
1276
|
+
});
|
|
1277
|
+
} else if (method === "sessions.abort" || method === "chat.abort") {
|
|
1278
|
+
// P0 停止按钮:杀掉匹配的活跃 claude 子进程
|
|
1279
|
+
const { aborted, matched } = activeRuns.abort({
|
|
1280
|
+
key: params.key ?? params.sessionKey ?? "",
|
|
1281
|
+
runId: params.runId ?? "",
|
|
1282
|
+
});
|
|
1283
|
+
log.info?.("claude.rpc.abort", "sessions.abort handled", {
|
|
1284
|
+
requestId: ctx.requestId, key: params.key || "", runId: params.runId || "",
|
|
1285
|
+
aborted, matched,
|
|
1286
|
+
});
|
|
1287
|
+
result = { ok: true, aborted, matched };
|
|
1288
|
+
} else {
|
|
1289
|
+
result = { ok: false, error: `unknown rpc method: ${method}` };
|
|
1290
|
+
}
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
result = { ok: false, error: err?.message ?? String(err) };
|
|
1293
|
+
}
|
|
1294
|
+
await ctx.publishEvent({
|
|
1295
|
+
event: "response",
|
|
1296
|
+
statusCode: 200,
|
|
1297
|
+
contentType: "application/json; charset=utf-8",
|
|
1298
|
+
body: JSON.stringify(result),
|
|
1299
|
+
});
|
|
1300
|
+
await ctx.publishEvent({ event: "end" });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// 步骤 1:优先走 unified dispatch
|
|
1305
|
+
const handled = await unifiedHandler(request, ctx);
|
|
1306
|
+
if (handled) return;
|
|
1307
|
+
|
|
1308
|
+
// 步骤 2:旧路径 — 与 index.mjs 原有逻辑一致
|
|
1309
|
+
const userText = extractUserText(request);
|
|
1310
|
+
if (!userText) {
|
|
1311
|
+
log.warn?.("turn.skip", "no user text in request", { requestId: ctx.requestId });
|
|
1312
|
+
await ctx.publishEvent({ event: "error", error: "empty user input" });
|
|
1313
|
+
await ctx.publishEvent({ event: "end" });
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
await ctx.publishEvent({ event: "start" });
|
|
1318
|
+
|
|
1319
|
+
let handle;
|
|
1320
|
+
try {
|
|
1321
|
+
handle = mkSpawn({ cwd, log });
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
const msg = err?.message || String(err);
|
|
1324
|
+
log.error?.("child.spawn.failed", "spawn claude failed", { err: msg });
|
|
1325
|
+
await ctx.publishEvent({ event: "error", error: `spawn failed: ${msg}` });
|
|
1326
|
+
await ctx.publishEvent({ event: "end" });
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// 写 user 消息进 stdin
|
|
1331
|
+
try {
|
|
1332
|
+
await writeUserMessage(handle.child, userText);
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
const msg = err?.message || String(err);
|
|
1335
|
+
log.error?.("child.stdin.failed", "write user message failed", { err: msg });
|
|
1336
|
+
await ctx.publishEvent({ event: "error", error: `stdin write failed: ${msg}` });
|
|
1337
|
+
await ctx.publishEvent({ event: "end" });
|
|
1338
|
+
try { handle.kill("stdin-failed"); } catch {}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// 透传 stdout 行 — claude adapter 直接吃 native stream-json,逐行 chunk 即可。
|
|
1343
|
+
let stdoutBuf = "";
|
|
1344
|
+
handle.child.stdout.on("data", (buf) => {
|
|
1345
|
+
stdoutBuf += buf.toString("utf8");
|
|
1346
|
+
let nl;
|
|
1347
|
+
while ((nl = stdoutBuf.indexOf("\n")) >= 0) {
|
|
1348
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
1349
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
1350
|
+
if (!line) continue;
|
|
1351
|
+
void ctx.publishEvent({ event: "chunk", data: line });
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// 等子进程退出
|
|
1356
|
+
const code = await new Promise((resolve) => {
|
|
1357
|
+
handle.child.once("exit", (c) => resolve(c == null ? -1 : c));
|
|
1358
|
+
handle.child.once("error", () => resolve(-1));
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
// 处理 stdout 残余
|
|
1362
|
+
const tail = stdoutBuf.trim();
|
|
1363
|
+
stdoutBuf = "";
|
|
1364
|
+
if (tail) {
|
|
1365
|
+
await ctx.publishEvent({ event: "chunk", data: tail });
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (code !== 0) {
|
|
1369
|
+
const stderrTail = handle.getStderrTail ? handle.getStderrTail() : "";
|
|
1370
|
+
log.warn?.("child.nonzero_exit", "claude exited non-zero", { code, stderrTail });
|
|
1371
|
+
await ctx.publishEvent({
|
|
1372
|
+
event: "error",
|
|
1373
|
+
error: `claude exited code=${code} stderr=${(stderrTail || "").slice(-400)}`,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
await ctx.publishEvent({ event: "end" });
|
|
1377
|
+
|
|
1378
|
+
usage.addCost({ turns: 1 });
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
1383
|
+
|
|
1384
|
+
function extractUserText(request) {
|
|
1385
|
+
const input = request?.request?.input;
|
|
1386
|
+
if (typeof input === "string") return input.trim();
|
|
1387
|
+
if (Array.isArray(input)) return collectTextFromItems(input).trim();
|
|
1388
|
+
if (input && typeof input === "object") {
|
|
1389
|
+
if (typeof input.text === "string") return input.text.trim();
|
|
1390
|
+
if (input.content !== undefined) return collectTextFromAny(input.content).trim();
|
|
1391
|
+
}
|
|
1392
|
+
if (typeof request?.clientPreviewText === "string") {
|
|
1393
|
+
return request.clientPreviewText.trim();
|
|
1394
|
+
}
|
|
1395
|
+
return "";
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function collectTextFromAny(value) {
|
|
1399
|
+
if (typeof value === "string") return value;
|
|
1400
|
+
if (Array.isArray(value)) return collectTextFromItems(value);
|
|
1401
|
+
if (value && typeof value === "object" && typeof value.text === "string") return value.text;
|
|
1402
|
+
return "";
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function collectTextFromItems(items) {
|
|
1406
|
+
const parts = [];
|
|
1407
|
+
for (const item of items) {
|
|
1408
|
+
if (typeof item === "string") { parts.push(item); continue; }
|
|
1409
|
+
if (!item || typeof item !== "object") continue;
|
|
1410
|
+
if (item.role && String(item.role).toLowerCase() !== "user") continue;
|
|
1411
|
+
if (typeof item.text === "string") { parts.push(item.text); continue; }
|
|
1412
|
+
if (item.content !== undefined) {
|
|
1413
|
+
const t = collectTextFromAny(item.content);
|
|
1414
|
+
if (t) parts.push(t);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return parts.join("\n\n");
|
|
1418
|
+
}
|