@youweichen/pi-harness 0.0.0-stage → 1.1.0
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/LICENSE +21 -0
- package/README.md +74 -2
- package/bin/pi-harness.mjs +1819 -0
- package/deploy/com.youweichen.pi-harness.plist +48 -0
- package/deploy/nginx-subpath.conf +88 -0
- package/deploy/pi-harness-task.xml +71 -0
- package/deploy/pi-harness.service +31 -0
- package/dist/server/agent-service.js +3131 -0
- package/dist/server/app-version.js +19 -0
- package/dist/server/attachments.js +526 -0
- package/dist/server/auth-file.js +61 -0
- package/dist/server/bg-servers.js +154 -0
- package/dist/server/client-eviction.js +19 -0
- package/dist/server/client-id.js +9 -0
- package/dist/server/client-state.js +202 -0
- package/dist/server/codemode-image-routes.js +36 -0
- package/dist/server/codemode-presentation.js +19 -0
- package/dist/server/component-updates.js +135 -0
- package/dist/server/context-breakdown.js +56 -0
- package/dist/server/control-socket.js +178 -0
- package/dist/server/conversation.js +65 -0
- package/dist/server/dir-browser.js +77 -0
- package/dist/server/editor-snapshot.js +38 -0
- package/dist/server/ensure-bash.js +80 -0
- package/dist/server/extension-display.js +23 -0
- package/dist/server/extensions-catalog.js +64 -0
- package/dist/server/extensions-model.js +46 -0
- package/dist/server/extensions-routes.js +255 -0
- package/dist/server/extensions-worker.js +188 -0
- package/dist/server/files-service.js +787 -0
- package/dist/server/index.js +1033 -0
- package/dist/server/markdown-images.js +42 -0
- package/dist/server/model-admin.js +801 -0
- package/dist/server/model-config-merge.js +32 -0
- package/dist/server/native-mcp-config.js +103 -0
- package/dist/server/native-mcp-presentation.js +12 -0
- package/dist/server/native-prompt-template.js +12 -0
- package/dist/server/native-tools.js +32 -0
- package/dist/server/node-agent.js +262 -0
- package/dist/server/node-sources.js +142 -0
- package/dist/server/node-workbench.js +729 -0
- package/dist/server/origin-policy.js +50 -0
- package/dist/server/patch-node-pty.js +96 -0
- package/dist/server/pi-environment.js +203 -0
- package/dist/server/plan/extension.js +74 -0
- package/dist/server/plan/progress.js +90 -0
- package/dist/server/plan/settings.js +41 -0
- package/dist/server/plan/state.js +123 -0
- package/dist/server/plugin-facilities.js +304 -0
- package/dist/server/plugin-updater.js +245 -0
- package/dist/server/plugins.js +712 -0
- package/dist/server/private-file.js +28 -0
- package/dist/server/process-utils.js +112 -0
- package/dist/server/prompt-delivery.js +80 -0
- package/dist/server/protocol-version.js +11 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/provider-auth.js +99 -0
- package/dist/server/query-cache.js +45 -0
- package/dist/server/recovery-state.js +37 -0
- package/dist/server/scm.js +459 -0
- package/dist/server/security-headers.js +24 -0
- package/dist/server/serialize.js +254 -0
- package/dist/server/session-file-read.js +225 -0
- package/dist/server/session-record-worker.js +21 -0
- package/dist/server/session-reload.js +13 -0
- package/dist/server/session-tree-controller.js +250 -0
- package/dist/server/session-tree.js +191 -0
- package/dist/server/settings-service.js +53 -0
- package/dist/server/skills-service.js +87 -0
- package/dist/server/slash-commands.js +286 -0
- package/dist/server/sqlite-preview.js +58 -0
- package/dist/server/sqlite-query.js +67 -0
- package/dist/server/sqlite-worker.js +12 -0
- package/dist/server/system-prompt-files.js +158 -0
- package/dist/server/system-prompt-routes.js +40 -0
- package/dist/server/system-prompt-view.js +49 -0
- package/dist/server/task-progress.js +233 -0
- package/dist/server/terminals.js +833 -0
- package/dist/server/text-sniff.js +269 -0
- package/dist/server/thinking-timing.js +91 -0
- package/dist/server/token-auth.js +37 -0
- package/dist/server/tool-output.js +97 -0
- package/dist/server/tool-text-incidents.js +82 -0
- package/dist/server/tool-text.js +67 -0
- package/dist/server/uploads.js +112 -0
- package/dist/server/user-attachments.js +96 -0
- package/dist/server/webui-context.js +257 -0
- package/dist/server/wiki-links.js +136 -0
- package/dist/server/wiki-pdf-worker.js +23 -0
- package/dist/server/wiki-pdf.js +36 -0
- package/dist/server/wiki-routes.js +184 -0
- package/dist/server/wiki-service.js +524 -0
- package/electron/main.mjs +607 -0
- package/extensions/webui.ts +192 -0
- package/package.json +122 -4
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/NodeWorkbench-BiP2ktQP.js +3 -0
- package/web/dist/assets/TerminalPanel-CKaSUsN-.js +2 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-CnwRJDRL.js +1 -0
- package/web/dist/assets/arc-CG4Q7-W_.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-BrGqX_Rg.js +36 -0
- package/web/dist/assets/blockDiagram-I7D4REHJ-C2t0PjZT.js +129 -0
- package/web/dist/assets/c4Diagram-7LVT6UL2-CadrcgGL.js +38 -0
- package/web/dist/assets/channel-C27Jizb6.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-CC3V2ITZ.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-DqKSbGmE.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-_CDr3JbY.js +1 -0
- package/web/dist/assets/chunk-IMKFNOWR-BIR4H4fX.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-CrsdBGXT.js +1 -0
- package/web/dist/assets/chunk-POPQ4Y6H-C6q9kQmf.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-l2XXTr2v.js +88 -0
- package/web/dist/assets/chunk-TICWLB2K-29I_mZgB.js +206 -0
- package/web/dist/assets/chunk-XXDRQBXY-htV9FBqi.js +1 -0
- package/web/dist/assets/classDiagram-ZZMXUADV-DYTBrAEP.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-DYTBrAEP.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-Cop-ihyx.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-IEk8qBRq.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-Bmzt6Qb5.js +62 -0
- package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
- package/web/dist/assets/dagre-GXQ25YYZ-DiXTJYYU.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BT7CLuNk.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-BZZ3nTCO.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-Dal8LeZh.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-D6V9FfNA.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-KUAIcFiD.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-s0FEPZrV.js +1 -0
- package/web/dist/assets/erDiagram-RLTQ6QDP-CiIQPFCD.js +99 -0
- package/web/dist/assets/flowDiagram-HODETNUW-CvG4MYWn.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-CUf115ii.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-o3K1r6fL.js +106 -0
- package/web/dist/assets/index-C3vDFSEV.css +10 -0
- package/web/dist/assets/index-C5or_GAq.js +276 -0
- package/web/dist/assets/infoDiagram-27XIBGKW-BxiBeQ8E.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-B1ONuCkH.js +70 -0
- package/web/dist/assets/journeyDiagram-3NMN7TZE-DwmuoXcX.js +139 -0
- package/web/dist/assets/kanban-definition-UXKFOSKX-pMKHubxH.js +89 -0
- package/web/dist/assets/katex-C5jXJg4s.js +257 -0
- package/web/dist/assets/layout-CEwfjQR0.js +1 -0
- package/web/dist/assets/linear-D22Gdbt1.js +1 -0
- package/web/dist/assets/markdown-BhJ_LInD.js +51 -0
- package/web/dist/assets/mermaid.core-CO2nrD04.js +312 -0
- package/web/dist/assets/mindmap-definition-YA3MSWOX-9MQn9ZAp.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-Dy6fLjM-.js +1 -0
- package/web/dist/assets/pieDiagram-E7YTZNPT-EDLez6nA.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-Cgcd9mvy.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-B4Td0tjj.js +1 -0
- package/web/dist/assets/react-BddUxV-9.js +24 -0
- package/web/dist/assets/requirementDiagram-BXWQKSXE-aF2QNZ78.js +84 -0
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-DgTYwYMa.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-EUcddWUc.js +162 -0
- package/web/dist/assets/sizeCapture-INFHLROL-CXHOv9Sx.js +1 -0
- package/web/dist/assets/stateDiagram-D77RDMKH-B-xlgfaK.js +1 -0
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-kwAwAlWX.js +1 -0
- package/web/dist/assets/swimlanes-42K2YHIH-BtFBOCDh.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-D5j5Cj3S.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CZlQY6pb.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-Cn61dJvL.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-fiMNrWpr.js +78 -0
- package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
- package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-CuzM_sof.js +7 -0
- package/web/dist/brand-mark.svg +5 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.svg +5 -0
- package/web/dist/fonts/WIKI-FONTS.md +5 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/LICENSE +93 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/dist/icon/1a-mark.svg +1 -0
- package/web/dist/icon.ico +0 -0
- package/web/dist/index.html +18 -0
- package/web/public/apple-touch-icon.png +0 -0
- package/web/public/brand-mark.svg +5 -0
- package/web/public/favicon-32.png +0 -0
- package/web/public/favicon.svg +5 -0
- package/web/public/fonts/WIKI-FONTS.md +5 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/public/fonts/source-serif-4/LICENSE +93 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/public/icon/1a-mark.svg +1 -0
- package/web/public/icon.ico +0 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
/** Built-in SSH node workbench. All remote operations are scoped by node and client. */
|
|
2
|
+
import { NodeAgent, nodeAgentCommand } from "./node-agent.js";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
6
|
+
import { join, dirname, resolve } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { Client } from "ssh2";
|
|
9
|
+
import { PluginSecrets } from "./plugin-facilities.js";
|
|
10
|
+
import { scanSource, detectSources, sourcePath, } from "./node-sources.js";
|
|
11
|
+
function unsupportedOptions(node) {
|
|
12
|
+
return (node.unsupported ?? []).filter((reason) => reason !== "key" || ((node.localAuth ?? node.auth) === "key" && !node.localKeyPath));
|
|
13
|
+
}
|
|
14
|
+
const MAX_OUTPUT = 64 * 1024;
|
|
15
|
+
const MAX_FILE = 512 * 1024;
|
|
16
|
+
const identity = (clientId, nodeId) => JSON.stringify([clientId, nodeId]);
|
|
17
|
+
const fail = (message) => { throw new Error(message); };
|
|
18
|
+
const field = (value, name, max = 256) => {
|
|
19
|
+
if (typeof value !== "string" || !value.trim() || value.length > max)
|
|
20
|
+
throw new Error(`${name} 无效`);
|
|
21
|
+
return value.trim();
|
|
22
|
+
};
|
|
23
|
+
const safePath = (value) => {
|
|
24
|
+
const path = field(value, "路径", 4096);
|
|
25
|
+
if (!path.startsWith("/") || path.includes("\0"))
|
|
26
|
+
throw new Error("需要远端绝对路径");
|
|
27
|
+
return path;
|
|
28
|
+
};
|
|
29
|
+
const tail = (value) => value.length > MAX_OUTPUT ? `[输出已截断]\n${value.slice(-MAX_OUTPUT)}` : value;
|
|
30
|
+
export class NodeWorkbench {
|
|
31
|
+
dataDir;
|
|
32
|
+
file;
|
|
33
|
+
secrets;
|
|
34
|
+
nodes = [];
|
|
35
|
+
sources = [];
|
|
36
|
+
syncing = new Set();
|
|
37
|
+
timer;
|
|
38
|
+
disposed = false;
|
|
39
|
+
transientSecrets = new Map();
|
|
40
|
+
observedKeys = new Map();
|
|
41
|
+
sinks = new Map();
|
|
42
|
+
connections = new Map();
|
|
43
|
+
constructor(dataDir) {
|
|
44
|
+
this.dataDir = dataDir;
|
|
45
|
+
this.file = join(dataDir, "nodes.json");
|
|
46
|
+
this.secrets = new PluginSecrets(dataDir, join(dataDir, "node-workbench"));
|
|
47
|
+
try {
|
|
48
|
+
const saved = JSON.parse(readFileSync(this.file, "utf8"));
|
|
49
|
+
this.nodes = saved.nodes ?? [];
|
|
50
|
+
this.sources = saved.sources ?? [];
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
this.nodes = [];
|
|
54
|
+
}
|
|
55
|
+
this.nodes = this.nodes.filter((node) => node && /^[0-9a-f-]{36}$/i.test(node.id) && typeof node.host === "string");
|
|
56
|
+
this.timer = setInterval(() => { for (const source of this.sources)
|
|
57
|
+
if (source.enabled)
|
|
58
|
+
void this.syncSource(source); }, 5000);
|
|
59
|
+
this.timer.unref();
|
|
60
|
+
}
|
|
61
|
+
save() {
|
|
62
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
63
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
64
|
+
writeFileSync(tmp, JSON.stringify({ nodes: this.nodes, sources: this.sources }, null, 2), { mode: 0o600 });
|
|
65
|
+
renameSync(tmp, this.file);
|
|
66
|
+
}
|
|
67
|
+
emit(clientId, event, data = {}, ids = {}) {
|
|
68
|
+
const msg = { type: "node_event", event, data, nodeId: ids.nodeId, terminalId: ids.terminalId, conversationId: ids.conversationId, requestId: ids.requestId };
|
|
69
|
+
for (const sink of this.sinks.get(clientId) ?? [])
|
|
70
|
+
sink(msg);
|
|
71
|
+
}
|
|
72
|
+
activeAgents() { return [...this.connections.values()].filter(connection => connection.agentStarting || connection.agent?.busy).length; }
|
|
73
|
+
state(clientId) {
|
|
74
|
+
return { sources: this.sources, nodes: this.nodes.map((node) => ({ ...node, auth: node.localAuth ?? node.auth, unsupported: unsupportedOptions(node), hasSecret: this.secrets.has(`node:${node.id}:secret`) || this.transientSecrets.has(identity(clientId, node.id)), fingerprint: node.fingerprint })), connections: [...this.connections.values()].filter((c) => c.clientId === clientId).map((c) => ({ nodeId: c.nodeId, connectedAt: c.connectedAt, latencyMs: c.connectedAt ? c.connectedAt - c.startedAt : undefined, status: c.ready ? "connected" : "connecting", agent: c.agent?.snapshot(), terminals: [...c.terminals.values()].map((t) => ({ id: t.id, conversationId: t.conversationId, busy: t.busy, output: t.buffer })) })) };
|
|
75
|
+
}
|
|
76
|
+
attach(clientId, sink) {
|
|
77
|
+
let set = this.sinks.get(clientId);
|
|
78
|
+
if (!set)
|
|
79
|
+
this.sinks.set(clientId, set = new Set());
|
|
80
|
+
set.add(sink);
|
|
81
|
+
this.emit(clientId, "state", this.state(clientId));
|
|
82
|
+
return () => { set?.delete(sink); if (!set?.size)
|
|
83
|
+
this.sinks.delete(clientId); };
|
|
84
|
+
}
|
|
85
|
+
node(id) { return this.nodes.find((n) => n.id === id) ?? fail("节点不存在"); }
|
|
86
|
+
connection(clientId, nodeId) {
|
|
87
|
+
const c = this.connections.get(identity(clientId, nodeId ?? ""));
|
|
88
|
+
if (!c?.ready)
|
|
89
|
+
throw new Error("节点未连接");
|
|
90
|
+
return c;
|
|
91
|
+
}
|
|
92
|
+
terminal(clientId, req) {
|
|
93
|
+
const t = this.connection(clientId, req.nodeId).terminals.get(req.terminalId ?? "");
|
|
94
|
+
if (!t || t.conversationId !== req.conversationId)
|
|
95
|
+
throw new Error("终端身份已过期");
|
|
96
|
+
return t;
|
|
97
|
+
}
|
|
98
|
+
agent(clientId, req) {
|
|
99
|
+
const agent = this.connection(clientId, req.nodeId).agent;
|
|
100
|
+
if (!agent || agent.id !== req.conversationId)
|
|
101
|
+
throw new Error("远端 Agent 会话身份已过期");
|
|
102
|
+
return agent;
|
|
103
|
+
}
|
|
104
|
+
async startAgent(clientId, req) {
|
|
105
|
+
const connection = this.connection(clientId, req.nodeId);
|
|
106
|
+
if (connection.agentStarting)
|
|
107
|
+
throw new Error("远端 Agent 正在启动");
|
|
108
|
+
if (connection.agent && connection.agent.snapshot().phase !== "closed")
|
|
109
|
+
return connection.agent.snapshot();
|
|
110
|
+
const cwd = safePath(req.payload?.cwd ?? this.node(req.nodeId).defaultDir);
|
|
111
|
+
connection.agentStarting = true;
|
|
112
|
+
try {
|
|
113
|
+
const stream = await new Promise((resolve, reject) => connection.client.exec(nodeAgentCommand(cwd), (error, stream) => error ? reject(error) : resolve(stream)));
|
|
114
|
+
try {
|
|
115
|
+
this.ensureCurrent(connection);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
stream.signal("TERM");
|
|
119
|
+
stream.end();
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
const agent = new NodeAgent(stream, cwd, state => {
|
|
123
|
+
if (connection.agent?.id === state.id && this.connections.get(identity(clientId, connection.nodeId)) === connection)
|
|
124
|
+
this.emit(clientId, "agent_state", { agent: state }, { nodeId: connection.nodeId, conversationId: state.id });
|
|
125
|
+
});
|
|
126
|
+
connection.agent = agent;
|
|
127
|
+
this.broadcastState();
|
|
128
|
+
await agent.start();
|
|
129
|
+
return agent.snapshot();
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
connection.agentStarting = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async handle(clientId, req) {
|
|
136
|
+
try {
|
|
137
|
+
const p = req.payload ?? {};
|
|
138
|
+
let result = {};
|
|
139
|
+
switch (req.action) {
|
|
140
|
+
case "state":
|
|
141
|
+
this.emit(clientId, "state", this.state(clientId));
|
|
142
|
+
break;
|
|
143
|
+
case "source_detect":
|
|
144
|
+
result = { sources: await detectSources() };
|
|
145
|
+
break;
|
|
146
|
+
case "source_add": {
|
|
147
|
+
const path = sourcePath(field(p.path, "路径", 4096)), kind = p.kind === "xshell" ? "xshell" : "ssh";
|
|
148
|
+
let source = this.sources.find((s) => s.path === path && s.kind === kind);
|
|
149
|
+
if (!source) {
|
|
150
|
+
if (this.sources.length >= 10)
|
|
151
|
+
throw new Error("最多 10 个配置来源");
|
|
152
|
+
source = { id: randomUUID(), kind, path, enabled: p.enabled !== false, count: 0, groups: 0 };
|
|
153
|
+
this.sources.push(source);
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
source.enabled = p.enabled !== false;
|
|
157
|
+
await this.syncSource(source);
|
|
158
|
+
if (source.error)
|
|
159
|
+
throw new Error(source.error);
|
|
160
|
+
result = { sourceId: source.id };
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "source_reveal": {
|
|
164
|
+
const source = this.sources.find((s) => s.id === p.id);
|
|
165
|
+
if (!source)
|
|
166
|
+
throw new Error("来源不存在");
|
|
167
|
+
const program = process.platform === "win32" ? "explorer.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
168
|
+
const args = process.platform === "darwin" ? ["-R", source.path] : [source.kind === "ssh" ? dirname(source.path) : source.path];
|
|
169
|
+
await new Promise((ok, reject) => execFile(program, args, { timeout: 10000 }, (error) => error ? reject(error) : ok()));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "source_sync":
|
|
173
|
+
case "source_toggle": {
|
|
174
|
+
const source = this.sources.find((s) => s.id === p.id);
|
|
175
|
+
if (!source)
|
|
176
|
+
throw new Error("来源不存在");
|
|
177
|
+
if (req.action === "source_toggle") {
|
|
178
|
+
source.enabled = Boolean(p.enabled);
|
|
179
|
+
this.save();
|
|
180
|
+
this.broadcastState();
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
await this.syncSource(source);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "credential_test": {
|
|
187
|
+
const node = this.node(req.nodeId);
|
|
188
|
+
if (p.auth !== undefined && p.auth !== "password" && p.auth !== "key")
|
|
189
|
+
throw new Error("认证方式无效");
|
|
190
|
+
const auth = (p.auth ?? node.localAuth ?? node.auth);
|
|
191
|
+
if (typeof p.secret !== "string" || (auth === "password" && !p.secret) || p.secret.length > 10000)
|
|
192
|
+
throw new Error("凭据无效");
|
|
193
|
+
const secret = p.secret;
|
|
194
|
+
const originalAuth = node.localAuth;
|
|
195
|
+
const candidate = { ...node, localAuth: auth };
|
|
196
|
+
if (auth === "key" && typeof p.keyPath === "string")
|
|
197
|
+
candidate.localKeyPath = field(p.keyPath, "私钥路径", 4096);
|
|
198
|
+
if (p.fingerprint) {
|
|
199
|
+
if (p.fingerprint !== this.observedKeys.get(identity(clientId, node.id)))
|
|
200
|
+
throw new Error("主机指纹已过期");
|
|
201
|
+
node.fingerprint = String(p.fingerprint);
|
|
202
|
+
}
|
|
203
|
+
candidate.fingerprint = node.fingerprint;
|
|
204
|
+
await this.connect(clientId, candidate, secret, req);
|
|
205
|
+
if (this.node(node.id) !== node || node.host !== candidate.host || node.port !== candidate.port || node.username !== candidate.username || node.keyPath !== candidate.keyPath || node.auth !== candidate.auth || node.localAuth !== originalAuth || node.sourceMissing) {
|
|
206
|
+
this.disconnect(clientId, node.id);
|
|
207
|
+
throw new Error("节点配置已变化,请重试");
|
|
208
|
+
}
|
|
209
|
+
if (auth !== (node.localAuth ?? node.auth)) {
|
|
210
|
+
for (const c of [...this.connections.values()])
|
|
211
|
+
if (c.nodeId === node.id && c.clientId !== clientId)
|
|
212
|
+
this.drop(c);
|
|
213
|
+
this.secrets.delete(`node:${node.id}:secret`);
|
|
214
|
+
for (const key of this.transientSecrets.keys())
|
|
215
|
+
if (JSON.parse(key)[1] === node.id)
|
|
216
|
+
this.transientSecrets.delete(key);
|
|
217
|
+
}
|
|
218
|
+
node.localAuth = auth;
|
|
219
|
+
if (auth === "key" && candidate.localKeyPath)
|
|
220
|
+
node.localKeyPath = candidate.localKeyPath;
|
|
221
|
+
else if (auth !== "key")
|
|
222
|
+
delete node.localKeyPath;
|
|
223
|
+
node.lastConnected = candidate.lastConnected;
|
|
224
|
+
const targets = p.sameGroup && auth === "password" ? this.nodes.filter((n) => n.group === node.group && (n.localAuth ?? n.auth) === auth && !n.sourceMissing && !unsupportedOptions(n).length) : [node];
|
|
225
|
+
for (const target of targets) {
|
|
226
|
+
if (p.persist !== false) {
|
|
227
|
+
this.secrets.set(`node:${target.id}:secret`, secret);
|
|
228
|
+
this.transientSecrets.delete(identity(clientId, target.id));
|
|
229
|
+
}
|
|
230
|
+
else
|
|
231
|
+
this.transientSecrets.set(identity(clientId, target.id), secret);
|
|
232
|
+
}
|
|
233
|
+
this.save();
|
|
234
|
+
this.broadcastState();
|
|
235
|
+
result = { count: targets.length };
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case "save": {
|
|
239
|
+
const id = typeof p.id === "string" && p.id ? p.id : randomUUID();
|
|
240
|
+
const old = this.nodes.find((n) => n.id === id);
|
|
241
|
+
if (p.id && !old)
|
|
242
|
+
throw new Error("节点不存在");
|
|
243
|
+
if (old?.sourceId)
|
|
244
|
+
throw new Error("来源节点请在原工具中编辑");
|
|
245
|
+
const node = {
|
|
246
|
+
id, group: typeof p.group === "string" ? p.group.slice(0, 80) : "默认", name: field(p.name, "名称"),
|
|
247
|
+
host: field(p.host, "地址"), port: Number(p.port ?? 22), username: field(p.username, "用户名"),
|
|
248
|
+
auth: p.auth === "key" || p.auth === "agent" ? p.auth : "password",
|
|
249
|
+
keyPath: typeof p.keyPath === "string" ? p.keyPath.slice(0, 4096) : undefined,
|
|
250
|
+
defaultDir: typeof p.defaultDir === "string" && p.defaultDir.startsWith("/") && !/[\r\n\0]/.test(p.defaultDir) ? p.defaultDir : "/",
|
|
251
|
+
fingerprint: old?.host === p.host && old?.port === Number(p.port ?? 22) ? old?.fingerprint : undefined,
|
|
252
|
+
lastConnected: old?.lastConnected,
|
|
253
|
+
};
|
|
254
|
+
if (!Number.isInteger(node.port) || node.port < 1 || node.port > 65535)
|
|
255
|
+
throw new Error("端口无效");
|
|
256
|
+
const priorSecret = old?.auth === node.auth ? this.secrets.get(`node:${id}:secret`) : undefined;
|
|
257
|
+
const usableSecret = p.secret === null ? undefined : typeof p.secret === "string" && p.secret ? p.secret : priorSecret;
|
|
258
|
+
if (node.auth === "password" && !usableSecret)
|
|
259
|
+
throw new Error("请填写 SSH 密码");
|
|
260
|
+
if (node.auth === "key" && !node.keyPath)
|
|
261
|
+
throw new Error("请填写本机私钥路径");
|
|
262
|
+
if (p.secret === null || (old && old.auth !== node.auth) || node.auth === "agent")
|
|
263
|
+
this.secrets.delete(`node:${id}:secret`);
|
|
264
|
+
if (node.auth !== "agent" && typeof p.secret === "string" && p.secret)
|
|
265
|
+
this.secrets.set(`node:${id}:secret`, p.secret);
|
|
266
|
+
if (p.secret !== undefined || old?.auth !== node.auth)
|
|
267
|
+
for (const key of this.transientSecrets.keys())
|
|
268
|
+
if (JSON.parse(key)[1] === id)
|
|
269
|
+
this.transientSecrets.delete(key);
|
|
270
|
+
if (old && (old.host !== node.host || old.port !== node.port || old.username !== node.username || old.auth !== node.auth || old.keyPath !== node.keyPath || p.secret)) {
|
|
271
|
+
for (const c of [...this.connections.values()])
|
|
272
|
+
if (c.nodeId === id)
|
|
273
|
+
this.drop(c);
|
|
274
|
+
}
|
|
275
|
+
this.nodes = old ? this.nodes.map((n) => n.id === id ? node : n) : [...this.nodes, node];
|
|
276
|
+
this.save();
|
|
277
|
+
result = { id };
|
|
278
|
+
this.broadcastState();
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "delete": {
|
|
282
|
+
const node = this.node(req.nodeId);
|
|
283
|
+
for (const c of [...this.connections.values()])
|
|
284
|
+
if (c.nodeId === node.id)
|
|
285
|
+
this.drop(c);
|
|
286
|
+
this.nodes = this.nodes.filter((n) => n.id !== node.id);
|
|
287
|
+
this.secrets.delete(`node:${node.id}:secret`);
|
|
288
|
+
for (const key of this.transientSecrets.keys())
|
|
289
|
+
if (JSON.parse(key)[1] === node.id)
|
|
290
|
+
this.transientSecrets.delete(key);
|
|
291
|
+
this.save();
|
|
292
|
+
this.broadcastState();
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case "forget_host_key": {
|
|
296
|
+
const node = this.node(req.nodeId);
|
|
297
|
+
for (const c of [...this.connections.values()])
|
|
298
|
+
if (c.nodeId === node.id)
|
|
299
|
+
this.drop(c);
|
|
300
|
+
delete node.fingerprint;
|
|
301
|
+
this.save();
|
|
302
|
+
this.broadcastState();
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case "import_legacy":
|
|
306
|
+
result = this.importLegacy();
|
|
307
|
+
this.broadcastState();
|
|
308
|
+
break;
|
|
309
|
+
case "import": {
|
|
310
|
+
const input = p.nodes;
|
|
311
|
+
if (!Array.isArray(input) || input.length > 200)
|
|
312
|
+
throw new Error("节点文件无效或超过 200 台");
|
|
313
|
+
let count = 0;
|
|
314
|
+
for (const item of input) {
|
|
315
|
+
if (!item || typeof item !== "object")
|
|
316
|
+
continue;
|
|
317
|
+
const h = item;
|
|
318
|
+
const host = field(h.host, "地址"), username = field(h.username, "用户名"), port = Number(h.port);
|
|
319
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
320
|
+
continue;
|
|
321
|
+
if (this.nodes.some((n) => n.host === host && n.port === port && n.username === username))
|
|
322
|
+
continue;
|
|
323
|
+
this.nodes.push({ id: randomUUID(), group: typeof h.group === "string" ? h.group.slice(0, 80) : "导入", name: field(h.name, "名称"), host, port, username, auth: h.auth === "key" || h.auth === "agent" ? h.auth : "password", keyPath: typeof h.keyPath === "string" ? h.keyPath.slice(0, 4096) : undefined, defaultDir: typeof h.defaultDir === "string" && h.defaultDir.startsWith("/") && !/[\r\n\0]/.test(h.defaultDir) ? h.defaultDir : "/" });
|
|
324
|
+
count++;
|
|
325
|
+
}
|
|
326
|
+
this.save();
|
|
327
|
+
this.broadcastState();
|
|
328
|
+
result = { count };
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case "export":
|
|
332
|
+
result = { nodes: this.nodes.map(({ fingerprint, ...n }) => n) };
|
|
333
|
+
break;
|
|
334
|
+
case "connect":
|
|
335
|
+
await this.connect(clientId, this.node(req.nodeId), undefined, req);
|
|
336
|
+
break;
|
|
337
|
+
case "trust": {
|
|
338
|
+
const node = this.node(req.nodeId);
|
|
339
|
+
const fp = field(p.fingerprint, "指纹");
|
|
340
|
+
if (node.fingerprint && node.fingerprint !== fp)
|
|
341
|
+
throw new Error("主机密钥已变化;请重新核实节点身份");
|
|
342
|
+
node.fingerprint = fp;
|
|
343
|
+
this.save();
|
|
344
|
+
await this.connect(clientId, node, undefined, req);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "disconnect":
|
|
348
|
+
this.disconnect(clientId, req.nodeId);
|
|
349
|
+
break;
|
|
350
|
+
case "agent_start":
|
|
351
|
+
result = { agent: await this.startAgent(clientId, req) };
|
|
352
|
+
break;
|
|
353
|
+
case "agent_prompt":
|
|
354
|
+
await this.agent(clientId, req).prompt(field(p.text, "消息", 200000), p.queue === "steer" || p.queue === "followUp" ? p.queue : undefined);
|
|
355
|
+
break;
|
|
356
|
+
case "agent_abort":
|
|
357
|
+
await this.agent(clientId, req).abort();
|
|
358
|
+
break;
|
|
359
|
+
case "agent_new":
|
|
360
|
+
await this.agent(clientId, req).newSession();
|
|
361
|
+
break;
|
|
362
|
+
case "agent_model":
|
|
363
|
+
await this.agent(clientId, req).model(field(p.provider, "服务商"), field(p.modelId, "模型"));
|
|
364
|
+
break;
|
|
365
|
+
case "agent_close":
|
|
366
|
+
this.agent(clientId, req).close();
|
|
367
|
+
break;
|
|
368
|
+
case "agent_dialog":
|
|
369
|
+
this.agent(clientId, req).dialog(field(p.id, "对话框"), { cancelled: p.cancelled === true, confirmed: p.confirmed === true, value: typeof p.value === "string" ? p.value : undefined });
|
|
370
|
+
break;
|
|
371
|
+
case "terminal_open":
|
|
372
|
+
result = await this.openTerminal(clientId, req);
|
|
373
|
+
break;
|
|
374
|
+
case "terminal_input": {
|
|
375
|
+
const t = this.terminal(clientId, req);
|
|
376
|
+
if (t.busy)
|
|
377
|
+
throw new Error("Agent 正在使用该终端");
|
|
378
|
+
t.stream.write(String(p.data ?? "").slice(0, 8192));
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case "terminal_resize":
|
|
382
|
+
this.terminal(clientId, req).stream.setWindow(Math.max(1, Number(p.rows) || 24), Math.max(1, Number(p.cols) || 80), 0, 0);
|
|
383
|
+
break;
|
|
384
|
+
case "terminal_close":
|
|
385
|
+
this.terminal(clientId, req).stream.end();
|
|
386
|
+
break;
|
|
387
|
+
case "interrupt": {
|
|
388
|
+
const t = this.terminal(clientId, req);
|
|
389
|
+
t.stream.write("\x03");
|
|
390
|
+
if (t.pending) {
|
|
391
|
+
const pending = t.pending;
|
|
392
|
+
t.pending = undefined;
|
|
393
|
+
pending.reject(new Error("命令已中断"));
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "list": {
|
|
398
|
+
const c = this.fileScope(clientId, req), path = safePath(p.path);
|
|
399
|
+
const entries = await this.list(c, path);
|
|
400
|
+
this.ensureCurrent(c);
|
|
401
|
+
result = { path, entries };
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "read": {
|
|
405
|
+
const c = this.fileScope(clientId, req), path = safePath(p.path);
|
|
406
|
+
const text = await this.read(c, path);
|
|
407
|
+
this.ensureCurrent(c);
|
|
408
|
+
result = { path, text };
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "write": {
|
|
412
|
+
const c = this.fileScope(clientId, req), path = safePath(p.path);
|
|
413
|
+
await this.write(c, path, String(p.text ?? ""));
|
|
414
|
+
this.ensureCurrent(c);
|
|
415
|
+
result = { path };
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
default: throw new Error("未知节点操作");
|
|
419
|
+
}
|
|
420
|
+
this.emit(clientId, "result", { action: req.action, ...result }, req);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
this.emit(clientId, "failure", { action: req.action, message: error.message }, req);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/** In-flight scans are serialized per source. Failures leave the previous snapshot intact. */
|
|
427
|
+
async syncSource(source) {
|
|
428
|
+
if (this.disposed || this.syncing.has(source.id))
|
|
429
|
+
return;
|
|
430
|
+
this.syncing.add(source.id);
|
|
431
|
+
try {
|
|
432
|
+
const scanned = await scanSource(source);
|
|
433
|
+
if (this.disposed)
|
|
434
|
+
return;
|
|
435
|
+
const changes = [];
|
|
436
|
+
for (const incoming of scanned) {
|
|
437
|
+
const old = this.nodes.find((n) => n.sourceId === source.id && n.sourceKey === incoming.sourceKey);
|
|
438
|
+
if (!old) {
|
|
439
|
+
this.nodes.push({ ...incoming, id: randomUUID(), sourceId: source.id, });
|
|
440
|
+
changes.push(`+ ${incoming.name}`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const changed = Object.keys(incoming).some((k) => JSON.stringify(old[k]) !== JSON.stringify(incoming[k])) || old.sourceMissing;
|
|
444
|
+
if (!changed)
|
|
445
|
+
continue;
|
|
446
|
+
const endpoint = old.host !== incoming.host || old.port !== incoming.port;
|
|
447
|
+
const credentials = old.username !== incoming.username || old.auth !== incoming.auth || old.keyPath !== incoming.keyPath;
|
|
448
|
+
for (const c of [...this.connections.values()])
|
|
449
|
+
if (c.nodeId === old.id)
|
|
450
|
+
this.drop(c);
|
|
451
|
+
if (endpoint)
|
|
452
|
+
delete old.fingerprint;
|
|
453
|
+
if (old.host !== incoming.host || credentials) {
|
|
454
|
+
delete old.localKeyPath;
|
|
455
|
+
delete old.localAuth;
|
|
456
|
+
this.secrets.delete(`node:${old.id}:secret`);
|
|
457
|
+
for (const key of this.transientSecrets.keys())
|
|
458
|
+
if (JSON.parse(key)[1] === old.id)
|
|
459
|
+
this.transientSecrets.delete(key);
|
|
460
|
+
}
|
|
461
|
+
changes.push(`${old.name}: ${old.host}:${old.port} → ${incoming.host}:${incoming.port}`);
|
|
462
|
+
Object.assign(old, incoming, { sourceMissing: false });
|
|
463
|
+
}
|
|
464
|
+
for (const old of this.nodes.filter((n) => n.sourceId === source.id))
|
|
465
|
+
if (!scanned.some((n) => n.sourceKey === old.sourceKey) && !old.sourceMissing) {
|
|
466
|
+
old.sourceMissing = true;
|
|
467
|
+
changes.push(`− ${old.name}`);
|
|
468
|
+
for (const c of [...this.connections.values()])
|
|
469
|
+
if (c.nodeId === old.id)
|
|
470
|
+
this.drop(c);
|
|
471
|
+
}
|
|
472
|
+
source.count = scanned.length;
|
|
473
|
+
source.groups = new Set(scanned.map((n) => n.group)).size;
|
|
474
|
+
source.lastSync = Date.now();
|
|
475
|
+
source.error = undefined;
|
|
476
|
+
if (changes.length)
|
|
477
|
+
source.changes = changes.slice(-20);
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
source.error = e.message;
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
this.syncing.delete(source.id);
|
|
484
|
+
if (!this.disposed) {
|
|
485
|
+
this.save();
|
|
486
|
+
this.broadcastState();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
dispose() {
|
|
491
|
+
this.disposed = true;
|
|
492
|
+
clearInterval(this.timer);
|
|
493
|
+
for (const c of [...this.connections.values()])
|
|
494
|
+
this.drop(c);
|
|
495
|
+
this.transientSecrets.clear();
|
|
496
|
+
}
|
|
497
|
+
broadcastState() { for (const id of this.sinks.keys())
|
|
498
|
+
this.emit(id, "state", this.state(id)); }
|
|
499
|
+
fileScope(clientId, req) {
|
|
500
|
+
const c = this.connection(clientId, req.nodeId);
|
|
501
|
+
if (req.conversationId && req.conversationId !== `node:${c.nodeId}`)
|
|
502
|
+
throw new Error("对话身份已过期");
|
|
503
|
+
if (req.terminalId)
|
|
504
|
+
this.terminal(clientId, req);
|
|
505
|
+
return c;
|
|
506
|
+
}
|
|
507
|
+
ensureCurrent(c) {
|
|
508
|
+
if (this.connections.get(identity(c.clientId, c.nodeId)) !== c || !c.ready)
|
|
509
|
+
throw new Error("连接已变化");
|
|
510
|
+
}
|
|
511
|
+
importLegacy() {
|
|
512
|
+
let count = 0;
|
|
513
|
+
for (const path of [join(this.dataDir, "plugins", "vscode-editor", "ssh-hosts.json"), join(this.dataDir, "plugins", "ssh", "ssh-hosts.json")]) {
|
|
514
|
+
try {
|
|
515
|
+
const hosts = JSON.parse(readFileSync(path, "utf8")).hosts;
|
|
516
|
+
if (!Array.isArray(hosts))
|
|
517
|
+
continue;
|
|
518
|
+
for (const h of hosts) {
|
|
519
|
+
if (!h.host || this.nodes.some((n) => n.host === h.host && n.port === (Number(h.port) || 22) && n.username === h.username))
|
|
520
|
+
continue;
|
|
521
|
+
this.nodes.push({ id: randomUUID(), group: "导入", name: String(h.name || h.host), host: String(h.host), port: Number(h.port) || 22, username: String(h.username || "root"), auth: h.privateKey ? "key" : "password", defaultDir: "/" });
|
|
522
|
+
count++;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch { /* missing legacy config */ }
|
|
526
|
+
}
|
|
527
|
+
if (count)
|
|
528
|
+
this.save();
|
|
529
|
+
return { count };
|
|
530
|
+
}
|
|
531
|
+
async connect(clientId, node, overrideSecret, req) {
|
|
532
|
+
const auth = node.localAuth ?? node.auth;
|
|
533
|
+
if (node.sourceMissing || unsupportedOptions(node).length)
|
|
534
|
+
throw new Error("来源已移除或配置包含暂不支持的选项,请检查节点详情");
|
|
535
|
+
const secret = overrideSecret ?? this.transientSecrets.get(identity(clientId, node.id)) ?? this.secrets.get(`node:${node.id}:secret`);
|
|
536
|
+
let keyChanged = false;
|
|
537
|
+
const opts = { host: node.host, port: node.port, username: node.username, readyTimeout: 15000, keepaliveInterval: 10000,
|
|
538
|
+
hostHash: "sha256", hostVerifier: (hash) => {
|
|
539
|
+
const fp = `SHA256:${hash}`;
|
|
540
|
+
this.observedKeys.set(identity(clientId, node.id), fp);
|
|
541
|
+
if (!node.fingerprint) {
|
|
542
|
+
this.emit(clientId, "trust_required", { fingerprint: fp, name: node.name, host: node.host }, { ...req, nodeId: node.id });
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
if (node.fingerprint !== fp)
|
|
546
|
+
keyChanged = true;
|
|
547
|
+
return node.fingerprint === fp;
|
|
548
|
+
} };
|
|
549
|
+
if (auth === "password")
|
|
550
|
+
opts.password = secret ?? "";
|
|
551
|
+
else if (auth === "agent") {
|
|
552
|
+
if (!process.env.SSH_AUTH_SOCK)
|
|
553
|
+
throw new Error("本机 SSH Agent 未就绪");
|
|
554
|
+
opts.agent = process.env.SSH_AUTH_SOCK;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
const keyPath = node.localKeyPath || node.keyPath;
|
|
558
|
+
const path = keyPath?.startsWith("~/") ? join(homedir(), keyPath.slice(2)) : keyPath;
|
|
559
|
+
if (!path)
|
|
560
|
+
throw new Error("需要私钥路径");
|
|
561
|
+
opts.privateKey = readFileSync(resolve(path));
|
|
562
|
+
if (secret)
|
|
563
|
+
opts.passphrase = secret;
|
|
564
|
+
}
|
|
565
|
+
this.disconnect(clientId, node.id);
|
|
566
|
+
const client = new Client();
|
|
567
|
+
const c = { client, clientId, nodeId: node.id, terminals: new Map(), ready: false, startedAt: Date.now() };
|
|
568
|
+
const key = identity(clientId, node.id);
|
|
569
|
+
this.connections.set(key, c);
|
|
570
|
+
this.broadcastState();
|
|
571
|
+
try {
|
|
572
|
+
await new Promise((ok, reject) => {
|
|
573
|
+
let settled = false;
|
|
574
|
+
// Interactive terminal: small keystroke packets must not wait for Nagle/delayed-ACK (~40ms each way).
|
|
575
|
+
client.once("ready", () => { client.setNoDelay(true); settled = true; ok(); });
|
|
576
|
+
client.on("error", (e) => { if (!settled) {
|
|
577
|
+
settled = true;
|
|
578
|
+
reject(e);
|
|
579
|
+
}
|
|
580
|
+
else
|
|
581
|
+
this.drop(c); });
|
|
582
|
+
client.on("close", () => { if (!settled) {
|
|
583
|
+
settled = true;
|
|
584
|
+
reject(new Error("SSH 连接已关闭"));
|
|
585
|
+
} this.drop(c); });
|
|
586
|
+
client.connect(opts);
|
|
587
|
+
});
|
|
588
|
+
if (this.connections.get(key) !== c)
|
|
589
|
+
throw new Error("连接已被新的请求替代");
|
|
590
|
+
c.ready = true;
|
|
591
|
+
c.connectedAt = Date.now();
|
|
592
|
+
node.lastConnected = c.connectedAt;
|
|
593
|
+
this.save();
|
|
594
|
+
this.broadcastState();
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
this.drop(c);
|
|
598
|
+
client.end();
|
|
599
|
+
if (keyChanged)
|
|
600
|
+
throw new Error("主机密钥已变化,连接已阻止;请核实节点身份");
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
drop(c) {
|
|
605
|
+
if (this.connections.get(identity(c.clientId, c.nodeId)) !== c)
|
|
606
|
+
return;
|
|
607
|
+
this.connections.delete(identity(c.clientId, c.nodeId));
|
|
608
|
+
for (const t of c.terminals.values()) {
|
|
609
|
+
t.pending?.reject(new Error("SSH 已断开"));
|
|
610
|
+
this.emit(c.clientId, "terminal_exit", {}, { nodeId: c.nodeId, terminalId: t.id, conversationId: t.conversationId });
|
|
611
|
+
}
|
|
612
|
+
c.agent?.close();
|
|
613
|
+
c.terminals.clear();
|
|
614
|
+
c.client.end();
|
|
615
|
+
this.broadcastState();
|
|
616
|
+
}
|
|
617
|
+
disconnect(clientId, nodeId) { const c = this.connections.get(identity(clientId, nodeId ?? "")); if (c)
|
|
618
|
+
this.drop(c); }
|
|
619
|
+
async openTerminal(clientId, req) {
|
|
620
|
+
const c = this.connection(clientId, req.nodeId);
|
|
621
|
+
const id = req.terminalId || randomUUID(), conversationId = `node:${c.nodeId}`;
|
|
622
|
+
if (c.terminals.has(id))
|
|
623
|
+
throw new Error("终端 ID 已存在");
|
|
624
|
+
const p = req.payload ?? {};
|
|
625
|
+
const stream = await new Promise((ok, reject) => c.client.shell({ cols: Number(p.cols) || 80, rows: Number(p.rows) || 24, term: "xterm-256color" }, (e, s) => e ? reject(e) : ok(s)));
|
|
626
|
+
try {
|
|
627
|
+
this.ensureCurrent(c);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
stream.end();
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
const t = { id, conversationId, stream, busy: false, buffer: "" };
|
|
634
|
+
c.terminals.set(id, t);
|
|
635
|
+
stream.on("data", (chunk) => {
|
|
636
|
+
const data = chunk.toString("utf8");
|
|
637
|
+
t.buffer = (t.buffer + data).slice(-MAX_OUTPUT);
|
|
638
|
+
if (t.pending) {
|
|
639
|
+
t.pending.output = tail(t.pending.output + data);
|
|
640
|
+
if (new RegExp(`(?:^|\\r?\\n)${t.pending.marker}(?::\\d+)?\\r?\\n`).test(t.pending.output)) {
|
|
641
|
+
const pending = t.pending;
|
|
642
|
+
t.pending = undefined;
|
|
643
|
+
clearTimeout(pending.timer);
|
|
644
|
+
pending.resolve(pending.output);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
this.emit(clientId, "terminal_output", { text: tail(data) }, { nodeId: c.nodeId, terminalId: id, conversationId });
|
|
648
|
+
});
|
|
649
|
+
stream.on("close", () => { t.pending?.reject(new Error("终端已断开")); c.terminals.delete(id); this.emit(clientId, "terminal_exit", {}, { nodeId: c.nodeId, terminalId: id, conversationId }); this.broadcastState(); });
|
|
650
|
+
const directory = this.node(c.nodeId).defaultDir;
|
|
651
|
+
if (directory !== "/")
|
|
652
|
+
stream.write(`cd '${directory.replaceAll("'", "'\\''")}'\n`);
|
|
653
|
+
this.broadcastState();
|
|
654
|
+
return { terminalId: id, conversationId };
|
|
655
|
+
}
|
|
656
|
+
async execute(clientId, nodeId, terminalId, command, signal) {
|
|
657
|
+
if (signal?.aborted)
|
|
658
|
+
throw new Error("命令已中断");
|
|
659
|
+
const c = this.connection(clientId, nodeId), t = c.terminals.get(terminalId);
|
|
660
|
+
if (!t)
|
|
661
|
+
throw new Error("请选择已连接的手动终端");
|
|
662
|
+
if (t.busy)
|
|
663
|
+
throw new Error("终端正忙");
|
|
664
|
+
t.busy = true;
|
|
665
|
+
this.emit(clientId, "terminal_busy", { busy: true }, { nodeId, terminalId, conversationId: t.conversationId });
|
|
666
|
+
try {
|
|
667
|
+
const marker = `__PI_READY_${randomUUID().replaceAll("-", "")}__`;
|
|
668
|
+
// Interrupt any foreground process, then wait for a shell-executed marker.
|
|
669
|
+
t.stream.write("\x03\n");
|
|
670
|
+
await this.waitMarker(t, marker, `printf '%s\\n' '${marker}'\n`, 5000, signal);
|
|
671
|
+
const end = `__PI_DONE_${randomUUID().replaceAll("-", "")}__`;
|
|
672
|
+
const output = await this.waitMarker(t, end, `${command}\nprintf '%s:%s\\n' '${end}' "$?"\n`, 120000, signal);
|
|
673
|
+
const code = Number(output.match(new RegExp(`${end}:(\\d+)`))?.[1] ?? 0);
|
|
674
|
+
return tail(output.replace(new RegExp(`${end}:\\d+`), "") + (code ? `\n[退出码 ${code}]` : ""));
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
t.busy = false;
|
|
678
|
+
this.emit(clientId, "terminal_busy", { busy: false }, { nodeId, terminalId, conversationId: t.conversationId });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
waitMarker(t, marker, input, ms, signal) {
|
|
682
|
+
if (signal?.aborted)
|
|
683
|
+
return Promise.reject(new Error("命令已中断"));
|
|
684
|
+
return new Promise((resolve, reject) => {
|
|
685
|
+
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); if (t.pending?.marker === marker)
|
|
686
|
+
t.pending = undefined; };
|
|
687
|
+
const abort = () => { t.stream.write("\x03"); cleanup(); reject(new Error("命令已中断")); };
|
|
688
|
+
const timer = setTimeout(() => { t.stream.write("\x03"); cleanup(); reject(new Error("无法确认 shell 提示符或命令超时")); }, ms);
|
|
689
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
690
|
+
t.pending = { marker, output: "", timer, resolve: (text) => { cleanup(); resolve(text); }, reject: (error) => { cleanup(); reject(error); } };
|
|
691
|
+
t.stream.write(input);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async sftp(c) {
|
|
695
|
+
this.ensureCurrent(c);
|
|
696
|
+
if (c.sftp)
|
|
697
|
+
return c.sftp;
|
|
698
|
+
c.sftp = await new Promise((ok, reject) => c.client.sftp((e, s) => e ? reject(e) : ok(s)));
|
|
699
|
+
this.ensureCurrent(c);
|
|
700
|
+
return c.sftp;
|
|
701
|
+
}
|
|
702
|
+
async list(c, path) {
|
|
703
|
+
const s = await this.sftp(c);
|
|
704
|
+
const entries = await new Promise((ok, reject) => s.readdir(path, (e, list) => e ? reject(e) : ok(list)));
|
|
705
|
+
return entries.map((e) => ({ name: e.filename, type: e.attrs.isDirectory() ? "dir" : "file", size: e.attrs.size }));
|
|
706
|
+
}
|
|
707
|
+
async read(c, path) {
|
|
708
|
+
const s = await this.sftp(c);
|
|
709
|
+
const st = await new Promise((ok, reject) => s.stat(path, (e, attrs) => e ? reject(e) : ok(attrs)));
|
|
710
|
+
if (st.size > MAX_FILE)
|
|
711
|
+
throw new Error("文件超过 512 KiB");
|
|
712
|
+
const chunks = [];
|
|
713
|
+
let size = 0;
|
|
714
|
+
for await (const chunk of s.createReadStream(path)) {
|
|
715
|
+
const b = Buffer.from(chunk);
|
|
716
|
+
size += b.length;
|
|
717
|
+
if (size > MAX_FILE)
|
|
718
|
+
throw new Error("文件超过 512 KiB");
|
|
719
|
+
chunks.push(b);
|
|
720
|
+
}
|
|
721
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
722
|
+
}
|
|
723
|
+
async write(c, path, text) {
|
|
724
|
+
if (Buffer.byteLength(text) > MAX_FILE)
|
|
725
|
+
throw new Error("文件超过 512 KiB");
|
|
726
|
+
const s = await this.sftp(c);
|
|
727
|
+
await new Promise((ok, reject) => { const stream = s.createWriteStream(path, { flags: "w" }); stream.once("error", reject); stream.once("close", ok); stream.end(text); });
|
|
728
|
+
}
|
|
729
|
+
}
|