termdock 1.4.295 → 1.4.297
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/.desktop-dist/main.js +35 -2
- package/README.en.md +257 -0
- package/README.md +284 -133
- package/dist/client/assets/App-BbqtNY2x.js +177 -0
- package/dist/client/assets/App-BbqtNY2x.js.br +0 -0
- package/dist/client/assets/App-BbqtNY2x.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-Dv5XRtRh.js → ChangeWalkthroughPanel-CvQJIp_C.js} +1 -1
- package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.gz +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js +1 -0
- package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-C01GXOEq.js +2 -0
- package/dist/client/assets/ComputerControlView-CW1uA133.js +4 -0
- package/dist/client/assets/ComputerControlView-CW1uA133.js.br +0 -0
- package/dist/client/assets/ComputerControlView-CW1uA133.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-mq69dKyr.js → DagPlayground-CdE3tQsl.js} +1 -1
- package/dist/client/assets/DagPlayground-CdE3tQsl.js.br +0 -0
- package/dist/client/assets/DagPlayground-CdE3tQsl.js.gz +0 -0
- package/dist/client/assets/{DiffLab-BqdHYGi5.js → DiffLab-BLGNnazd.js} +1 -1
- package/dist/client/assets/DiffLab-BLGNnazd.js.br +0 -0
- package/dist/client/assets/DiffLab-BLGNnazd.js.gz +0 -0
- package/dist/client/assets/{DiffReview-BvzGxpuc.js → DiffReview-CYw3ASI6.js} +1 -1
- package/dist/client/assets/DiffReview-CYw3ASI6.js.br +0 -0
- package/dist/client/assets/DiffReview-CYw3ASI6.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-DrfddB7x.js → DiffReviewLab-CySIuWq_.js} +1 -1
- package/dist/client/assets/DiffReviewLab-CySIuWq_.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-CySIuWq_.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-3bJV_A5s.js → DiffViewer-WsYvN75z.js} +13 -13
- package/dist/client/assets/DiffViewer-WsYvN75z.js.br +0 -0
- package/dist/client/assets/DiffViewer-WsYvN75z.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-CX7VtoJB.js → ModelPreview-Dnlx9ByX.js} +1 -1
- package/dist/client/assets/ModelPreview-Dnlx9ByX.js.br +0 -0
- package/dist/client/assets/ModelPreview-Dnlx9ByX.js.gz +0 -0
- package/dist/client/assets/RightSidebar-Cp5b_y0u.js +1678 -0
- package/dist/client/assets/RightSidebar-Cp5b_y0u.js.br +0 -0
- package/dist/client/assets/RightSidebar-Cp5b_y0u.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-PJwhLTPH.js → abnfDiagram-VCTEODGH-CK1mGFRQ.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.gz +0 -0
- package/dist/client/assets/app-DfACMixk.js +35 -0
- package/dist/client/assets/app-DfACMixk.js.br +0 -0
- package/dist/client/assets/app-DfACMixk.js.gz +0 -0
- package/dist/client/assets/{arc-l3FXjCEl.js → arc-yZE1gqDl.js} +1 -1
- package/dist/client/assets/arc-yZE1gqDl.js.br +0 -0
- package/dist/client/assets/arc-yZE1gqDl.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-DdNbKi-4.js → architectureDiagram-5GKGNRK7-U5XSC7cY.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-Cu63oaxR.js → blockDiagram-I7D4REHJ-pVJnntSG.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DQiV_yk6.js → c4Diagram-7LVT6UL2-Bwz0AJJV.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.gz +0 -0
- package/dist/client/assets/channel-qqjDJtcZ.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-COKJ79Ld.js → chunk-2Q5K7J3B-CXJ1kP4X.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-BfV-m7oa.js → chunk-5VM5RSS4-Dpdum_2z.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-sLDCEnU8.js → chunk-F27PBJKO-BCG5Zi_-.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-DYu5qS7g.js → chunk-IMKFNOWR-BM-GcNcv.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-DixkILcl.js → chunk-JWPE2WC7-BWiXpQT9.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-CfW5Vm_-.js → chunk-POPQ4Y6H-vQSslMNv.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-nJnbUFzs.js → chunk-SVP7TREG-Aqfz6nPA.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-BwyiSQgx.js → chunk-TICWLB2K-DDjekMd0.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-TCk4c2XN.js → chunk-XXDRQBXY-BegIBcFg.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-BDuBrfNz.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BDuBrfNz.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-rroo-EOb.js → cose-bilkent-JH36ORCC-B2izFsBY.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-CkFeV85k.js → cynefin-OW5HDTMX-C7HA99Zc.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-C7HA99Zc.js.br +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-CkFeV85k.js.gz → cynefin-OW5HDTMX-C7HA99Zc.js.gz} +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-hBwpU_bW.js → cynefinDiagram-5FMLGOSQ-vNIzcCYl.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-B321-hdp.js → dagre-GXQ25YYZ-Bv9WHNT9.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-bY87_ptA.js → diagram-S7CK7UJ4-DFg5UNic.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN--cez0H_o.js → diagram-UQ7AKVKN-BFQsCAs7.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-BupH_VRK.js → diagram-VSXAHHWV-C4R8TKEv.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-DewqaxvF.js → diagram-VX7I27RA-CL75HIf0.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-wDumcTuR.js → diagram-Z3DM3KII-CfJampuO.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-C0OY91We.js → ebnfDiagram-PWID7BFC-Ct53jhNd.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-Cw9aNqvJ.js → erDiagram-RLTQ6QDP-BgL2xOOa.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-BC7XUXeI.js → flowDiagram-HODETNUW-Cp4Nu5jY.js} +1 -1
- package/dist/client/assets/{flowDiagram-HODETNUW-BC7XUXeI.js.br → flowDiagram-HODETNUW-Cp4Nu5jY.js.br} +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-Cp4Nu5jY.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-DovhJIz5.js → ganttDiagram-EL5Y4UJY-DI0rdyPX.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-wjVok2Lg.js → gestureArbiter-Belsq4xq.js} +1 -1
- package/dist/client/assets/gestureArbiter-Belsq4xq.js.br +0 -0
- package/dist/client/assets/gestureArbiter-Belsq4xq.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-BmrbbyW1.js → gitGraphDiagram-WWUBYQGX-C0H4loQP.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.gz +0 -0
- package/dist/client/assets/index-CYmI1EsU.css +1 -0
- package/dist/client/assets/index-CYmI1EsU.css.br +0 -0
- package/dist/client/assets/index-CYmI1EsU.css.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-CQEu4Jwy.js → infoDiagram-27XIBGKW-BN4RpQTL.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-BPqibqsm.js → ishikawaDiagram-5VMMS53U-BZQ5qZB_.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-DIkhubnf.js → journeyDiagram-3NMN7TZE-Q8bx0_N4.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-CBT0DIUR.js → kanban-definition-UXKFOSKX-D7JhGtpO.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.gz +0 -0
- package/dist/client/assets/{layout-C1CMlOIU.js → layout-DERUg0xU.js} +1 -1
- package/dist/client/assets/layout-DERUg0xU.js.br +0 -0
- package/dist/client/assets/layout-DERUg0xU.js.gz +0 -0
- package/dist/client/assets/{linear-C4gyqor0.js → linear-lNTe4x00.js} +1 -1
- package/dist/client/assets/linear-lNTe4x00.js.br +0 -0
- package/dist/client/assets/linear-lNTe4x00.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-CMfa4ncd.js → mermaid.core-WHCwLQDQ.js} +5 -5
- package/dist/client/assets/mermaid.core-WHCwLQDQ.js.br +0 -0
- package/dist/client/assets/mermaid.core-WHCwLQDQ.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-C-JmgKOI.js → mindmap-definition-YA3MSWOX-BouapA4v.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-BmUG2IwA.js → passwordBootstrap-E0piP814.js} +1 -1
- package/dist/client/assets/passwordBootstrap-E0piP814.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-E0piP814.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-IY-AEKQa.js → pegDiagram-XKGWAZYB-Curpo5Rq.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-9Qq43qED.js → pieDiagram-E7YTZNPT-BLQCHaOG.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-BjQncx5i.js → quadrantDiagram-AXDQQJYC-PViFqGao.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-Cr1rL0wZ.js → railroadDiagram-O6MQD6OU-BtHo1LpT.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.gz +0 -0
- package/dist/client/assets/rdpSession-O_f9ejOI.js +1 -0
- package/dist/client/assets/rdpSession-O_f9ejOI.js.br +0 -0
- package/dist/client/assets/rdpSession-O_f9ejOI.js.gz +0 -0
- package/dist/client/assets/{referenceLongPress-ConC5-ym.js → referenceLongPress-BZr7Lqzh.js} +1 -1
- package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.br +0 -0
- package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-DU2q_iZg.js → requirementDiagram-BXWQKSXE-BUm81HUU.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.gz +0 -0
- package/dist/client/assets/rfb-CxsXsMUX.js +5 -0
- package/dist/client/assets/rfb-CxsXsMUX.js.br +0 -0
- package/dist/client/assets/rfb-CxsXsMUX.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BYlx9kXC.js → sankeyDiagram-P5KCCOFB-B-_-aZ_y.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-D_uAKme-.js → sequenceDiagram-WJ2MYXX4-DT6LDIfx.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-H_uqscf4.js → sizeCapture-INFHLROL-CTYerd9j.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.gz +0 -0
- package/dist/client/assets/{sparkles-D2z4Ue6G.js → sparkles-CgaUBIwH.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-BvsQjIIW.js → stateDiagram-D77RDMKH-CQbb5x2_.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CJAVXcx1.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-LmUJag-p.js → swimlanes-42K2YHIH-BHZ4X2Fd.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-VL2nyI27.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-DZ5n0Pim.js → timeline-definition-24CTP7MA-BrjH6R6w.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-Ct6iIBmv.js → triangle-alert-C9IHD_eF.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-BkTA_Ciz.js → vennDiagram-4TSXK5OY-C9f5jYgk.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-BkTA_Ciz.js.br → vennDiagram-4TSXK5OY-C9f5jYgk.js.br} +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-C9f5jYgk.js.gz +0 -0
- package/dist/client/assets/{videoCompression-VZIoFnyJ.js → videoCompression-DJaU8exd.js} +1 -1
- package/dist/client/assets/videoCompression-DJaU8exd.js.br +0 -0
- package/dist/client/assets/videoCompression-DJaU8exd.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-7hzU176j.js → wardleyDiagram-VM6X3IG4-6ljFvtiC.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-Bx4P96Bb.js → xychartDiagram-S5SC5T6Z-BaR2hm3e.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.gz +0 -0
- package/dist/client/index.html +2 -2
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.gz +0 -0
- package/dist/client/sw.js +1 -1
- package/dist/client/sw.js.br +0 -0
- package/dist/client/sw.js.gz +0 -0
- package/dist/server/agent/collaborationCli.js +45 -4
- package/dist/server/agent/collaborationDeliveryWorker.js +1 -1
- package/dist/server/agent/collaborationGroupRoutes.js +0 -2
- package/dist/server/agent/collaborationPeerTransport.js +8 -2
- package/dist/server/agent/collaborationResultPresentation.js +14 -0
- package/dist/server/agent/collaborationRoutes.js +18 -2
- package/dist/server/agent/collaborationService.js +46 -2
- package/dist/server/agent/collaborationStore.js +12 -21
- package/dist/server/agent/collaborationTaskBundles.js +121 -0
- package/dist/server/agent/collaborationTaskCli.js +54 -0
- package/dist/server/agent/collaborationTaskRoutes.js +58 -0
- package/dist/server/agent/collaborationTaskService.js +355 -0
- package/dist/server/agent/collaborationTaskStore.js +835 -0
- package/dist/server/agent/collaborationTaskTypes.js +1 -0
- package/dist/server/agent/collaborationTaskWorkspace.js +165 -0
- package/dist/server/agent/collaborationTeam.js +16 -0
- package/dist/server/agent/executionArchives.js +52 -0
- package/dist/server/entry.js +17 -2
- package/dist/server/federation/runtime.js +30 -7
- package/dist/server/routes/computer.js +198 -0
- package/dist/server/routes/computerTarget.js +23 -0
- package/dist/server/routes/guacamoleProtocol.js +55 -0
- package/dist/server/routes/rdp.js +162 -0
- package/dist/server/routes/terminal.js +441 -362
- package/dist/server/shared/computerPreferences.js +29 -0
- package/dist/server/utils/computerCredentials.js +88 -0
- package/dist/server/utils/sharedSampling.js +93 -0
- package/dist/server/utils/terminalOutputDelivery.js +8 -1
- package/dist/server/utils/tmuxMetadataWriter.js +61 -0
- package/package.json +4 -1
- package/runtime-manifest.json +4 -4
- package/dist/client/assets/App-D-cerXsD.js +0 -158
- package/dist/client/assets/App-D-cerXsD.js.br +0 -0
- package/dist/client/assets/App-D-cerXsD.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-Dv5XRtRh.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-Dv5XRtRh.js.gz +0 -0
- package/dist/client/assets/DagPlayground-mq69dKyr.js.br +0 -0
- package/dist/client/assets/DagPlayground-mq69dKyr.js.gz +0 -0
- package/dist/client/assets/DiffLab-BqdHYGi5.js.br +0 -0
- package/dist/client/assets/DiffLab-BqdHYGi5.js.gz +0 -0
- package/dist/client/assets/DiffReview-BvzGxpuc.js.br +0 -0
- package/dist/client/assets/DiffReview-BvzGxpuc.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-DrfddB7x.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-DrfddB7x.js.gz +0 -0
- package/dist/client/assets/DiffViewer-3bJV_A5s.js.br +0 -0
- package/dist/client/assets/DiffViewer-3bJV_A5s.js.gz +0 -0
- package/dist/client/assets/ModelPreview-CX7VtoJB.js.br +0 -0
- package/dist/client/assets/ModelPreview-CX7VtoJB.js.gz +0 -0
- package/dist/client/assets/RightSidebar-BK6vWNw3.js +0 -1678
- package/dist/client/assets/RightSidebar-BK6vWNw3.js.br +0 -0
- package/dist/client/assets/RightSidebar-BK6vWNw3.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-PJwhLTPH.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-PJwhLTPH.js.gz +0 -0
- package/dist/client/assets/app-C7pXMvSE.js +0 -35
- package/dist/client/assets/app-C7pXMvSE.js.br +0 -0
- package/dist/client/assets/app-C7pXMvSE.js.gz +0 -0
- package/dist/client/assets/arc-l3FXjCEl.js.br +0 -0
- package/dist/client/assets/arc-l3FXjCEl.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DdNbKi-4.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DdNbKi-4.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Cu63oaxR.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Cu63oaxR.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DQiV_yk6.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DQiV_yk6.js.gz +0 -0
- package/dist/client/assets/channel-CwC957gi.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-sLDCEnU8.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-sLDCEnU8.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-DYu5qS7g.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-DYu5qS7g.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-nJnbUFzs.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-nJnbUFzs.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BwyiSQgx.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BwyiSQgx.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-CmoJ7-rE.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-CmoJ7-rE.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-rroo-EOb.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-rroo-EOb.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CkFeV85k.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-hBwpU_bW.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-hBwpU_bW.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-B321-hdp.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-B321-hdp.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-bY87_ptA.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-bY87_ptA.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN--cez0H_o.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN--cez0H_o.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-BupH_VRK.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-BupH_VRK.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DewqaxvF.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DewqaxvF.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-wDumcTuR.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-wDumcTuR.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C0OY91We.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C0OY91We.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-Cw9aNqvJ.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-Cw9aNqvJ.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-BC7XUXeI.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DovhJIz5.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-DovhJIz5.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-wjVok2Lg.js.br +0 -0
- package/dist/client/assets/gestureArbiter-wjVok2Lg.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BmrbbyW1.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BmrbbyW1.js.gz +0 -0
- package/dist/client/assets/index-BeD4iJ6L.css +0 -1
- package/dist/client/assets/index-BeD4iJ6L.css.br +0 -0
- package/dist/client/assets/index-BeD4iJ6L.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BPqibqsm.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BPqibqsm.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DIkhubnf.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DIkhubnf.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CBT0DIUR.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CBT0DIUR.js.gz +0 -0
- package/dist/client/assets/layout-C1CMlOIU.js.br +0 -0
- package/dist/client/assets/layout-C1CMlOIU.js.gz +0 -0
- package/dist/client/assets/linear-C4gyqor0.js.br +0 -0
- package/dist/client/assets/linear-C4gyqor0.js.gz +0 -0
- package/dist/client/assets/mermaid.core-CMfa4ncd.js.br +0 -0
- package/dist/client/assets/mermaid.core-CMfa4ncd.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-C-JmgKOI.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-C-JmgKOI.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-BmUG2IwA.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-BmUG2IwA.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-IY-AEKQa.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-IY-AEKQa.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-9Qq43qED.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-9Qq43qED.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjQncx5i.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BjQncx5i.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Cr1rL0wZ.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Cr1rL0wZ.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-ConC5-ym.js.br +0 -0
- package/dist/client/assets/referenceLongPress-ConC5-ym.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DU2q_iZg.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DU2q_iZg.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BYlx9kXC.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BYlx9kXC.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D_uAKme-.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D_uAKme-.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-H_uqscf4.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-H_uqscf4.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-BvsQjIIW.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-BvsQjIIW.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-DNgoExtD.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-LmUJag-p.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-LmUJag-p.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-BRuArMR5.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-DZ5n0Pim.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-DZ5n0Pim.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-BkTA_Ciz.js.gz +0 -0
- package/dist/client/assets/videoCompression-VZIoFnyJ.js.br +0 -0
- package/dist/client/assets/videoCompression-VZIoFnyJ.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-7hzU176j.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-7hzU176j.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Bx4P96Bb.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Bx4P96Bb.js.gz +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function taskMemberKey(member) { return `${member.serviceId}:${member.sessionId}`; }
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { mkdir, readFile, writeFile, rename, realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
const exec = promisify(execFile);
|
|
7
|
+
const git = async (cwd, args) => (await exec('git', ['-C', cwd, ...args], {
|
|
8
|
+
timeout: 30_000, maxBuffer: 2 * 1024 * 1024,
|
|
9
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_MERGE_AUTOEDIT: 'no' },
|
|
10
|
+
})).stdout.trim();
|
|
11
|
+
const home = path.join(os.homedir(), '.termdock', 'task-workspaces');
|
|
12
|
+
/** Only committed code enters task worktrees; the user's checkout is never reset, cleaned or merged. */
|
|
13
|
+
export async function prepareTaskWorkspace(task, sourceCwd, dependencies, importCommit) {
|
|
14
|
+
if (!/^[a-f0-9]{32}$/.test(task.id))
|
|
15
|
+
throw new Error('任务标识无效');
|
|
16
|
+
const generation = task.events.filter(e => e.kind === 'scheduled').at(-1)?.id;
|
|
17
|
+
if (!generation || !/^[a-f0-9]{32}$/.test(generation))
|
|
18
|
+
throw new Error('缺少执行目录准备记录');
|
|
19
|
+
const repository = await git(sourceCwd, ['rev-parse', '--show-toplevel']);
|
|
20
|
+
const rootId = task.workflow?.rootTaskId ?? task.id;
|
|
21
|
+
const goalDir = path.join(home, rootId);
|
|
22
|
+
await mkdir(goalDir, { recursive: true, mode: 0o700 });
|
|
23
|
+
// Pin a base per repository and goal. A moved user HEAD cannot change later tasks' starting point.
|
|
24
|
+
const { createHash } = await import('node:crypto');
|
|
25
|
+
const common = await realpath(path.resolve(repository, await git(repository, ['rev-parse', '--git-common-dir'])));
|
|
26
|
+
const baseFile = path.join(goalDir, `${createHash('sha256').update(common).digest('hex').slice(0, 16)}.base`);
|
|
27
|
+
let base;
|
|
28
|
+
try {
|
|
29
|
+
base = (await readFile(baseFile, 'utf8')).trim();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT')
|
|
33
|
+
throw error;
|
|
34
|
+
base = await git(repository, ['rev-parse', 'HEAD']);
|
|
35
|
+
try {
|
|
36
|
+
await writeFile(baseFile, base, { flag: 'wx', mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== 'EEXIST')
|
|
40
|
+
throw error;
|
|
41
|
+
base = (await readFile(baseFile, 'utf8')).trim();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!/^[a-f0-9]{40,64}$/.test(base))
|
|
45
|
+
throw new Error('目标基线无效');
|
|
46
|
+
const cwd = path.join(goalDir, `${task.id}-${generation.slice(0, 8)}`);
|
|
47
|
+
const branch = `termdock/task/${task.id}-${generation.slice(0, 8)}`;
|
|
48
|
+
try {
|
|
49
|
+
await readFile(path.join(cwd, '.git'), 'utf8');
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== 'ENOENT')
|
|
53
|
+
throw error;
|
|
54
|
+
await git(repository, ['worktree', 'add', '-b', branch, cwd, base]);
|
|
55
|
+
}
|
|
56
|
+
if (await git(cwd, ['rev-parse', '--show-toplevel']) !== cwd || await git(cwd, ['branch', '--show-current']) !== branch)
|
|
57
|
+
throw new Error('独立目录与任务分支不匹配,保留现场等待处理');
|
|
58
|
+
const readyFile = path.join(goalDir, `${task.id}-${generation.slice(0, 8)}.ready`);
|
|
59
|
+
let ready = false;
|
|
60
|
+
try {
|
|
61
|
+
ready = (await readFile(readyFile, 'utf8')) === generation;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== 'ENOENT')
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (!ready) {
|
|
68
|
+
// A failed merge is deliberately left in this task's worktree for a member to resolve.
|
|
69
|
+
if (await git(cwd, ['status', '--porcelain']))
|
|
70
|
+
throw new Error(`依赖集成需要处理,现场保留在 ${cwd};请协调者安排解决冲突后继续`);
|
|
71
|
+
for (const dependency of dependencies) {
|
|
72
|
+
const artifact = dependency.artifacts.find(a => a.id === dependency.acceptedArtifactId);
|
|
73
|
+
const commit = artifact?.evidence?.commit;
|
|
74
|
+
if (!commit || !/^[a-f0-9]{40,64}$/.test(commit))
|
|
75
|
+
throw new Error(`依赖「${dependency.title}」缺少已评审提交,请补充交付`);
|
|
76
|
+
try {
|
|
77
|
+
await git(cwd, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
if (!importCommit)
|
|
81
|
+
throw new Error(`依赖「${dependency.title}」的提交在此仓库不可用,请协调者先同步提交`);
|
|
82
|
+
await importCommit(dependency, cwd);
|
|
83
|
+
await git(cwd, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
84
|
+
}
|
|
85
|
+
await git(cwd, ['-c', 'user.name=Termdock', '-c', 'user.email=tasks@termdock.local', 'merge', '--no-edit', commit]);
|
|
86
|
+
}
|
|
87
|
+
const temp = `${readyFile}.${process.pid}.tmp`;
|
|
88
|
+
await writeFile(temp, generation, { mode: 0o600 });
|
|
89
|
+
await rename(temp, readyFile);
|
|
90
|
+
}
|
|
91
|
+
return { cwd, repository, branch, base };
|
|
92
|
+
}
|
|
93
|
+
export async function captureTaskCommit(workspace) {
|
|
94
|
+
if (!(await realpath(workspace.cwd)).startsWith(`${await realpath(home)}${path.sep}`) || await git(workspace.cwd, ['branch', '--show-current']) !== workspace.branch)
|
|
95
|
+
throw new Error('交付目录与任务分支不匹配');
|
|
96
|
+
if (await git(workspace.cwd, ['status', '--porcelain']))
|
|
97
|
+
throw new Error('独立目录还有未提交改动,请先提交再报告完成;不会自动暂存或提交');
|
|
98
|
+
const commit = await git(workspace.cwd, ['rev-parse', 'HEAD']);
|
|
99
|
+
return { commit, branch: workspace.branch, cwd: workspace.cwd, base: workspace.base };
|
|
100
|
+
}
|
|
101
|
+
async function assertManagedWorkspace(workspace) {
|
|
102
|
+
const relative = path.relative(home, workspace.cwd);
|
|
103
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)
|
|
104
|
+
|| !/^termdock\/task\/[a-f0-9]{32}-[a-f0-9]{8}$/.test(workspace.branch))
|
|
105
|
+
throw new Error('目录不是 Termdock 独立执行工作区');
|
|
106
|
+
const actual = await realpath(workspace.cwd);
|
|
107
|
+
if (actual !== path.resolve(workspace.cwd) || !actual.startsWith(`${await realpath(home)}${path.sep}`)
|
|
108
|
+
|| await git(actual, ['rev-parse', '--show-toplevel']) !== actual
|
|
109
|
+
|| await git(actual, ['branch', '--show-current']) !== workspace.branch)
|
|
110
|
+
throw new Error('执行目录或分支发生变化,现场已保留');
|
|
111
|
+
const common = async (cwd) => realpath(path.resolve(cwd, await git(cwd, ['rev-parse', '--git-common-dir'])));
|
|
112
|
+
if (await common(actual) !== await common(workspace.repository))
|
|
113
|
+
throw new Error('执行目录不属于原仓库,现场已保留');
|
|
114
|
+
}
|
|
115
|
+
/** Remove only a verified, clean worktree. Keep its branch and all Git commits. */
|
|
116
|
+
export async function archiveTaskWorkspace(workspace) {
|
|
117
|
+
try {
|
|
118
|
+
await assertManagedWorkspace(workspace);
|
|
119
|
+
const commit = await git(workspace.cwd, ['rev-parse', 'HEAD']);
|
|
120
|
+
if (await git(workspace.cwd, ['status', '--porcelain']) || await git(workspace.cwd, ['ls-files', '--others', '--ignored', '--exclude-standard'])) {
|
|
121
|
+
return { state: 'retained', reason: '目录含未提交或忽略的文件,已保留现场', commit };
|
|
122
|
+
}
|
|
123
|
+
await git(workspace.repository, ['worktree', 'remove', workspace.cwd]);
|
|
124
|
+
return { state: 'removed', commit };
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return { state: 'retained', reason: error instanceof Error ? error.message : String(error) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export async function snapshotTaskWorkspace(workspace) {
|
|
131
|
+
await assertManagedWorkspace(workspace);
|
|
132
|
+
return git(workspace.cwd, ['rev-parse', 'HEAD']);
|
|
133
|
+
}
|
|
134
|
+
export async function restoreTaskWorkspace(workspace, cleanup) {
|
|
135
|
+
if (cleanup.state !== 'removed') {
|
|
136
|
+
try {
|
|
137
|
+
await assertManagedWorkspace(workspace);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (cleanup.state !== 'pending' || error.code !== 'ENOENT')
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const relative = path.relative(home, workspace.cwd);
|
|
146
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)
|
|
147
|
+
|| !/^termdock\/task\/[a-f0-9]{32}-[a-f0-9]{8}$/.test(workspace.branch) || !cleanup.commit)
|
|
148
|
+
throw new Error('工作区恢复记录无效');
|
|
149
|
+
try {
|
|
150
|
+
await stat(workspace.cwd);
|
|
151
|
+
await assertManagedWorkspace(workspace);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error.code !== 'ENOENT')
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (await git(workspace.repository, ['rev-parse', workspace.branch]) !== cleanup.commit)
|
|
159
|
+
throw new Error('归档分支已有变化,请核对后再恢复;不会重置分支');
|
|
160
|
+
await mkdir(path.dirname(workspace.cwd), { recursive: true, mode: 0o700 });
|
|
161
|
+
if (!(await realpath(path.dirname(workspace.cwd))).startsWith(`${await realpath(home)}${path.sep}`))
|
|
162
|
+
throw new Error('工作区父目录发生变化');
|
|
163
|
+
await git(workspace.repository, ['worktree', 'add', workspace.cwd, workspace.branch]);
|
|
164
|
+
await assertManagedWorkspace(workspace);
|
|
165
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Provision only missing roles. Persist each member before proceeding so a
|
|
2
|
+
* failed launch, retry or server restart cannot create another full team. */
|
|
3
|
+
export async function ensureTeam(options) {
|
|
4
|
+
const ids = [];
|
|
5
|
+
const labels = [`自动协调者 (${options.agentSlug})`, `自动执行与评审 (${options.agentSlug})`];
|
|
6
|
+
for (const role of labels) {
|
|
7
|
+
const members = options.members();
|
|
8
|
+
const reserved = members.find(member => member.role?.startsWith(role.split(' (')[0]));
|
|
9
|
+
if (reserved && (reserved.role !== role || reserved.cwd !== options.cwd)) {
|
|
10
|
+
throw new Error('本组已有自动配置的成员,请使用原 Agent 和目录,或在成员设置中移出后重新配置');
|
|
11
|
+
}
|
|
12
|
+
const existing = reserved ?? members.find(member => !ids.includes(member.id) && member.agentSlug === options.agentSlug && member.cwd === options.cwd);
|
|
13
|
+
ids.push(existing?.id ?? await options.spawn(role));
|
|
14
|
+
}
|
|
15
|
+
return { coordinatorSessionId: ids[0], reviewerSessionIds: [ids[1]] };
|
|
16
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { normalizePersistedAgentResumeInfo } from './resumePersistence.js';
|
|
4
|
+
/** Explicit archives are durable records, independent of the recent-history cap. */
|
|
5
|
+
export class ExecutionArchiveStore {
|
|
6
|
+
file;
|
|
7
|
+
entries;
|
|
8
|
+
constructor(file) {
|
|
9
|
+
this.file = file;
|
|
10
|
+
try {
|
|
11
|
+
const document = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
12
|
+
if (document.version !== 1 || !Array.isArray(document.entries))
|
|
13
|
+
throw new Error('执行归档文件格式无效');
|
|
14
|
+
this.entries = document.entries.map((entry) => {
|
|
15
|
+
const agent = normalizePersistedAgentResumeInfo(entry.agent);
|
|
16
|
+
if (!agent || !entry.sessionId || !entry.workspace?.cwd || !Array.isArray(entry.groups) || !Array.isArray(entry.taskIds))
|
|
17
|
+
throw new Error('执行归档记录无效');
|
|
18
|
+
return { ...entry, agent };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code !== 'ENOENT')
|
|
23
|
+
throw error;
|
|
24
|
+
this.entries = [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
list() { return structuredClone(this.entries); }
|
|
28
|
+
get(id) { return this.list().find(entry => entry.sessionId === id); }
|
|
29
|
+
save(entry) {
|
|
30
|
+
if (!normalizePersistedAgentResumeInfo(entry.agent))
|
|
31
|
+
throw new Error('没有可恢复的 Agent 原生会话 ID,终端已保留');
|
|
32
|
+
const entries = [structuredClone(entry), ...this.entries.filter(item => item.sessionId !== entry.sessionId)];
|
|
33
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true, mode: 0o700 });
|
|
34
|
+
const temporary = `${this.file}.${process.pid}.tmp`;
|
|
35
|
+
fs.writeFileSync(temporary, JSON.stringify({ version: 1, entries }), { mode: 0o600 });
|
|
36
|
+
fs.renameSync(temporary, this.file);
|
|
37
|
+
this.entries = entries;
|
|
38
|
+
return structuredClone(entry);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function completedExecution(tasks, serviceId, sessionId) {
|
|
42
|
+
const matches = (member) => member?.serviceId === serviceId && member.sessionId === sessionId;
|
|
43
|
+
if (tasks.some(task => matches(task.coordinator) || task.workflow?.reviewers.some(matches)
|
|
44
|
+
|| task.status === 'open' && (matches(task.attempts.find(a => a.id === task.activeAttemptId)?.assignee) || matches(task.scheduledAssignee)))) {
|
|
45
|
+
throw new Error('此会话仍承担协作工作,不能归档;请完成当前任务后再操作');
|
|
46
|
+
}
|
|
47
|
+
const completed = tasks.filter(task => task.status !== 'open' && task.workspace && task.workflow?.kind === 'step' && task.workflow.isolated
|
|
48
|
+
&& matches(task.attempts.find(a => a.id === task.activeAttemptId)?.assignee));
|
|
49
|
+
if (!completed.length)
|
|
50
|
+
throw new Error('此会话没有已完成的独立执行任务,终端已保留');
|
|
51
|
+
return completed;
|
|
52
|
+
}
|
package/dist/server/entry.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CollaborationService } from './agent/collaborationService.js';
|
|
2
2
|
import { X509Certificate } from 'node:crypto';
|
|
3
3
|
import { CollaborationPeerTransport, connectCollaborationRpc } from './agent/collaborationPeerTransport.js';
|
|
4
|
-
import { collaborationStore, deliverPeerCollaboration, collaborationLocalActivity, collaborationDirectorySessions } from './routes/terminal.js';
|
|
4
|
+
import { collaborationStore, collaborationTaskStore, deliverPeerCollaboration, collaborationLocalActivity, collaborationDirectorySessions, prepareCollaborationTaskWorker } from './routes/terminal.js';
|
|
5
|
+
import { CollaborationTaskService } from './agent/collaborationTaskService.js';
|
|
5
6
|
import { setPushTargetPeerId } from './notifications/pushService.js';
|
|
6
7
|
import { desktopDirectTargets } from './federation/desktopTargets.js';
|
|
7
8
|
import { createOpenAccessRouter } from './federation/openAccess.js';
|
|
@@ -31,6 +32,7 @@ import { fileURLToPath } from 'url';
|
|
|
31
32
|
import { WebSocketServer } from 'ws';
|
|
32
33
|
import terminalRoutes, { handleTerminalWebSocket, handleControlWebSocket, resolveStableFederationSessionId } from './routes/terminal.js';
|
|
33
34
|
import androidRoutes, { handleAndroidWebSocket } from './routes/android.js';
|
|
35
|
+
import computerRoutes, { handleComputerWebSocket, stopComputerBridges } from './routes/computer.js';
|
|
34
36
|
import { exitWithStartupFailure } from './utils/startupFailure.js';
|
|
35
37
|
import { androidRecordings } from './android/recording.js';
|
|
36
38
|
import { stopAllScrcpySessions } from './android/scrcpy.js';
|
|
@@ -348,6 +350,9 @@ export function createApp(options = {}) {
|
|
|
348
350
|
app.use('/api/android', requireAuth({ bypass: isTrustedLocalCliRequest }));
|
|
349
351
|
app.use('/api/android', csrfProtection.verifyMiddleware({ bypass: isTrustedLocalCliRequest }));
|
|
350
352
|
app.use('/api/android', androidRoutes);
|
|
353
|
+
app.use('/api/computer', requireAuth({ bypass: isTrustedLocalCliRequest }));
|
|
354
|
+
app.use('/api/computer', csrfProtection.verifyMiddleware({ bypass: isTrustedLocalCliRequest }));
|
|
355
|
+
app.use('/api/computer', computerRoutes);
|
|
351
356
|
if (fs.existsSync(bundledClientIndexPath)) {
|
|
352
357
|
const clientPath = pinBundledRuntimeClientDist(bundledClientDistPath);
|
|
353
358
|
const compression = createStaticCompressionMiddleware(clientPath);
|
|
@@ -499,8 +504,9 @@ export function startServer(options = {}) {
|
|
|
499
504
|
target.close(); });
|
|
500
505
|
server.once('close', () => relayRouter.close());
|
|
501
506
|
server.once('close', () => { void stopAllScrcpySessions(); void androidRecordings.stopAll(); });
|
|
507
|
+
server.once('close', stopComputerBridges);
|
|
502
508
|
const federation = createFederationRuntime(app, path.join(homedir(), '.termdock', 'federation'), {
|
|
503
|
-
terminal: handleTerminalWebSocket, control: handleControlWebSocket, android: handleAndroidWebSocket,
|
|
509
|
+
terminal: handleTerminalWebSocket, control: handleControlWebSocket, android: handleAndroidWebSocket, computer: handleComputerWebSocket,
|
|
504
510
|
}, {
|
|
505
511
|
resolveSessionId: resolveStableFederationSessionId,
|
|
506
512
|
collaborationConnected: (subjectId, rpc) => {
|
|
@@ -616,11 +622,20 @@ export function startServer(options = {}) {
|
|
|
616
622
|
collaborationService = new CollaborationService({ file: path.join(homedir(), '.termdock', 'federation', 'collaboration-services.json'),
|
|
617
623
|
store: collaborationStore, transport: collaborationTransport, node: () => app.locals.collaborationNode,
|
|
618
624
|
sessions: collaborationDirectorySessions, reverse: reverseCollaboration, connect: connectPeer,
|
|
625
|
+
tasks: {
|
|
626
|
+
receive: (peer, packet) => app.locals.collaborationTasks.receive(peer, packet),
|
|
627
|
+
heads: ids => app.locals.collaborationTasks?.heads(ids) ?? [],
|
|
628
|
+
sync: (peer, heads) => app.locals.collaborationTasks.sync(peer, heads),
|
|
629
|
+
},
|
|
619
630
|
deviceScope: device => runtime.store.listEffective({ subjectId: device, serviceId: runtime.serviceId })
|
|
620
631
|
.filter(grant => grant.scope.kind === 'sessions' && grant.actions.includes('session.input'))
|
|
621
632
|
.flatMap(grant => grant.scope.kind === 'sessions' ? grant.scope.sessionIds : []),
|
|
622
633
|
pairConnect: peer => connectCollaborationRpc(runtime.identity, peer) });
|
|
623
634
|
app.locals.collaborationService = collaborationService;
|
|
635
|
+
const collaborationTasks = new CollaborationTaskService(collaborationTaskStore, collaborationStore, collaborationService, deliverPeerCollaboration, (task, template, dependencies) => prepareCollaborationTaskWorker(task, template, dependencies, collaborationService, (taskId, dependencyId, offset) => collaborationTasks.dependencyChunk(taskId, dependencyId, offset)));
|
|
636
|
+
app.locals.collaborationTasks = collaborationTasks;
|
|
637
|
+
collaborationTasks.start();
|
|
638
|
+
server.once('close', () => collaborationTasks.close());
|
|
624
639
|
collaborationService.start();
|
|
625
640
|
server.once('close', () => collaborationService?.close());
|
|
626
641
|
collaborationTransport.start();
|
|
@@ -252,8 +252,8 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
revokeTimer = setInterval(() => {
|
|
255
|
-
for (const { socket: logical, sessionId, android } of sockets.values()) {
|
|
256
|
-
if (android) {
|
|
255
|
+
for (const { socket: logical, sessionId, android, computer } of sockets.values()) {
|
|
256
|
+
if (android || computer) {
|
|
257
257
|
if (!full(subjectId))
|
|
258
258
|
logical.close(4003, 'Authorization revoked');
|
|
259
259
|
continue;
|
|
@@ -297,7 +297,12 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
297
297
|
else if (packet.type === 'collaboration-service') {
|
|
298
298
|
if (!options.collaborationService)
|
|
299
299
|
throw new Error('COLLABORATION_UPGRADE_REQUIRED');
|
|
300
|
-
|
|
300
|
+
// Task preparation may use the same channel for a directory request; do not block its reader.
|
|
301
|
+
void Promise.resolve(options.collaborationService(subjectId, packet)).then(result => send({ type: 'result', id: packet.id, ...result }))
|
|
302
|
+
.catch(error => { try {
|
|
303
|
+
send({ type: 'error', id: packet.id, error: error instanceof Error ? error.message : 'REQUEST_FAILED' });
|
|
304
|
+
}
|
|
305
|
+
catch { /* Connection closed. */ } });
|
|
301
306
|
}
|
|
302
307
|
else if (packet.type === 'collaboration-scoped-peer') {
|
|
303
308
|
if (!options.registerScopedCollaborationPeer)
|
|
@@ -564,18 +569,20 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
564
569
|
const url = new URL(packet.path, 'http://inner');
|
|
565
570
|
const match = /^\/api\/terminal\/([^/%]+)\/ws$/.exec(url.pathname);
|
|
566
571
|
const androidMatch = /^\/api\/android\/([^/%]+)\/ws$/.exec(url.pathname);
|
|
572
|
+
const computer = url.pathname === '/api/computer/ws';
|
|
567
573
|
if (match)
|
|
568
574
|
check('session.view', match[1]);
|
|
569
575
|
else if (url.pathname === '/api/control/ws')
|
|
570
576
|
check('service.view');
|
|
571
|
-
else if (androidMatch) {
|
|
577
|
+
else if (androidMatch || computer) {
|
|
572
578
|
if (!full(subjectId))
|
|
573
579
|
throw new Error('API_NOT_ALLOWED');
|
|
574
580
|
}
|
|
575
581
|
else
|
|
576
582
|
throw new Error('API_NOT_ALLOWED');
|
|
577
|
-
const logical = new LogicalSocket(channel, packet.id, () => androidMatch ? full(subjectId) : allowed(subjectId, match ? 'session.view' : 'service.view', match?.[1]));
|
|
578
|
-
sockets.set(packet.id, { socket: logical, sessionId: match?.[1], android: Boolean(androidMatch)
|
|
583
|
+
const logical = new LogicalSocket(channel, packet.id, () => (androidMatch || computer) ? full(subjectId) : allowed(subjectId, match ? 'session.view' : 'service.view', match?.[1]));
|
|
584
|
+
sockets.set(packet.id, { socket: logical, sessionId: match?.[1], android: Boolean(androidMatch), computer,
|
|
585
|
+
computerProtocol: computer ? url.searchParams.get('protocol') ?? 'vnc' : undefined });
|
|
579
586
|
logical.once('close', () => sockets.delete(packet.id));
|
|
580
587
|
send({ type: 'ws-ready', id: packet.id });
|
|
581
588
|
if (match)
|
|
@@ -593,6 +600,15 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
593
600
|
maxFps: Number(url.searchParams.get('max_fps')) || undefined,
|
|
594
601
|
});
|
|
595
602
|
}
|
|
603
|
+
else if (computer) {
|
|
604
|
+
if (!handlers.computer) {
|
|
605
|
+
logical.close(4403, 'API_NOT_ALLOWED');
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
handlers.computer(logical, url.searchParams.get('host') ?? '', {
|
|
609
|
+
protocol: url.searchParams.get('protocol') ?? undefined, port: url.searchParams.get('port') ?? undefined,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
596
612
|
else
|
|
597
613
|
handlers.control(logical, randomUUID());
|
|
598
614
|
}
|
|
@@ -601,7 +617,14 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
601
617
|
if (!item || typeof packet.data !== 'string')
|
|
602
618
|
throw new Error('INVALID_STREAM');
|
|
603
619
|
const data = JSON.parse(packet.data);
|
|
604
|
-
if (item.
|
|
620
|
+
if (item.computer) {
|
|
621
|
+
if (!full(subjectId))
|
|
622
|
+
throw new Error('AUTHORIZATION_DENIED');
|
|
623
|
+
const types = item.computerProtocol === 'rdp' ? ['start', 'instruction', 'ack'] : ['data', 'ack'];
|
|
624
|
+
if (!types.includes(data?.type))
|
|
625
|
+
throw new Error('ACTION_NOT_ALLOWED');
|
|
626
|
+
}
|
|
627
|
+
else if (item.android) {
|
|
605
628
|
if (!full(subjectId))
|
|
606
629
|
throw new Error('AUTHORIZATION_DENIED');
|
|
607
630
|
if (!['control', 'ack', 'ping', 'bitrate'].includes(data?.type))
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
4
|
+
import { Socket } from 'node:net';
|
|
5
|
+
import { resolveComputerHost } from './computerTarget.js';
|
|
6
|
+
import { handleRdpWebSocket, rdpBackendAvailable, stopRdpBridges } from './rdp.js';
|
|
7
|
+
import { computerLoginKey, computerPreferences } from '../shared/computerPreferences.js';
|
|
8
|
+
import { loadSettings, updateSettings } from '../utils/settings.js';
|
|
9
|
+
import { ComputerCredentialStore } from '../utils/computerCredentials.js';
|
|
10
|
+
export function createComputerRouter(read = loadSettings, update = updateSettings, credentials = new ComputerCredentialStore()) {
|
|
11
|
+
const router = Router();
|
|
12
|
+
router.get('/status', async (_req, res) => res.json({ platform: process.platform, hostname: hostname(), rdpAvailable: await rdpBackendAvailable() }));
|
|
13
|
+
router.get('/preferences', (_req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const saved = read().computerControl;
|
|
16
|
+
const preferences = computerPreferences(saved, process.platform);
|
|
17
|
+
let credentialKeys = [], credentialError = false;
|
|
18
|
+
try {
|
|
19
|
+
credentialKeys = [preferences.local, preferences.remote].filter(profile => profile.rememberLogin && credentials.has(profile)).map(computerLoginKey);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
credentialError = true;
|
|
23
|
+
}
|
|
24
|
+
res.set('Cache-Control', 'no-store').json({ preferences, configured: Boolean(saved), credentialKeys, credentialError });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
res.status(500).json({ error: 'COMPUTER_PREFERENCES_READ_FAILED' });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// The full-service encrypted API boundary applies to every credential route.
|
|
31
|
+
router.post('/credentials/:action', (req, res) => {
|
|
32
|
+
res.set('Cache-Control', 'no-store');
|
|
33
|
+
try {
|
|
34
|
+
const { profile, password } = req.body ?? {};
|
|
35
|
+
const preferences = computerPreferences(read().computerControl, process.platform);
|
|
36
|
+
const matched = [preferences.local, preferences.remote].find(value => isDeepStrictEqual(value, profile));
|
|
37
|
+
const action = req.params.action;
|
|
38
|
+
const allowedKeys = action === 'save' ? ['profile', 'password'] : ['profile'];
|
|
39
|
+
if (!matched || !['save', 'use', 'forget'].includes(action) || Object.keys(req.body).some(key => !allowedKeys.includes(key))) {
|
|
40
|
+
res.status(400).json({ error: 'INVALID_COMPUTER_CREDENTIALS' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (action === 'forget') {
|
|
44
|
+
credentials.forget(matched);
|
|
45
|
+
res.json({ saved: false });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!matched.rememberLogin) {
|
|
49
|
+
res.status(400).json({ error: 'COMPUTER_LOGIN_NOT_REMEMBERED' });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (action === 'save') {
|
|
53
|
+
if (typeof password !== 'string' || !password || password.length > 1024 || password.includes('\0')) {
|
|
54
|
+
res.status(400).json({ error: 'INVALID_COMPUTER_CREDENTIALS' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
credentials.save(matched, password);
|
|
58
|
+
res.json({ saved: true });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const secret = credentials.get(matched);
|
|
62
|
+
if (!secret) {
|
|
63
|
+
res.status(404).json({ error: 'COMPUTER_CREDENTIALS_MISSING' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Only the authenticated service page receives it, in memory, over Noise.
|
|
67
|
+
res.json({ password: secret });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
res.status(500).json({ error: 'COMPUTER_CREDENTIALS_FAILED' });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
router.put('/preferences', (req, res) => {
|
|
74
|
+
const preferences = computerPreferences(req.body, process.platform);
|
|
75
|
+
// Strict projection prevents credentials and unknown fields entering settings.
|
|
76
|
+
if (!req.body || !isDeepStrictEqual(preferences, req.body)) {
|
|
77
|
+
res.status(400).json({ error: 'INVALID_COMPUTER_PREFERENCES' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
update((settings) => { settings.computerControl = preferences; });
|
|
82
|
+
res.json({ preferences });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
res.status(500).json({ error: 'COMPUTER_PREFERENCES_WRITE_FAILED' });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return router;
|
|
89
|
+
}
|
|
90
|
+
export default createComputerRouter();
|
|
91
|
+
const bridges = new Set();
|
|
92
|
+
export function stopComputerBridges() {
|
|
93
|
+
stopRdpBridges();
|
|
94
|
+
for (const bridge of bridges)
|
|
95
|
+
bridge.destroy();
|
|
96
|
+
bridges.clear();
|
|
97
|
+
}
|
|
98
|
+
/** RFB bytes are base64 records inside the existing authenticated Noise tunnel.
|
|
99
|
+
* The eight-record receive window applies TCP backpressure without dropping bytes. */
|
|
100
|
+
export function handleComputerWebSocket(socket, host, options) {
|
|
101
|
+
if (options?.protocol === 'rdp') {
|
|
102
|
+
handleRdpWebSocket(socket, host, Number(options.port ?? 3389));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (options?.protocol && options.protocol !== 'vnc') {
|
|
106
|
+
socket.close(4400, 'COMPUTER_INVALID_PROTOCOL');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let closed = false;
|
|
110
|
+
let tcp;
|
|
111
|
+
let sent = 0;
|
|
112
|
+
let acked = 0;
|
|
113
|
+
let lastAckAt = Date.now();
|
|
114
|
+
let connected = false;
|
|
115
|
+
const finish = (code = 1000, reason = '') => {
|
|
116
|
+
if (closed)
|
|
117
|
+
return;
|
|
118
|
+
closed = true;
|
|
119
|
+
clearTimeout(connectTimer);
|
|
120
|
+
clearInterval(flowTimer);
|
|
121
|
+
if (tcp) {
|
|
122
|
+
bridges.delete(tcp);
|
|
123
|
+
tcp.destroy();
|
|
124
|
+
}
|
|
125
|
+
socket.close(code, reason);
|
|
126
|
+
};
|
|
127
|
+
const connectTimer = setTimeout(() => finish(4408, 'COMPUTER_CONNECT_TIMEOUT'), 10_000);
|
|
128
|
+
const flowTimer = setInterval(() => {
|
|
129
|
+
if (sent > acked && Date.now() - lastAckAt > 30_000)
|
|
130
|
+
finish(4408, 'COMPUTER_STREAM_TIMEOUT');
|
|
131
|
+
}, 5000);
|
|
132
|
+
const pump = () => {
|
|
133
|
+
if (closed || !tcp)
|
|
134
|
+
return;
|
|
135
|
+
while (sent - acked < 8 && tcp.readableLength > 0) {
|
|
136
|
+
const bytes = tcp.read(Math.min(tcp.readableLength, 32 * 1024));
|
|
137
|
+
if (!bytes)
|
|
138
|
+
break;
|
|
139
|
+
if (sent === acked)
|
|
140
|
+
lastAckAt = Date.now();
|
|
141
|
+
socket.send(JSON.stringify({ type: 'data', seq: ++sent, data: bytes.toString('base64') }));
|
|
142
|
+
if (closed)
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
socket.on('close', () => finish());
|
|
147
|
+
socket.on('error', () => finish());
|
|
148
|
+
socket.on('message', raw => {
|
|
149
|
+
if (closed)
|
|
150
|
+
return;
|
|
151
|
+
try {
|
|
152
|
+
const message = JSON.parse(raw.toString());
|
|
153
|
+
if (message.type === 'ack') {
|
|
154
|
+
if (!Number.isSafeInteger(message.seq) || message.seq <= acked || message.seq > sent)
|
|
155
|
+
throw new Error('INVALID_ACK');
|
|
156
|
+
acked = message.seq;
|
|
157
|
+
lastAckAt = Date.now();
|
|
158
|
+
pump();
|
|
159
|
+
}
|
|
160
|
+
else if (message.type === 'data') {
|
|
161
|
+
if (!connected || !tcp || typeof message.data !== 'string' || message.data.length > 88_000
|
|
162
|
+
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(message.data))
|
|
163
|
+
throw new Error('INVALID_DATA');
|
|
164
|
+
const bytes = Buffer.from(message.data, 'base64');
|
|
165
|
+
if (!bytes.length || bytes.length > 64 * 1024 || tcp.writableLength + bytes.length > 1024 * 1024)
|
|
166
|
+
throw new Error('INPUT_LIMIT');
|
|
167
|
+
tcp.write(bytes);
|
|
168
|
+
}
|
|
169
|
+
else
|
|
170
|
+
throw new Error('INVALID_MESSAGE');
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
finish(4400, 'COMPUTER_INVALID_MESSAGE');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
void (async () => {
|
|
177
|
+
try {
|
|
178
|
+
const address = await resolveComputerHost(host);
|
|
179
|
+
if (closed)
|
|
180
|
+
return;
|
|
181
|
+
tcp = new Socket();
|
|
182
|
+
bridges.add(tcp);
|
|
183
|
+
tcp.setNoDelay(true);
|
|
184
|
+
tcp.on('readable', pump);
|
|
185
|
+
tcp.on('connect', () => { connected = true; clearTimeout(connectTimer); });
|
|
186
|
+
tcp.on('end', () => finish(1000, 'COMPUTER_DISCONNECTED'));
|
|
187
|
+
tcp.on('close', () => finish(1000, 'COMPUTER_DISCONNECTED'));
|
|
188
|
+
tcp.on('error', error => {
|
|
189
|
+
const code = error.code;
|
|
190
|
+
finish(4502, code === 'ECONNREFUSED' ? 'COMPUTER_SCREEN_SHARING_OFF' : 'COMPUTER_UNREACHABLE');
|
|
191
|
+
});
|
|
192
|
+
tcp.connect({ host: address.address, family: address.family, port: 5900 });
|
|
193
|
+
}
|
|
194
|
+
catch (reason) {
|
|
195
|
+
finish(4502, reason instanceof Error && /^COMPUTER_(?:INVALID_HOST|PRIVATE_HOST_ONLY)$/.test(reason.message) ? reason.message : 'COMPUTER_UNREACHABLE');
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
export function privateComputerAddress(address) {
|
|
4
|
+
if (isIP(address) === 4) {
|
|
5
|
+
const [a, b] = address.split('.').map(Number);
|
|
6
|
+
return a === 127 || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
|
|
7
|
+
}
|
|
8
|
+
const value = address.toLowerCase();
|
|
9
|
+
if (value.startsWith('::ffff:'))
|
|
10
|
+
return privateComputerAddress(value.slice(7));
|
|
11
|
+
return isIP(value) === 6 && (value === '::1' || value.startsWith('fc') || value.startsWith('fd'));
|
|
12
|
+
}
|
|
13
|
+
/** Resolve once, reject mixed public/private DNS answers, and dial the pinned IP. */
|
|
14
|
+
export async function resolveComputerHost(host) {
|
|
15
|
+
host = host.trim();
|
|
16
|
+
if (!host || host.length > 253 || (!isIP(host) && !/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i.test(host))) {
|
|
17
|
+
throw new Error('COMPUTER_INVALID_HOST');
|
|
18
|
+
}
|
|
19
|
+
const addresses = await lookup(host, { all: true });
|
|
20
|
+
if (!addresses.length || addresses.some(item => !privateComputerAddress(item.address)))
|
|
21
|
+
throw new Error('COMPUTER_PRIVATE_HOST_ONLY');
|
|
22
|
+
return addresses[0];
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function guacamoleInstruction(...values) {
|
|
2
|
+
return values.map(value => `${Array.from(value).length}.${value}`).join(',') + ';';
|
|
3
|
+
}
|
|
4
|
+
/** Incremental UTF-8-decoded Guacamole parser. Lengths count Unicode code points. */
|
|
5
|
+
export class GuacamoleParser {
|
|
6
|
+
instruction;
|
|
7
|
+
digits = '';
|
|
8
|
+
remaining = 0;
|
|
9
|
+
value = '';
|
|
10
|
+
values = [];
|
|
11
|
+
phase = 'length';
|
|
12
|
+
size = 0;
|
|
13
|
+
constructor(instruction) {
|
|
14
|
+
this.instruction = instruction;
|
|
15
|
+
}
|
|
16
|
+
receive(chunk) {
|
|
17
|
+
for (const char of chunk) {
|
|
18
|
+
if (++this.size > 2 * 1024 * 1024)
|
|
19
|
+
throw new Error('GUAC_INSTRUCTION_LIMIT');
|
|
20
|
+
if (this.phase === 'length') {
|
|
21
|
+
if (/^[0-9]$/.test(char) && this.digits.length < 7)
|
|
22
|
+
this.digits += char;
|
|
23
|
+
else if (char === '.' && this.digits) {
|
|
24
|
+
this.remaining = Number(this.digits);
|
|
25
|
+
this.digits = '';
|
|
26
|
+
if (this.remaining > 1024 * 1024)
|
|
27
|
+
throw new Error('GUAC_ELEMENT_LIMIT');
|
|
28
|
+
this.phase = this.remaining ? 'value' : 'separator';
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
throw new Error('GUAC_INVALID_LENGTH');
|
|
32
|
+
}
|
|
33
|
+
else if (this.phase === 'value') {
|
|
34
|
+
this.value += char;
|
|
35
|
+
if (--this.remaining === 0)
|
|
36
|
+
this.phase = 'separator';
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (char !== ',' && char !== ';')
|
|
40
|
+
throw new Error('GUAC_INVALID_SEPARATOR');
|
|
41
|
+
this.values.push(this.value);
|
|
42
|
+
this.value = '';
|
|
43
|
+
this.phase = 'length';
|
|
44
|
+
if (this.values.length > 256)
|
|
45
|
+
throw new Error('GUAC_ARGUMENT_LIMIT');
|
|
46
|
+
if (char === ';') {
|
|
47
|
+
const values = this.values;
|
|
48
|
+
this.values = [];
|
|
49
|
+
this.size = 0;
|
|
50
|
+
this.instruction(values);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|