oh-my-agent-web 0.7.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +394 -248
- package/README.md +387 -247
- package/dist/server/approval/rules.js +4 -16
- package/dist/server/approval/scope.js +3 -3
- package/dist/server/cli-config.js +3 -3
- package/dist/server/cli-info.js +9 -9
- package/dist/server/cli.js +108 -23
- package/dist/server/engine/builtin.js +64 -1
- package/dist/server/engine/catalog-probe.js +34 -0
- package/dist/server/engine/claude-map.js +296 -0
- package/dist/server/engine/claude-sessions.js +581 -0
- package/dist/server/engine/claude-transport.js +75 -0
- package/dist/server/engine/claude.js +414 -0
- package/dist/server/engine/cli-version.js +119 -0
- package/dist/server/engine/codex-map.js +19 -23
- package/dist/server/engine/codex-sessions.js +5 -5
- package/dist/server/engine/codex-subagents.js +349 -0
- package/dist/server/engine/codex.js +215 -2
- package/dist/server/engine/dsh-history.js +1 -1
- package/dist/server/engine/dsh-log.js +47 -36
- package/dist/server/engine/dsh-map.js +3 -5
- package/dist/server/engine/dsh-prompt-patch.js +48 -0
- package/dist/server/engine/dsh-sessions.js +10 -11
- package/dist/server/engine/dsh-transport.js +2 -2
- package/dist/server/engine/dsh.js +45 -2
- package/dist/server/engine/engine-versions.js +41 -0
- package/dist/server/engine/fake.js +38 -7
- package/dist/server/engine/model-registry-reload.js +1 -1
- package/dist/server/engine/opencode-map.js +2 -2
- package/dist/server/engine/opencode-projects.js +1 -1
- package/dist/server/engine/opencode-sessions.js +2 -2
- package/dist/server/engine/opencode.js +39 -1
- package/dist/server/engine/pi-events.js +4 -4
- package/dist/server/engine/pi-plugins.js +8 -6
- package/dist/server/engine/pi-sdk.js +179 -5
- package/dist/server/engine/pi-subagent-tool.js +2 -2
- package/dist/server/engine/pi-subagents.js +2 -2
- package/dist/server/engine/pi-transcript.js +1 -1
- package/dist/server/engine/registry.js +12 -2
- package/dist/server/engine/suggestion-guidance.js +22 -9
- package/dist/server/engine/wiring.js +57 -3
- package/dist/server/files/content-search.js +187 -0
- package/dist/server/files/file-search.js +3 -3
- package/dist/server/files/file-service.js +33 -15
- package/dist/server/files/files-routes.js +68 -7
- package/dist/server/files/git-info.js +51 -11
- package/dist/server/files/path-resolve.js +176 -0
- package/dist/server/files/pick-dir-routes.js +93 -0
- package/dist/server/files/pick-path-routes.js +126 -0
- package/dist/server/http/auth-routes.js +71 -4
- package/dist/server/http/auth.js +48 -30
- package/dist/server/http/aux-routes.js +20 -4
- package/dist/server/http/file-headers.js +3 -3
- package/dist/server/http/health-and-static.js +1 -1
- package/dist/server/http/live-routes.js +4 -4
- package/dist/server/http/login-page.js +128 -1
- package/dist/server/http/password-store.js +31 -0
- package/dist/server/index.js +93 -11
- package/dist/server/markers/actions.js +8 -8
- package/dist/server/mcp/codex-mcp-store.js +190 -0
- package/dist/server/mcp/json-mcp-store.js +276 -0
- package/dist/server/mcp/mcp-auth.js +1 -1
- package/dist/server/mcp/mcp-command.js +0 -2
- package/dist/server/mcp/mcp-engine-shared.js +112 -0
- package/dist/server/mcp/mcp-import.js +4 -5
- package/dist/server/mcp/mcp-market.js +2 -3
- package/dist/server/mcp/mcp-routes.js +4 -3
- package/dist/server/mcp/none-mcp-store.js +27 -0
- package/dist/server/mcp/pi-mcp-config.js +1 -1
- package/dist/server/plugins/engine-plugin-stores.js +265 -0
- package/dist/server/plugins/plugin-routes.js +4 -3
- package/dist/server/protocol-parse.js +52 -4
- package/dist/server/protocol.js +0 -1
- package/dist/server/providers/catalog-writes.js +1 -1
- package/dist/server/providers/catalog.js +2 -2
- package/dist/server/providers/codex-models.js +1 -1
- package/dist/server/providers/codex-test.js +1 -1
- package/dist/server/providers/enabled-models.js +1 -11
- package/dist/server/providers/import-routes.js +2 -2
- package/dist/server/providers/probe-routes.js +26 -0
- package/dist/server/providers/provider-handlers.js +3 -3
- package/dist/server/providers/provider-pool.js +2 -2
- package/dist/server/providers/provider-store.js +5 -2
- package/dist/server/providers/subscription-routes.js +4 -2
- package/dist/server/providers/validate.js +13 -14
- package/dist/server/push/push-routes.js +32 -3
- package/dist/server/push/push.js +43 -9
- package/dist/server/session/client-view.js +2 -2
- package/dist/server/session/close-ephemeral.js +23 -0
- package/dist/server/session/conversation-create.js +31 -9
- package/dist/server/session/delete-session.js +25 -14
- package/dist/server/session/engine-switch.js +12 -5
- package/dist/server/session/fork-session.js +38 -0
- package/dist/server/session/lock.js +4 -4
- package/dist/server/session/model-controller.js +14 -2
- package/dist/server/session/move-session.js +40 -0
- package/dist/server/session/persist-ephemeral.js +48 -0
- package/dist/server/session/pi-opener.js +2 -2
- package/dist/server/session/pi-refresh.js +1 -1
- package/dist/server/session/pi-sessions-provider.js +2 -2
- package/dist/server/session/pi-sessions.js +107 -18
- package/dist/server/session/runtime.js +144 -9
- package/dist/server/session/session-stats.js +2 -2
- package/dist/server/session/settings.js +45 -49
- package/dist/server/session/subagents.js +4 -1
- package/dist/server/session/suggest-title.js +33 -0
- package/dist/server/session/title-prompt.js +126 -0
- package/dist/server/session/turn-finish.js +2 -2
- package/dist/server/session/turn-queue.js +5 -5
- package/dist/server/session/turn-watchdog.js +2 -2
- package/dist/server/skills/skill-install.js +3 -5
- package/dist/server/skills/skill-store.js +3 -3
- package/dist/server/skills/skillhub.js +3 -4
- package/dist/server/snapshot/messages.js +2 -8
- package/dist/server/snapshot/sender.js +1 -1
- package/dist/server/snapshot/state.js +3 -5
- package/dist/server/subagents/profile-store.js +11 -9
- package/dist/server/terminal/terminal-session.js +9 -9
- package/dist/server/uploads/upload-routes.js +1 -1
- package/dist/server/uploads/upload-store.js +4 -6
- package/dist/server/util/env.js +27 -21
- package/dist/server/util/ids.js +1 -1
- package/dist/server/util/json-file.js +38 -0
- package/dist/server/util/log-file.js +3 -3
- package/dist/server/util/log.js +4 -4
- package/dist/server/util/origin.js +5 -24
- package/dist/server/util/paths.js +25 -3
- package/dist/server/util/pricing-index.js +2 -6
- package/dist/server/util/pricing.js +10 -10
- package/dist/server/util/spawn-cli.js +2 -2
- package/dist/server/util/upstream-version.js +10 -12
- package/dist/server/util/version.js +2 -2
- package/dist/server/web-proxy/proxy-routes.js +167 -0
- package/dist/server/web-proxy/proxy-url.js +60 -0
- package/dist/server/web-proxy/rewrite.js +84 -0
- package/dist/server/ws/handle-message.js +21 -2
- package/package.json +13 -14
- package/scripts/prune-prebuilds.mjs +1 -1
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_abnfDiagram-WEYOIA3C-Dq-2fRb4.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_architectureDiagram-ZHVYKT53-Dh5CEs4k.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_c4Diagram-XDPPPE54-Cb19QmmE.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_cynefinDiagram-WF24UDRP-COk-IL9O.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-CBK3MWTC-Gp_0nfEJ.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-ODIL2UCW-rzPf_9IN.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-PFBEJUXE-B5EnLWwl.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-TYUNFSNR-DVpFKFwC.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_ebnfDiagram-V4P3TTPT-CDL1qiPY.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_ishikawaDiagram-HPEHV2GE-CSkoONzp.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_mindmap-definition-6OY57UG3-B9sdVB6_.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_pegDiagram-ECIUJNBX-B6P7baCF.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_railroadDiagram-Y72NRBL7-W_a8Vy2l.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_swimlanes-B3ENRKFQ-DvGif-RR.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_swimlanesDiagram-AYQRV4AK-D0gFG_EI.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_usecaseDiagram-WPOLBNNX-BjgUBJiR.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_vennDiagram-Q3Y3IDCW-DnVuWTpF.js +1 -0
- package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_wardleyDiagram-IVAIDKWA-B2vDAqpl.js +1 -0
- package/web/dist/assets/{arc-BTAvbR16.js → arc-Df9SI9yh.js} +1 -1
- package/web/dist/assets/{architecture-WOLXFQ4H-fewLD5_7.js → architecture-WOLXFQ4H-iO49E8Y1.js} +1 -1
- package/web/dist/assets/{blockDiagram-RJAE7QA3-ClmgyxZl.js → blockDiagram-RJAE7QA3-D8d1ukIs.js} +1 -1
- package/web/dist/assets/channel-C2-OT56h.js +1 -0
- package/web/dist/assets/{chunk-55XCU76K-DdC5EhZG.js → chunk-55XCU76K-C5wOvqDb.js} +1 -1
- package/web/dist/assets/{chunk-7S6XSTFO-CgToWHK6.js → chunk-7S6XSTFO-WIrWcDbG.js} +1 -1
- package/web/dist/assets/{chunk-AQDTSKZB-D-rQMnp7.js → chunk-AQDTSKZB-OkzYqFsw.js} +1 -1
- package/web/dist/assets/{chunk-DSNLSHJZ-cuZoTGMH.js → chunk-DSNLSHJZ-CHEA9kYd.js} +2 -2
- package/web/dist/assets/{chunk-HNE4Q36P-CiKr9iSN.js → chunk-HNE4Q36P-CPsq34CI.js} +1 -1
- package/web/dist/assets/{chunk-VQ5W62QI-DbTfXAAT.js → chunk-VQ5W62QI-CamPnEuF.js} +1 -1
- package/web/dist/assets/{chunk-WQQL4CDK-DHanHfB5.js → chunk-WQQL4CDK-gNypBVAM.js} +1 -1
- package/web/dist/assets/{chunk-ZAUX2LTX-SrjFXpy2.js → chunk-ZAUX2LTX--4WyRBRV.js} +1 -1
- package/web/dist/assets/{chunk-ZCHMQSOR-7Wy8xXQ5.js → chunk-ZCHMQSOR-umv3Lkpn.js} +1 -1
- package/web/dist/assets/{classDiagram-v2-4MYWR3UA-CBe5ZDvZ.js → classDiagram-v2-4MYWR3UA-C1XldpO1.js} +1 -1
- package/web/dist/assets/{cose-bilkent-JH36ORCC-Dljdyvhw.js → cose-bilkent-JH36ORCC-BvugZzFU.js} +1 -1
- package/web/dist/assets/{cynefin-EF2NZ3EQ-CcZU7GFP.js → cynefin-EF2NZ3EQ-6qXB70s4.js} +1 -1
- package/web/dist/assets/{dagre-NK4GIZVX-Bq_6y8qp.js → dagre-NK4GIZVX-DDdlABYw.js} +1 -1
- package/web/dist/assets/{diagram-I5XIIV2D-C2lnnYVG.js → diagram-I5XIIV2D-T8oWy5CY.js} +1 -1
- package/web/dist/assets/{diagram-RJA3I4VV-Bqf8Z6wJ.js → diagram-RJA3I4VV-CkdHTtfV.js} +1 -1
- package/web/dist/assets/{elk-QONTBCTQ-Cdzq_cJ5.js → elk-QONTBCTQ-BiEUbJJX.js} +1 -1
- package/web/dist/assets/{erDiagram-KYQIGQAA-ZJLCjPFC.js → erDiagram-KYQIGQAA-q3WwhyzH.js} +1 -1
- package/web/dist/assets/{eventmodeling-K75KTNOO-DHqeIpes.js → eventmodeling-K75KTNOO-BO5gW_NC.js} +1 -1
- package/web/dist/assets/{flowDiagram-IUGLLWBU-AVgYvXKN.js → flowDiagram-IUGLLWBU-DubjP6UK.js} +1 -1
- package/web/dist/assets/{ganttDiagram-R7GPNTJY-CR1SG1M7.js → ganttDiagram-R7GPNTJY-lKNRH8CI.js} +1 -1
- package/web/dist/assets/{gitGraph-VSP46ZUC-CIZQPK0R.js → gitGraph-VSP46ZUC-Cwlv3omT.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-PDYL7XHY-BhOCWEh6.js → gitGraphDiagram-PDYL7XHY-Bo9GpFvW.js} +1 -1
- package/web/dist/assets/index-CeYhxqkk.js +113 -0
- package/web/dist/assets/index-GJgJwZ23.css +1 -0
- package/web/dist/assets/{info-OHQRW6UA-C2wuRxeQ.js → info-OHQRW6UA-g3MbTRmN.js} +1 -1
- package/web/dist/assets/{infoDiagram-MVDRKAX5-BP5doAB5.js → infoDiagram-MVDRKAX5-CmILYLoB.js} +1 -1
- package/web/dist/assets/{journeyDiagram-BOSHW7O7-BcIDRTPS.js → journeyDiagram-BOSHW7O7-Av8T0U1C.js} +1 -1
- package/web/dist/assets/{kanban-definition-FNSA4HC5-CRH_bcoT.js → kanban-definition-FNSA4HC5-B9NyWQYN.js} +1 -1
- package/web/dist/assets/{line-iA4GPA_S.js → line-BUrll1My.js} +1 -1
- package/web/dist/assets/{linear-BGUKwkTH.js → linear-DjET0phX.js} +1 -1
- package/web/dist/assets/{math-DgNW_bVP.js → math-BJAjkhlu.js} +1 -1
- package/web/dist/assets/{mermaid-parser.core-CsqmgpEv.js → mermaid-parser.core-CaTdkhkS.js} +3 -3
- package/web/dist/assets/{mermaid.core-C1yCGQK6.js → mermaid.core-Cktx937U.js} +4 -4
- package/web/dist/assets/{packet-JDAUHWVQ-2ggaoz2g.js → packet-JDAUHWVQ-DhG6l3PG.js} +1 -1
- package/web/dist/assets/{pie-XZMESJXO-h_yslQQs.js → pie-XZMESJXO-YPcOx7s0.js} +1 -1
- package/web/dist/assets/{pieDiagram-GNMPY74T-6-JZMFyk.js → pieDiagram-GNMPY74T-DesiZKUl.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-D57K4MC3-K5r1HOgL.js → quadrantDiagram-D57K4MC3-Cn1kBVgu.js} +1 -1
- package/web/dist/assets/{radar-ABXABTNO-6fOJGGgq.js → radar-ABXABTNO-CtuRfYSz.js} +1 -1
- package/web/dist/assets/{railroad-I3PHUGI6-B_gUgQ3d.js → railroad-I3PHUGI6-D5Gnyy0R.js} +1 -1
- package/web/dist/assets/{railroad-abnf-LNEFI6M7-CJ_eqiXZ.js → railroad-abnf-LNEFI6M7-Kw4pmJEm.js} +1 -1
- package/web/dist/assets/{railroad-ebnf-DWYD2UWJ-BTsgjLTU.js → railroad-ebnf-DWYD2UWJ-D8Mk5dIe.js} +1 -1
- package/web/dist/assets/{railroad-peg-JR7BVNR7-D91uiLln.js → railroad-peg-JR7BVNR7-R5Jwzs8F.js} +1 -1
- package/web/dist/assets/{requirementDiagram-EQYSW5RA-uapnV_OO.js → requirementDiagram-EQYSW5RA-B9RDq-cL.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-SY24JHO5-BQsbFgUb.js → sankeyDiagram-SY24JHO5-DoEatOm6.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-BLEHNHQU-iRoSYX_e.js → sequenceDiagram-BLEHNHQU-DGE5Z9lT.js} +1 -1
- package/web/dist/assets/{src-BBrKcs9r.js → src-BnEL7pZw.js} +1 -1
- package/web/dist/assets/{stateDiagram-v2-A6IESKWZ-Bzj9z5KH.js → stateDiagram-v2-A6IESKWZ-_vn8W-k_.js} +1 -1
- package/web/dist/assets/{timeline-definition-JXYKGLGB-DLUUuJX0.js → timeline-definition-JXYKGLGB-BaSLTtgR.js} +1 -1
- package/web/dist/assets/{treeView-D4JQ5SDB-CphIQ3n5.js → treeView-D4JQ5SDB-DZElyFgL.js} +1 -1
- package/web/dist/assets/{treemap-SAJKECNS-DqU3insw.js → treemap-SAJKECNS-DWDO4K4G.js} +1 -1
- package/web/dist/assets/{wardley-7MLQ67FV-Ch7d8cmC.js → wardley-7MLQ67FV-DTOUsk6Q.js} +1 -1
- package/web/dist/assets/{xychartDiagram-4URNHDU4-DdxpWBeM.js → xychartDiagram-4URNHDU4-DiaCfgAB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/docs/screenshots/01-chat.png +0 -0
- package/docs/screenshots/02-answer.png +0 -0
- package/docs/screenshots/03-files-diff.png +0 -0
- package/docs/screenshots/04-terminal.png +0 -0
- package/docs/screenshots/05-models.png +0 -0
- package/docs/screenshots/06-skills.png +0 -0
- package/docs/screenshots/07-skills-hub.png +0 -0
- package/docs/screenshots/08-mcp.png +0 -0
- package/docs/screenshots/09-subagents.png +0 -0
- package/docs/screenshots/10-settings.png +0 -0
- package/docs/screenshots/11-plugins.png +0 -0
- package/docs/screenshots/12-voice.png +0 -0
- package/docs/screenshots/13-push.png +0 -0
- package/docs/screenshots/README.md +0 -45
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_abnfDiagram-WEYOIA3C-CvNbnJ75.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_architectureDiagram-ZHVYKT53-T7NZZ4ao.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_c4Diagram-XDPPPE54-ByuAPg39.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_cynefinDiagram-WF24UDRP-BY5DHttT.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-CBK3MWTC-D7ZYyrF7.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-ODIL2UCW-C6kcOZFc.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-PFBEJUXE-CzFPFq0D.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-TYUNFSNR-B4G4T8SL.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_ebnfDiagram-V4P3TTPT-BkQ4xwD0.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_ishikawaDiagram-HPEHV2GE-52xqckJv.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_mindmap-definition-6OY57UG3-7cQksbFn.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_pegDiagram-ECIUJNBX-udgtBpwv.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_railroadDiagram-Y72NRBL7-Cd2dcQWz.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanes-B3ENRKFQ-p34pL1Yk.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanesDiagram-AYQRV4AK-BslfPs4r.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_usecaseDiagram-WPOLBNNX-DJZoN9Xk.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_vennDiagram-Q3Y3IDCW-DPXo67cZ.js +0 -1
- package/web/dist/assets/_omaw-mermaid-pruned_chunk_wardleyDiagram-IVAIDKWA-BE6ukwkg.js +0 -1
- package/web/dist/assets/channel-DfaYIFFE.js +0 -1
- package/web/dist/assets/index-CZQjJbRg.js +0 -104
- package/web/dist/assets/index-ChDUWKZX.css +0 -1
- /package/web/dist/assets/{_omaw-mermaid-pruned_katex_katex-Zojedrce.js → _oh-my-agent-web-mermaid-pruned_katex_katex-Zojedrce.js} +0 -0
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
* ⚠️ `user/message` 里大量是**系统注入**(runtime-context / skill-catalog / goal …):
|
|
21
21
|
* 只有 `source.kind === "user"` 才是真的用户发言,其余一律不进对话区、也不拿来取标题。
|
|
22
22
|
*/
|
|
23
|
-
import { existsSync, readFileSync, readdirSync,
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { zstdDecompressSync } from "node:zlib";
|
|
26
26
|
/** projectKey(cwd) —— 镜像 DSH 的会话目录命名:分隔符 → "-",其它非法字符 → ~XXXX(大写 hex) */
|
|
27
|
-
|
|
27
|
+
function projectKey(cwd) {
|
|
28
28
|
let readable = "";
|
|
29
29
|
let separatorRun = false;
|
|
30
30
|
for (let i = 0; i < cwd.length; i += 1) {
|
|
@@ -47,7 +47,7 @@ export function projectKey(cwd) {
|
|
|
47
47
|
return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
|
|
48
48
|
}
|
|
49
49
|
/** 多帧 zstd 解压:按魔数切帧;切错了就向后合并重试(数据体里可能误命中魔数) */
|
|
50
|
-
|
|
50
|
+
function zstdDecompressAll(buf) {
|
|
51
51
|
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
52
52
|
const starts = [];
|
|
53
53
|
for (let i = 0; i + 4 <= buf.length; i += 1) {
|
|
@@ -181,39 +181,6 @@ export function firstUserText(log, maxChars = 30) {
|
|
|
181
181
|
}
|
|
182
182
|
return null;
|
|
183
183
|
}
|
|
184
|
-
/** 递归列出目录下的会话日志文件(按 mtime 倒序) */
|
|
185
|
-
export function findSessionFiles(root) {
|
|
186
|
-
if (!existsSync(root))
|
|
187
|
-
return [];
|
|
188
|
-
const out = [];
|
|
189
|
-
const walk = (dir, depth) => {
|
|
190
|
-
if (depth > 3)
|
|
191
|
-
return;
|
|
192
|
-
let entries;
|
|
193
|
-
try {
|
|
194
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
for (const entry of entries) {
|
|
200
|
-
const full = join(dir, entry.name);
|
|
201
|
-
if (entry.isDirectory())
|
|
202
|
-
walk(full, depth + 1);
|
|
203
|
-
else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl"))
|
|
204
|
-
out.push(full);
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
walk(root, 0);
|
|
208
|
-
return out.sort((left, right) => {
|
|
209
|
-
try {
|
|
210
|
-
return statSync(right).mtimeMs - statSync(left).mtimeMs;
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
return 0;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
184
|
/** 按会话 id 找它的日志文件:优先用 cwd 直算目录,否则在 sessions 下一层找 */
|
|
218
185
|
export function locateSessionFile(sessionRoot, sessionId, cwd) {
|
|
219
186
|
const candidates = [];
|
|
@@ -243,3 +210,47 @@ export function locateSessionFile(sessionRoot, sessionId, cwd) {
|
|
|
243
210
|
}
|
|
244
211
|
return null;
|
|
245
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* 删掉一条会话在 DSH 自己存储里的目录(`<sessions>/<projectKey>/<sessionId>/`)。
|
|
215
|
+
*
|
|
216
|
+
* 用途只有一个:**模型目录探针**临时开的那条会话(见 catalog-probe.ts)别留在
|
|
217
|
+
* DSH 的 web / tui 历史里。做法是先按 cwd 直算、再扫一层(与 locateSessionFile 同样的
|
|
218
|
+
* 两条路径)—— 目录命名细节不对也知道要删哪个,删不掉就算了,侧栏还要按临时目录过滤。
|
|
219
|
+
*
|
|
220
|
+
* 返回 true = 真的删掉了。
|
|
221
|
+
*/
|
|
222
|
+
export function deleteDshSession(sessionRoot, sessionId, cwd) {
|
|
223
|
+
const groups = [];
|
|
224
|
+
if (cwd)
|
|
225
|
+
groups.push(projectKey(cwd));
|
|
226
|
+
try {
|
|
227
|
+
for (const entry of readdirSync(sessionRoot, { withFileTypes: true })) {
|
|
228
|
+
if (entry.isDirectory() && !groups.includes(entry.name))
|
|
229
|
+
groups.push(entry.name);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
let removed = false;
|
|
236
|
+
for (const group of groups) {
|
|
237
|
+
const dir = join(sessionRoot, group, sessionId);
|
|
238
|
+
if (!existsSync(dir))
|
|
239
|
+
continue;
|
|
240
|
+
try {
|
|
241
|
+
rmSync(dir, { recursive: true, force: true });
|
|
242
|
+
removed = true;
|
|
243
|
+
// 组目录空了就一起删(DSH 自己也会留一堆空目录,但没必要再添一个)
|
|
244
|
+
try {
|
|
245
|
+
rmdirSync(join(sessionRoot, group));
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* 还有别的会话在里头 */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
/* 删不掉就算了 */
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return removed;
|
|
256
|
+
}
|
|
@@ -34,7 +34,7 @@ export function parseModelValue(value) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
/** 我们在界面上用的模型 id:`provider/model`(可读、可回填) */
|
|
37
|
-
|
|
37
|
+
function modelIdOf(choice) {
|
|
38
38
|
const parsed = parseModelValue(choice.value);
|
|
39
39
|
return parsed ? `${parsed.provider}/${parsed.model}` : null;
|
|
40
40
|
}
|
|
@@ -51,7 +51,7 @@ export function modelOptions(config) {
|
|
|
51
51
|
models.push({
|
|
52
52
|
id,
|
|
53
53
|
label: choice.name ?? id,
|
|
54
|
-
...(
|
|
54
|
+
...(current && id === `${current.provider}/${current.model}` ? { current: true } : {}),
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
return models;
|
|
@@ -121,9 +121,7 @@ function textOfContent(update) {
|
|
|
121
121
|
if (!content)
|
|
122
122
|
return "";
|
|
123
123
|
if (Array.isArray(content)) {
|
|
124
|
-
return content
|
|
125
|
-
.map((item) => (item?.content?.type === "text" ? (item.content.text ?? "") : ""))
|
|
126
|
-
.join("");
|
|
124
|
+
return content.map((item) => (item?.content?.type === "text" ? (item.content.text ?? "") : "")).join("");
|
|
127
125
|
}
|
|
128
126
|
return content.type === "text" ? (content.text ?? "") : "";
|
|
129
127
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把「下一步建议」的写法带进 dsh(ADR-0022 的 dsh 版)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么只能走 patch 文件:ACP 是**唯一**入口,而它的 `session/new` 只有
|
|
5
|
+
* `cwd / additionalDirectories / mcpServers / _meta`(schema 实测),没有系统提示字段;
|
|
6
|
+
* dsh 的系统提示由插件栈里的 `@deepseek-ai/dsh-system-prompt` 组装,其中
|
|
7
|
+
* `personaSuffix` 排在系统提示末尾 —— 正好是这段说明该待的位置。
|
|
8
|
+
*
|
|
9
|
+
* 两个必须写清的约束:
|
|
10
|
+
* 1. patch 的 `config` 是**整块替换**(不是深合并):只写 `personaSuffix` 会把 acp 模板
|
|
11
|
+
* 原本的 `personaPrefix` 冲掉(实测 dump 里会整个消失)。所以两句 persona 都带上,
|
|
12
|
+
* 取值照抄 `@deepseek-ai/dsh-acp-app` 的默认值;dsh 改这两句默认值时这里要跟着改。
|
|
13
|
+
* 2. 它是**进程级**的:dsh 子进程启动时才 compose 配置栈,所以改完「想想」条数要
|
|
14
|
+
* 重启工作台(引擎)才生效 —— 不像 pi / codex 那样每条新会话现读。
|
|
15
|
+
*/
|
|
16
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
/** `@deepseek-ai/dsh-acp-app` 给 acp profile 的默认 persona(照抄,别改措辞) */
|
|
19
|
+
const PERSONA_PREFIX = "You are a coding agent powered by the {{model}} model.";
|
|
20
|
+
const PERSONA_SUFFIX = "Your working directory is {{cwd}}.";
|
|
21
|
+
/** YAML 块标量的缩进:`personaSuffix: |` 下的内容比 key 深两级 */
|
|
22
|
+
function indentBlock(text) {
|
|
23
|
+
return text
|
|
24
|
+
.split("\n")
|
|
25
|
+
.map((line) => (line === "" ? "" : ` ${line}`))
|
|
26
|
+
.join("\n");
|
|
27
|
+
}
|
|
28
|
+
/** 纯函数:生成 patch 文件内容(喂给 `dsh --patch <file>`) */
|
|
29
|
+
export function dshSuggestionPatch(guidance) {
|
|
30
|
+
return [
|
|
31
|
+
"# 由 oh-my-agent-web 生成(不要手改):给 dsh 的 system prompt 追加「下一步建议」写法。",
|
|
32
|
+
"# 改条数请改设置里的「想想」,然后重启工作台 —— dsh 的配置栈是进程启动时 compose 的。",
|
|
33
|
+
"- id: system-prompt",
|
|
34
|
+
" config:",
|
|
35
|
+
" personaPrefix: |",
|
|
36
|
+
indentBlock(PERSONA_PREFIX),
|
|
37
|
+
" personaSuffix: |",
|
|
38
|
+
indentBlock(`${PERSONA_SUFFIX}\n\n${guidance}`),
|
|
39
|
+
"",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
/** 写到一个临时目录里(不污染用户的 `dsh-patches` 目录),返回 patch 文件路径 */
|
|
43
|
+
export function writeDshSuggestionPatch(dir, guidance) {
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
const file = join(dir, "dsh-suggestion-patch.yml");
|
|
46
|
+
writeFileSync(file, dshSuggestionPatch(guidance), "utf8");
|
|
47
|
+
return file;
|
|
48
|
+
}
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* 拿 header(cwd/createdAt)与 `session/title`,缓存里没有时兜底。
|
|
11
11
|
*
|
|
12
12
|
* 为什么要自己扫目录:ACP 的 `session/list` 只回 `{sessionId, cwd}`,**没有标题和时间**;
|
|
13
|
-
* 侧栏需要标题/时间/模型,所以直接读 DSH
|
|
14
|
-
*
|
|
13
|
+
* 侧栏需要标题/时间/模型,所以直接读 DSH 自己的持久化,并额外用 workspace.json 与投影
|
|
14
|
+
* 缓存,避免每个会话都解压日志)。
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
@@ -19,7 +19,7 @@ import { basename, join } from "node:path";
|
|
|
19
19
|
import { firstUserText, locateSessionFile, readSessionHeader, readSessionLog, sessionTitle } from "./dsh-log.js";
|
|
20
20
|
import { isScratchCwd } from "./scratch.js";
|
|
21
21
|
/** 读 workspace.json;没有/坏了都返回空表(侧栏退回按 cwd 分组) */
|
|
22
|
-
|
|
22
|
+
function readWorkspaces(home) {
|
|
23
23
|
const file = join(home, "storages", "workspace.json");
|
|
24
24
|
if (!existsSync(file))
|
|
25
25
|
return {};
|
|
@@ -31,7 +31,7 @@ export function readWorkspaces(home) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
/** workspace.json → path → 项目名(title 优先,缺了用目录名) */
|
|
34
|
-
|
|
34
|
+
function workspaceNames(workspaces) {
|
|
35
35
|
const out = new Map();
|
|
36
36
|
for (const row of Object.values(workspaces.tables?.workspaces ?? {})) {
|
|
37
37
|
const path = row.path?.trim();
|
|
@@ -149,18 +149,17 @@ function summaryOf(home, session) {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
/** 纯分组:会话 → 项目(workspace 的 path 精确匹配;否则按 cwd 单独成组) */
|
|
152
|
-
|
|
152
|
+
function groupDshSessions(sessions, names, options = {}) {
|
|
153
153
|
const groups = new Map();
|
|
154
154
|
for (const session of sessions) {
|
|
155
155
|
if (options.archived?.has(session.id))
|
|
156
156
|
continue;
|
|
157
157
|
const key = session.cwd;
|
|
158
|
-
const group = groups.get(key) ??
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
158
|
+
const group = groups.get(key) ?? {
|
|
159
|
+
cwd: session.cwd,
|
|
160
|
+
name: names.get(session.cwd) ?? basename(session.cwd) ?? session.cwd,
|
|
161
|
+
sessions: [],
|
|
162
|
+
};
|
|
164
163
|
group.sessions.push({
|
|
165
164
|
id: session.id,
|
|
166
165
|
engineSessionId: session.id,
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 为什么走 ACP(`dsh --profile acp`)而不是 SDK(`--profile sdk`)或 headless:
|
|
5
5
|
* - SDK 面只有 `initialize / session/prompt / shutdown`,没有会话列表/续接/取消/配置/权限,
|
|
6
|
-
*
|
|
6
|
+
* 这些要么自己读文件、要么自己写插件补(等于把 26 个本地依赖 + 重启换模型揽进来);
|
|
7
7
|
* - ACP 原生给了 `session/new|list|resume|close`、`session/set_config_option`(模型/思考档)、
|
|
8
8
|
* `session/cancel`、`session/request_permission`,我们只需当客户端;
|
|
9
9
|
* - headless 是一次性进程,没有权限问询。
|
|
10
10
|
*
|
|
11
|
-
* 协议细节见 ADR-0037;映射规则在 dsh-map.ts
|
|
11
|
+
* 协议细节见 ADR-0037;映射规则在 dsh-map.ts(纯函数)。
|
|
12
12
|
*/
|
|
13
13
|
import { createJsonRpcStdioClient } from "./jsonrpc-stdio.js";
|
|
14
14
|
/** ACP 要求初始化的能力协商;我们不用它的 fs/terminal 桥(工具都在 DSH 进程里跑) */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { createDshClient } from "./dsh-transport.js";
|
|
4
|
-
import {
|
|
4
|
+
import { withProbeCwd } from "./catalog-probe.js";
|
|
5
|
+
import { deleteDshSession, locateSessionFile, readSessionHeader, readSessionLog } from "./dsh-log.js";
|
|
5
6
|
import { logToTranscript, logToUiMessages, statsFromLog } from "./dsh-history.js";
|
|
6
7
|
import { sanitizeHistoricalActions } from "../markers/actions.js";
|
|
7
8
|
import { currentModelId, currentThinkingLevel, friendlyDshError, modelOptions, modelValueFor, parseModelValue, permissionApprovalRequest, permissionOptionId, pickDefaultModelValue, stopReasonToTurnEnd, thinkingLevels, updateToEvent, } from "./dsh-map.js";
|
|
@@ -24,6 +25,8 @@ export function createDshEngine(options = {}) {
|
|
|
24
25
|
* 真正切模型时仍会先 resume(拿不到就报可读错误)。
|
|
25
26
|
*/
|
|
26
27
|
let lastConfig = [];
|
|
28
|
+
/** 目录探针跑过没有(每个实例只探一次;`reloadModels` / `dispose` 重置) */
|
|
29
|
+
let catalogProbed = false;
|
|
27
30
|
const rememberTool = (id, update) => {
|
|
28
31
|
toolFacts.set(id, { title: update.title, rawInput: update.rawInput });
|
|
29
32
|
if (toolFacts.size > MAX_TOOL_FACTS) {
|
|
@@ -60,6 +63,30 @@ export function createDshEngine(options = {}) {
|
|
|
60
63
|
return binding.config;
|
|
61
64
|
return [...bindings.values()].find((item) => item.config.length > 0)?.config ?? lastConfig;
|
|
62
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* 没有会话时把模型目录弄进来(原因与代价见 catalog-probe.ts 文件头):
|
|
68
|
+
* DSH 的目录只在 `session/new` 的 `configOptions` 里 → 开个临时会话抄一份,
|
|
69
|
+
* 然后 `session/close` 并删掉它在 `~/.dsh` 里的会话目录(不在 DSH 自己的历史里留空会话)。
|
|
70
|
+
*/
|
|
71
|
+
const warmCatalog = async () => {
|
|
72
|
+
if (catalogProbed || lastConfig.length > 0)
|
|
73
|
+
return;
|
|
74
|
+
catalogProbed = true;
|
|
75
|
+
await withProbeCwd(async (cwd) => {
|
|
76
|
+
const created = await client.request("session/new", {
|
|
77
|
+
cwd,
|
|
78
|
+
mcpServers: [],
|
|
79
|
+
});
|
|
80
|
+
const probeId = created?.sessionId;
|
|
81
|
+
if (!probeId)
|
|
82
|
+
return;
|
|
83
|
+
applyConfig(undefined, created.configOptions);
|
|
84
|
+
await client.request("session/close", { sessionId: probeId }).catch(() => undefined);
|
|
85
|
+
if (!deleteDshSession(sessionsRoot, probeId, cwd)) {
|
|
86
|
+
options.log?.(`dsh:模型目录探针会话没删掉(${probeId}),侧栏不会显示(临时目录)`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
63
90
|
/** 历史会话首次使用时才 resume(ACP 的 resume 会拿到写句柄,所以按需) */
|
|
64
91
|
const ensureLive = async (conversationId) => {
|
|
65
92
|
const binding = bindingOf(conversationId);
|
|
@@ -147,9 +174,24 @@ export function createDshEngine(options = {}) {
|
|
|
147
174
|
version: () => client.version(),
|
|
148
175
|
async prewarm() {
|
|
149
176
|
await client.ensureStarted();
|
|
177
|
+
// 顺手把模型目录也弄进来:否则切过来、还没开会话时下拉是空的(见 catalog-probe.ts)
|
|
178
|
+
await warmCatalog().catch((error) => {
|
|
179
|
+
options.log?.(`dsh:读取模型目录失败:${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
/** 重读模型表(改完 dsh 自己的 provider / 模型配置后):清掉快照,重新探一次 */
|
|
183
|
+
async reloadModels() {
|
|
184
|
+
lastConfig = [];
|
|
185
|
+
catalogProbed = false;
|
|
186
|
+
await warmCatalog().catch((error) => {
|
|
187
|
+
options.log?.(`dsh:重读模型目录失败:${error instanceof Error ? error.message : String(error)}`);
|
|
188
|
+
});
|
|
150
189
|
},
|
|
151
190
|
async createSession(sessionOptions) {
|
|
152
|
-
const created = await client.request("session/new", {
|
|
191
|
+
const created = await client.request("session/new", {
|
|
192
|
+
cwd: sessionOptions.workspace,
|
|
193
|
+
mcpServers: [],
|
|
194
|
+
});
|
|
153
195
|
const sessionId = created?.sessionId;
|
|
154
196
|
if (!sessionId)
|
|
155
197
|
throw new Error("dsh 没有返回 sessionId");
|
|
@@ -222,6 +264,7 @@ export function createDshEngine(options = {}) {
|
|
|
222
264
|
},
|
|
223
265
|
async dispose() {
|
|
224
266
|
lastConfig = [];
|
|
267
|
+
catalogProbed = false;
|
|
225
268
|
bindings.clear();
|
|
226
269
|
bySession.clear();
|
|
227
270
|
active.clear();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function createEngineVersionProber(options) {
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
const inflight = new Map();
|
|
4
|
+
const probe = (descriptor) => {
|
|
5
|
+
const known = inflight.get(descriptor.name);
|
|
6
|
+
if (known)
|
|
7
|
+
return known;
|
|
8
|
+
const pending = (async () => {
|
|
9
|
+
try {
|
|
10
|
+
const value = await descriptor.version?.();
|
|
11
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
options.onLog?.(`探测 ${descriptor.name} 的版本号失败:${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
inflight.set(descriptor.name, pending);
|
|
19
|
+
return pending;
|
|
20
|
+
};
|
|
21
|
+
const snapshot = () => Object.fromEntries(cache);
|
|
22
|
+
return {
|
|
23
|
+
known: snapshot,
|
|
24
|
+
remember(name, version) {
|
|
25
|
+
// 握手给的版本最准:记下来,后面 refresh 也不会把它擦掉
|
|
26
|
+
if (version && version.trim() !== "")
|
|
27
|
+
cache.set(name, version.trim());
|
|
28
|
+
},
|
|
29
|
+
async refresh() {
|
|
30
|
+
await Promise.all(options.descriptors().map(async (descriptor) => {
|
|
31
|
+
if (cache.has(descriptor.name))
|
|
32
|
+
return;
|
|
33
|
+
const value = await probe(descriptor);
|
|
34
|
+
// 期间可能有 remember()(握手)写过更准的值,别覆盖
|
|
35
|
+
if (!cache.has(descriptor.name))
|
|
36
|
+
cache.set(descriptor.name, value);
|
|
37
|
+
}));
|
|
38
|
+
return snapshot();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const BIG_OUTPUT_CHARS = 200 * 1024;
|
|
2
2
|
/** 触发词 → 脚本。默认行为:把用户输入原样念一遍,证明「逐字说话」。 */
|
|
3
|
-
|
|
3
|
+
function planFor(text) {
|
|
4
4
|
const key = text.toLowerCase();
|
|
5
5
|
if (key.includes("boom")) {
|
|
6
6
|
return [
|
|
@@ -80,6 +80,8 @@ export function createFakeEngine(options = {}) {
|
|
|
80
80
|
const switchedModels = [];
|
|
81
81
|
let approvalHandler = null;
|
|
82
82
|
let toolCallSeq = 0;
|
|
83
|
+
/** 派生会话的 id 序号(真引擎是 pi 的 `createSessionId()`,同样不复用传进来的 id) */
|
|
84
|
+
let forkSeq = 0;
|
|
83
85
|
async function streamText(sessionId, text, onEvent, slow) {
|
|
84
86
|
const buffer = sessions.get(sessionId)?.transcript;
|
|
85
87
|
for (let index = 0; index < text.length; index += chunkChars) {
|
|
@@ -95,12 +97,41 @@ export function createFakeEngine(options = {}) {
|
|
|
95
97
|
return {
|
|
96
98
|
version: () => "fake",
|
|
97
99
|
async createSession(sessionOptions) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
const ephemeral = sessionOptions.ephemeral === true;
|
|
101
|
+
/** 派生:把源会话的转录拄过来(按 `fake-<会话>-<序号>` 里的序号截断) */
|
|
102
|
+
let transcript = [];
|
|
103
|
+
if (sessionOptions.fork) {
|
|
104
|
+
const source = sessions.get(sessionOptions.fork.sourceSessionId);
|
|
105
|
+
// 与真引擎一致:没落盘的会话不能 fork
|
|
106
|
+
if (!source?.sessionPath)
|
|
107
|
+
throw new Error("这条会话还没存过盘(临时对话),不能从它派生分支");
|
|
108
|
+
const cut = Number(/-(\d+)$/.exec(sessionOptions.fork.leafId)?.[1] ?? Number.NaN);
|
|
109
|
+
transcript = Number.isFinite(cut) ? source.transcript.slice(0, cut + 1) : [...source.transcript];
|
|
110
|
+
// 与真引擎一致(pi 的 `createBranchedSession`):派生会**另生一个会话 id**,
|
|
111
|
+
// 不采纳传进来的那个。宿主必须拿回报的 id 当会话 id,否则发消息就会
|
|
112
|
+
// `engine.no_session`(引擎表里只有新 id)—— 这里必须忠实复现,不然测试盖不住。
|
|
113
|
+
forkSeq += 1;
|
|
114
|
+
const forkedId = `forked-${forkSeq}`;
|
|
115
|
+
const forkedPath = `fake-sessions/${forkedId}.jsonl`;
|
|
116
|
+
sessions.set(forkedId, { workspace: sessionOptions.workspace, transcript, sessionPath: forkedPath });
|
|
117
|
+
return { sessionPath: forkedPath, sessionId: forkedId };
|
|
118
|
+
}
|
|
119
|
+
// 临时会话没有会话文件(与 pi 的 `--no-session` 一致)
|
|
120
|
+
const sessionPath = ephemeral ? null : `fake-sessions/${sessionOptions.sessionId}.jsonl`;
|
|
121
|
+
sessions.set(sessionOptions.sessionId, { workspace: sessionOptions.workspace, transcript, sessionPath });
|
|
122
|
+
return { sessionPath };
|
|
123
|
+
},
|
|
124
|
+
/** 临时会话转正:给它一个文件路径(真引擎是原生导出写盘) */
|
|
125
|
+
async persistSession(sessionId) {
|
|
126
|
+
const session = sessions.get(sessionId);
|
|
127
|
+
if (!session)
|
|
128
|
+
return { sessionPath: null };
|
|
129
|
+
if (session.sessionPath)
|
|
130
|
+
return { sessionPath: session.sessionPath };
|
|
131
|
+
if (session.transcript.length === 0)
|
|
132
|
+
return { sessionPath: null };
|
|
133
|
+
session.sessionPath = `fake-sessions/${sessionId}.jsonl`;
|
|
134
|
+
return { sessionPath: session.sessionPath };
|
|
104
135
|
},
|
|
105
136
|
/** 打开已有会话(ADR-0021 §2):假引擎没有真文件,只登记「已打开」 */
|
|
106
137
|
async openSession(openOptions) {
|
|
@@ -22,7 +22,7 @@ async function readJson(path, fallback) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
/** 把 pi 的两份配置读成「可直接 registerProvider 的形状」 */
|
|
25
|
-
|
|
25
|
+
async function readProviderConfigs(agentDir) {
|
|
26
26
|
const catalog = await readJson(join(agentDir, "models.json"), {});
|
|
27
27
|
const store = await readJson(join(agentDir, "models-store.json"), {});
|
|
28
28
|
const auth = await readJson(join(agentDir, "auth.json"), {});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* opencode ACP
|
|
2
|
+
* opencode ACP 的**纯映射**(不碰网络/子进程)。
|
|
3
3
|
*
|
|
4
4
|
* 三件事:
|
|
5
5
|
* 1. `configOptions`(模型 / 思考档)→ 我们的模型下拉与档位;
|
|
@@ -90,7 +90,7 @@ export function modelValueFor(config, modelId) {
|
|
|
90
90
|
* 思考档那一项:按 `category: "thought_level"` 找(dsh 叫 reasoning_effort,opencode 叫 effort)。
|
|
91
91
|
* 不支持推理的模型没有这一项 → 返回 undefined,界面自动隐藏控件。
|
|
92
92
|
*/
|
|
93
|
-
|
|
93
|
+
function thinkingOptionOf(config) {
|
|
94
94
|
return (config ?? []).find((entry) => entry.category === "thought_level" || entry.id === "effort" || entry.id === "reasoning_effort");
|
|
95
95
|
}
|
|
96
96
|
export function thinkingLevels(config) {
|
|
@@ -17,7 +17,7 @@ import { existsSync } from "node:fs";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
/** opencode 的库文件:跟随 XDG(它自己也是这么找数据的) */
|
|
20
|
-
|
|
20
|
+
function opencodeDbFile(env = process.env, home = homedir()) {
|
|
21
21
|
const xdg = env.XDG_DATA_HOME?.trim();
|
|
22
22
|
const dataHome = xdg && xdg !== "" ? xdg : join(home, ".local", "share");
|
|
23
23
|
return join(dataHome, "opencode", "opencode.db");
|
|
@@ -30,13 +30,13 @@ const timestampOf = (value) => {
|
|
|
30
30
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
|
-
* 纯分组:`session/list` 的行 →
|
|
33
|
+
* 纯分组:`session/list` 的行 → 侧栏形状。
|
|
34
34
|
*
|
|
35
35
|
* - 临时目录(/tmp、/var/folders…)里的一次性会话丢掉,别塞噪音进侧栏;
|
|
36
36
|
* - 同 cwd 归一组,组内按更新时间倒序;组之间按「最近一条会话」倒序;
|
|
37
37
|
* - 没有标题的会话不编名字(opencode 自己会给「New session - …」,照它的原文显示)。
|
|
38
38
|
*/
|
|
39
|
-
|
|
39
|
+
function groupOpencodeSessions(rows) {
|
|
40
40
|
// 逐个项目点名时同一条会话会被问到多次(项目根与它的子目录都解析到同一个项目),先去重:
|
|
41
41
|
// 同一个 sessionId 只留 updatedAt 最新的一条。
|
|
42
42
|
const byId = new Map();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isScratchCwd } from "./scratch.js";
|
|
2
|
+
import { withProbeCwd } from "./catalog-probe.js";
|
|
2
3
|
import { currentModelId, currentThinkingLevel, exportStats, exportToTranscript, exportToUiMessages, friendlyOpencodeError, hasPendingUsage, hasUsableToolFacts, isFinalToolStatus, modelOptions, modelValueFor, parseModelValue, permissionApprovalRequest, permissionOptionId, stopReasonToTurnEnd, textEventOf, thinkingConfigId, thinkingLevels, toolEndEvent, toolStartEvent, usageEventOf, } from "./opencode-map.js";
|
|
3
4
|
import { deleteOpencodeSession, readOpencodeExport } from "./opencode-sessions.js";
|
|
4
5
|
import { createOpencodeClient } from "./opencode-transport.js";
|
|
@@ -16,6 +17,7 @@ const EXPORT_CACHE_MS = 3000;
|
|
|
16
17
|
const EXPORT_SETTLE_MS = 3000;
|
|
17
18
|
export function createOpencodeEngine(options = {}) {
|
|
18
19
|
const bin = options.bin ?? "opencode";
|
|
20
|
+
const removeSession = options.deleteSession ?? ((engineSessionId) => deleteOpencodeSession(bin, engineSessionId));
|
|
19
21
|
const client = options.client ?? createOpencodeClient({ bin, args: options.args, log: options.log ?? (() => { }) });
|
|
20
22
|
const bindings = new Map();
|
|
21
23
|
const byEngineSession = new Map();
|
|
@@ -33,6 +35,8 @@ export function createOpencodeEngine(options = {}) {
|
|
|
33
35
|
* 真正切模型时仍会先 resume(拿不到就报可读错误)。
|
|
34
36
|
*/
|
|
35
37
|
let lastConfig = [];
|
|
38
|
+
/** 目录探针跑过没有(每个实例只探一次;`reloadModels` / `dispose` 重置) */
|
|
39
|
+
let catalogProbed = false;
|
|
36
40
|
const rememberTool = (facts) => {
|
|
37
41
|
const previous = toolFacts.get(facts.toolCallId);
|
|
38
42
|
// 后到的 update 更详细(title 从 "bash" 变成真命令),合并而不是覆盖
|
|
@@ -57,6 +61,27 @@ export function createOpencodeEngine(options = {}) {
|
|
|
57
61
|
return binding.config;
|
|
58
62
|
return [...bindings.values()].find((item) => item.config.length > 0)?.config ?? lastConfig;
|
|
59
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* 没有会话时把模型目录弄进来(原因与代价见 catalog-probe.ts 文件头)。
|
|
66
|
+
* opencode 的目录只在 `session/new` 的 `configOptions` 里,所以开一个临时会话抄一份,
|
|
67
|
+
* 然后 `session/close` + `opencode session delete` 收掉(不留在 CLI/TUI 的历史里)。
|
|
68
|
+
*/
|
|
69
|
+
const warmCatalog = async () => {
|
|
70
|
+
if (catalogProbed || lastConfig.length > 0)
|
|
71
|
+
return;
|
|
72
|
+
catalogProbed = true;
|
|
73
|
+
await withProbeCwd(async (cwd) => {
|
|
74
|
+
const created = await client.request("session/new", { cwd, mcpServers: [] });
|
|
75
|
+
const engineSessionId = created?.sessionId;
|
|
76
|
+
if (!engineSessionId)
|
|
77
|
+
return;
|
|
78
|
+
applyConfig(undefined, created.configOptions);
|
|
79
|
+
await client.request("session/close", { sessionId: engineSessionId }).catch(() => undefined);
|
|
80
|
+
await removeSession(engineSessionId).catch((error) => {
|
|
81
|
+
options.log?.(`opencode:模型目录探针会话没删掉(${engineSessionId}):${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
};
|
|
60
85
|
const invalidateExport = (conversationId) => {
|
|
61
86
|
const binding = bindingOf(conversationId);
|
|
62
87
|
if (binding)
|
|
@@ -202,6 +227,18 @@ export function createOpencodeEngine(options = {}) {
|
|
|
202
227
|
version: () => client.version(),
|
|
203
228
|
async prewarm() {
|
|
204
229
|
await client.ensureStarted();
|
|
230
|
+
// 顺手把模型目录也弄进来:否则切过来、还没开会话时下拉是空的(见 catalog-probe.ts)
|
|
231
|
+
await warmCatalog().catch((error) => {
|
|
232
|
+
options.log?.(`opencode:读取模型目录失败:${error instanceof Error ? error.message : String(error)}`);
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
/** 重读模型表(改完 opencode 自己的配置后):清掉快照,重新探一次 */
|
|
236
|
+
async reloadModels() {
|
|
237
|
+
lastConfig = [];
|
|
238
|
+
catalogProbed = false;
|
|
239
|
+
await warmCatalog().catch((error) => {
|
|
240
|
+
options.log?.(`opencode:重读模型目录失败:${error instanceof Error ? error.message : String(error)}`);
|
|
241
|
+
});
|
|
205
242
|
},
|
|
206
243
|
async createSession(sessionOptions) {
|
|
207
244
|
const created = await client
|
|
@@ -324,6 +361,7 @@ export function createOpencodeEngine(options = {}) {
|
|
|
324
361
|
toolFacts.clear();
|
|
325
362
|
exportCache.clear();
|
|
326
363
|
settledAt.clear();
|
|
364
|
+
catalogProbed = false; // 探针会话随进程一起没了:下次切回来重新探
|
|
327
365
|
client.close();
|
|
328
366
|
},
|
|
329
367
|
setApprovalHandler(handler) {
|
|
@@ -387,7 +425,7 @@ export function createOpencodeEngine(options = {}) {
|
|
|
387
425
|
},
|
|
388
426
|
/** opencode 原生删除(`opencode session delete <sid>`);改名没接(ACP 无此方法) */
|
|
389
427
|
async deleteSession(handle) {
|
|
390
|
-
await
|
|
428
|
+
await removeSession(handle);
|
|
391
429
|
exportCache.delete(handle);
|
|
392
430
|
},
|
|
393
431
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SDK 会话事件 → 端口事件的映射(ADR-0003 的端口实现里最容易出错的一层)。
|
|
3
3
|
*
|
|
4
4
|
* 从 `pi-sdk.ts` 拆出(400 行护栏):这是**纯映射**,与「怎么建会话、怎么订阅」无关,
|
|
5
|
-
* 所以可以脱离 SDK
|
|
5
|
+
* 所以可以脱离 SDK 用合成事件直接驱动(整张映射表都能覆盖,不烧 token)。
|
|
6
6
|
*/
|
|
7
7
|
import { parseUsage } from "../util/usage.js";
|
|
8
8
|
function asRecord(value) {
|
|
@@ -22,7 +22,7 @@ export function resultText(result) {
|
|
|
22
22
|
.join("\n");
|
|
23
23
|
}
|
|
24
24
|
/** 把 bash 工具结果的退出码抠出来(不同工具 details 形状不同,取不到就不给) */
|
|
25
|
-
|
|
25
|
+
function resultExitCode(result) {
|
|
26
26
|
const details = asRecord(asRecord(result).details);
|
|
27
27
|
const value = details.exitCode ?? asRecord(asRecord(result).structuredContent).exitCode;
|
|
28
28
|
return typeof value === "number" ? value : undefined;
|
|
@@ -30,7 +30,7 @@ export function resultExitCode(result) {
|
|
|
30
30
|
/** 一行预览的长度:超过就截(参数再长,抬头也只有一行) */
|
|
31
31
|
const SUMMARY_CHARS = 200;
|
|
32
32
|
/** 完整参数的上限:不能让一条快照因参数膨胀(正常命令远小于这个数) */
|
|
33
|
-
|
|
33
|
+
const FULL_ARGS_CHARS = 4000;
|
|
34
34
|
/** 把任意参数变成文本(字符串原样返回:有的引擎直接给命令串,不是 JSON) */
|
|
35
35
|
function argsText(args) {
|
|
36
36
|
if (typeof args === "string")
|
|
@@ -63,7 +63,7 @@ function providerError(raw) {
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* 纯函数:把 SDK 会话事件翻译成端口事件。返回 null 表示这个事件不影响 UI。
|
|
66
|
-
*
|
|
66
|
+
* (合成事件就能覆盖这张映射表 —— 不需要真的连模型。)
|
|
67
67
|
*/
|
|
68
68
|
export function mapSessionEvent(event) {
|
|
69
69
|
const record = asRecord(event);
|