pi-cloud 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +29 -0
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/pi-cloud.mjs +1 -1
- package/client/dist/.vite/manifest.json +303 -303
- package/client/dist/assets/{EditorPanel-qAJ9i_Hs.css → EditorPanel-5KsKffCI.css} +1 -1
- package/client/dist/assets/{EditorPanel-C4j_LnbI.js → EditorPanel-jZgIBqB6.js} +189 -189
- package/client/dist/assets/{GitHistoryView-i__uu43I.js → GitHistoryView-fz83ZRs_.js} +1 -1
- package/client/dist/assets/{MemoryCenter-DOvz1FIy.js → MemoryCenter-Di8khKRv.js} +1 -1
- package/client/dist/assets/{PhCaretLeft.vue-C3HxQ-jr.js → PhCaretLeft.vue-C_mgOR0C.js} +1 -1
- package/client/dist/assets/SettingsDialog-BsTvb8AX.js +2 -0
- package/client/dist/assets/SettingsDialog-JpBlJzlR.css +1 -0
- package/client/dist/assets/{TaskQueuePanel-DPnDhZFb.js → TaskQueuePanel-Bbk8CKW5.js} +1 -1
- package/client/dist/assets/{TerminalPanel-BQhGwCE5.js → TerminalPanel-CWNxgT84.js} +1 -1
- package/client/dist/assets/{abnfDiagram-VCTEODGH-DyGBNBbN.js → abnfDiagram-VCTEODGH-o53BjVqy.js} +1 -1
- package/client/dist/assets/{arc-B_21hOEg.js → arc-DsZjnKYA.js} +1 -1
- package/client/dist/assets/{architectureDiagram-5GKGNRK7-CjyAJ-2z.js → architectureDiagram-5GKGNRK7-u5Bv1Ym6.js} +1 -1
- package/client/dist/assets/{blockDiagram-NRAW4CY4-CmrEp6Fe.js → blockDiagram-NRAW4CY4-BSDeSfLw.js} +1 -1
- package/client/dist/assets/{c4Diagram-UCG6FXSJ-DfT2xU1s.js → c4Diagram-UCG6FXSJ-B6NfOWtd.js} +1 -1
- package/client/dist/assets/channel-vm9b-dk7.js +1 -0
- package/client/dist/assets/{chunk-2Q5K7J3B-BVEJoy95.js → chunk-2Q5K7J3B-CgX2R8z-.js} +1 -1
- package/client/dist/assets/{chunk-5VM5RSS4-AYOc2H0F.js → chunk-5VM5RSS4-ha_fObnM.js} +1 -1
- package/client/dist/assets/{chunk-F27PBJKO-CAdIcM4C.js → chunk-F27PBJKO-DI1DZ-wc.js} +1 -1
- package/client/dist/assets/{chunk-G27WJ6UU-Bk4Qjg6Y.js → chunk-G27WJ6UU-Dg1UIr6p.js} +1 -1
- package/client/dist/assets/{chunk-JWPE2WC7-u-wi2HbJ.js → chunk-JWPE2WC7-B3XeLm0J.js} +1 -1
- package/client/dist/assets/{chunk-LCL6LL3I-BgicKCMi.js → chunk-LCL6LL3I-qz7b1c_I.js} +1 -1
- package/client/dist/assets/{chunk-POPQ4Y6H-DhJ3_oti.js → chunk-POPQ4Y6H-B5NSvr4G.js} +1 -1
- package/client/dist/assets/{chunk-SVP7TREG-B-FKnWYy.js → chunk-SVP7TREG-7QwKLsdb.js} +1 -1
- package/client/dist/assets/{chunk-XXDRQBXY-Dnsv7iwr.js → chunk-XXDRQBXY-CAosos5-.js} +1 -1
- package/client/dist/assets/classDiagram-DTDB5LWJ-BZ5qRTrF.js +1 -0
- package/client/dist/assets/classDiagram-v2-JRS7N3AN-BZ5qRTrF.js +1 -0
- package/client/dist/assets/{cose-bilkent-JH36ORCC-DL2lIpOf.js → cose-bilkent-JH36ORCC-BsDQpeGC.js} +1 -1
- package/client/dist/assets/{cssMode-BcZYukVu.js → cssMode-Bixb-eY-.js} +1 -1
- package/client/dist/assets/{cynefin-OW5HDTMX-tZ1QaCp9.js → cynefin-OW5HDTMX-Drahq6GQ.js} +1 -1
- package/client/dist/assets/{cynefinDiagram-5FMLGOSQ-GLzVU_pJ.js → cynefinDiagram-5FMLGOSQ-CnkS0oyb.js} +1 -1
- package/client/dist/assets/{dagre-3AP2YEHR-B8V1RvVr.js → dagre-3AP2YEHR-BKQcTp5_.js} +1 -1
- package/client/dist/assets/{diagram-S7CK7UJ4-MTofUt2F.js → diagram-S7CK7UJ4-CRJVo1Nm.js} +1 -1
- package/client/dist/assets/{diagram-UQ7AKVKN-Dw6Onbe3.js → diagram-UQ7AKVKN-DsP-WSSg.js} +1 -1
- package/client/dist/assets/{diagram-VSXAHHWV-C4WupsI1.js → diagram-VSXAHHWV-Ccd1iqfn.js} +1 -1
- package/client/dist/assets/{diagram-VX7I27RA-CF78AvRx.js → diagram-VX7I27RA-D5w0ryS3.js} +1 -1
- package/client/dist/assets/{diagram-Z3DM3KII-xT7zx4S2.js → diagram-Z3DM3KII-DpUTugp8.js} +1 -1
- package/client/dist/assets/{ebnfDiagram-PWID7BFC-zkR-_zOG.js → ebnfDiagram-PWID7BFC-Donm234O.js} +1 -1
- package/client/dist/assets/{erDiagram-SSCWMZ5O-BzU9E1j8.js → erDiagram-SSCWMZ5O-CvtobvjZ.js} +1 -1
- package/client/dist/assets/{flowDiagram-A5DVABFB-BM-160SB.js → flowDiagram-A5DVABFB-v8UIYgnu.js} +1 -1
- package/client/dist/assets/{freemarker2-B4PXq-2i.js → freemarker2-CCBoOScX.js} +1 -1
- package/client/dist/assets/{ganttDiagram-EL5Y4UJY-LVhMiHMo.js → ganttDiagram-EL5Y4UJY-TCK2YgJA.js} +1 -1
- package/client/dist/assets/{gitGraphDiagram-WWUBYQGX-CYGEpNO-.js → gitGraphDiagram-WWUBYQGX-BTVfzqSz.js} +1 -1
- package/client/dist/assets/{handlebars-DleyW4LO.js → handlebars-D_iO3oKV.js} +1 -1
- package/client/dist/assets/{html-BJ95-ybC.js → html-BxskKvwE.js} +1 -1
- package/client/dist/assets/{htmlMode-Cqq8N0XF.js → htmlMode-BRqzPJmn.js} +1 -1
- package/client/dist/assets/index-2q9oNVFR.js +407 -0
- package/client/dist/assets/{index-PSBDlhaw.js → index-B4PeEJ0f.js} +1 -1
- package/client/dist/assets/{index-D1Z3F2UU.css → index-DBGNeJhr.css} +1 -1
- package/client/dist/assets/{infoDiagram-RXCK75RN-Ctmoz63S.js → infoDiagram-RXCK75RN-BkzEFABK.js} +1 -1
- package/client/dist/assets/{ishikawaDiagram-5VMMS53U-DiWG9VHU.js → ishikawaDiagram-5VMMS53U-DuVtEdHP.js} +1 -1
- package/client/dist/assets/{javascript-BIGigzp1.js → javascript-DLM6N2IL.js} +1 -1
- package/client/dist/assets/{journeyDiagram-EYS64GPL-CImdI3K_.js → journeyDiagram-EYS64GPL-Cx5cuIcS.js} +1 -1
- package/client/dist/assets/{jsonMode-l6_K9bFf.js → jsonMode-BZHZxAtU.js} +1 -1
- package/client/dist/assets/{kanban-definition-3QL26DDD-CSSranPL.js → kanban-definition-3QL26DDD-wXihmZ_e.js} +1 -1
- package/client/dist/assets/{layout-CE5PddK3.js → layout-C2KDikn-.js} +1 -1
- package/client/dist/assets/{linear-CkzMiZL-.js → linear-C6ueyh3n.js} +1 -1
- package/client/dist/assets/{liquid-C8hVjUZv.js → liquid-CK9kFoUD.js} +1 -1
- package/client/dist/assets/{lspLanguageFeatures-ZfiCKJ_J.js → lspLanguageFeatures-C5jWIiYu.js} +1 -1
- package/client/dist/assets/{mdx-DWmYYep4.js → mdx-DJw_6jAn.js} +1 -1
- package/client/dist/assets/{mermaid.core-BlcsAN4s.js → mermaid.core-CcwuCXLB.js} +5 -5
- package/client/dist/assets/{mindmap-definition-FBJOCRG2-D01B1gH8.js → mindmap-definition-FBJOCRG2-JL1NZYBY.js} +1 -1
- package/client/dist/assets/{pegDiagram-XKGWAZYB-DhdSTBt5.js → pegDiagram-XKGWAZYB-DS69_m-N.js} +1 -1
- package/client/dist/assets/{pieDiagram-E7YTZNPT-hVSzmo7b.js → pieDiagram-E7YTZNPT-DkXpPONo.js} +1 -1
- package/client/dist/assets/{python-AbIuqkub.js → python-Z3BmknC5.js} +1 -1
- package/client/dist/assets/{quadrantDiagram-AXDQQJYC-CVexQ_Dn.js → quadrantDiagram-AXDQQJYC-DkkBvxWI.js} +1 -1
- package/client/dist/assets/{railroadDiagram-O6MQD6OU-BPCMbhAx.js → railroadDiagram-O6MQD6OU-DCC05goQ.js} +1 -1
- package/client/dist/assets/{razor-PLr1HLk0.js → razor-CQA_N38O.js} +1 -1
- package/client/dist/assets/{requirementDiagram-EFPCY7ZU-EntABUqc.js → requirementDiagram-EFPCY7ZU-BwD-Jp5O.js} +1 -1
- package/client/dist/assets/{sankeyDiagram-P5KCCOFB-D68oih_M.js → sankeyDiagram-P5KCCOFB-GlrDZlAs.js} +1 -1
- package/client/dist/assets/{sequenceDiagram-WJ2MYXX4-DWzaT5vI.js → sequenceDiagram-WJ2MYXX4-u5NKUnHb.js} +1 -1
- package/client/dist/assets/{sizeCapture-X5ZJPWSS-HbYBaSbU.js → sizeCapture-X5ZJPWSS-DWoXbO58.js} +1 -1
- package/client/dist/assets/{stateDiagram-HBIQ2CUA-BteRSnYL.js → stateDiagram-HBIQ2CUA-Bm1dFyez.js} +1 -1
- package/client/dist/assets/stateDiagram-v2-4QOOHH4V-DjQoyKBb.js +1 -0
- package/client/dist/assets/{swimlanes-XN3QIQJK-BAvVt5mV.js → swimlanes-XN3QIQJK-DUNKblS5.js} +1 -1
- package/client/dist/assets/swimlanesDiagram-VK2B7HYN-7MB5gr5J.js +8 -0
- package/client/dist/assets/{timeline-definition-24CTP7MA-D6yKUiYl.js → timeline-definition-24CTP7MA-toWxO6Z2.js} +1 -1
- package/client/dist/assets/{tsMode-DrakxHLR.js → tsMode-DZAgDpaL.js} +1 -1
- package/client/dist/assets/{typescript-DcMOCMAD.js → typescript-BGesFGk_.js} +1 -1
- package/client/dist/assets/{useTerminal-Dhk5K03J.js → useTerminal-BcxidJuH.js} +1 -1
- package/client/dist/assets/{vennDiagram-4TSXK5OY-CGU61Pyq.js → vennDiagram-4TSXK5OY-BkCayCPN.js} +1 -1
- package/client/dist/assets/{wardleyDiagram-VM6X3IG4-BrTe4Lxv.js → wardleyDiagram-VM6X3IG4-BqKWO2eU.js} +1 -1
- package/client/dist/assets/{workers-CqVatS52.js → workers-CPOEug1v.js} +1 -1
- package/client/dist/assets/{xml-xUSiNagm.js → xml-BLpTRmDm.js} +1 -1
- package/client/dist/assets/{xychartDiagram-S5SC5T6Z-CnMmHAmD.js → xychartDiagram-S5SC5T6Z-BIAgMyo_.js} +1 -1
- package/client/dist/assets/{yaml-CSavWraE.js → yaml-PqYnl7Hj.js} +1 -1
- package/client/dist/index.html +2 -2
- package/client/dist/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/78-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/78-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Add-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Add-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/B5pc-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/B5pc-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/CNS1-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/CNS1-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/CNS2-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/CNS2-V.bcmap +3 -0
- package/client/dist/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
- package/client/dist/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Ext-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Ext-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GB-H.bcmap +4 -0
- package/client/dist/pdfjs/cmaps/GB-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBT-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBT-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Hankaku.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Hiragana.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Katakana.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/LICENSE +36 -0
- package/client/dist/pdfjs/cmaps/NWP-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/NWP-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/Roman.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/V.bcmap +0 -0
- package/client/dist/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
- package/package.json +1 -1
- package/server/dist/index.js +1232 -75
- package/client/dist/assets/SettingsDialog-BTyTnZYM.css +0 -1
- package/client/dist/assets/SettingsDialog-VlpollL1.js +0 -2
- package/client/dist/assets/channel-CROBmx9n.js +0 -1
- package/client/dist/assets/classDiagram-DTDB5LWJ-D2KSupgp.js +0 -1
- package/client/dist/assets/classDiagram-v2-JRS7N3AN-D2KSupgp.js +0 -1
- package/client/dist/assets/index-C1R3qRwd.js +0 -407
- package/client/dist/assets/stateDiagram-v2-4QOOHH4V-CN8Njcij.js +0 -1
- package/client/dist/assets/swimlanesDiagram-VK2B7HYN-p55moRoN.js +0 -8
package/server/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { randomBytes as
|
|
2
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
3
3
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
4
4
|
import { spawn as spawn3 } from "node:child_process";
|
|
5
5
|
import * as os9 from "node:os";
|
|
@@ -53,8 +53,8 @@ function configuredAllowedRoots() {
|
|
|
53
53
|
return raw.split(",").map((root) => root.trim()).filter(Boolean);
|
|
54
54
|
}
|
|
55
55
|
function isInsideRoot(candidate, root) {
|
|
56
|
-
const
|
|
57
|
-
return
|
|
56
|
+
const relative4 = path2.relative(root, candidate);
|
|
57
|
+
return relative4 === "" || !relative4.startsWith("..") && !path2.isAbsolute(relative4);
|
|
58
58
|
}
|
|
59
59
|
async function realpathIfExists(candidate) {
|
|
60
60
|
try {
|
|
@@ -101,6 +101,23 @@ async function resolveAllowedPath(requestedPath) {
|
|
|
101
101
|
}
|
|
102
102
|
return resolvedPath;
|
|
103
103
|
}
|
|
104
|
+
async function resolveAllowedExistingPath(requestedPath) {
|
|
105
|
+
const resolvedPath = path2.resolve(expandHomePath(requestedPath));
|
|
106
|
+
if (isPathCheckDisabled()) return fs.realpath(resolvedPath);
|
|
107
|
+
const configuredRoots = configuredAllowedRoots().map((root) => path2.resolve(expandHomePath(root)));
|
|
108
|
+
const matchedRoot = configuredRoots.find((root) => isInsideRoot(resolvedPath, root));
|
|
109
|
+
if (!matchedRoot) throw new PathAccessError(resolvedPath);
|
|
110
|
+
const relativePath = path2.relative(matchedRoot, resolvedPath);
|
|
111
|
+
if (relativePath.startsWith("..") || path2.isAbsolute(relativePath)) {
|
|
112
|
+
throw new PathAccessError(resolvedPath);
|
|
113
|
+
}
|
|
114
|
+
const realPath = await fs.realpath(resolvedPath);
|
|
115
|
+
const roots = await allowedRoots();
|
|
116
|
+
if (!roots.some((root) => isInsideRoot(realPath, root))) {
|
|
117
|
+
throw new PathAccessError(resolvedPath);
|
|
118
|
+
}
|
|
119
|
+
return realPath;
|
|
120
|
+
}
|
|
104
121
|
|
|
105
122
|
// src/services/project-mover.ts
|
|
106
123
|
var ProjectMover = class {
|
|
@@ -1762,11 +1779,11 @@ function sendError(reply, error) {
|
|
|
1762
1779
|
// src/routes/files.ts
|
|
1763
1780
|
import { spawn } from "child_process";
|
|
1764
1781
|
import { isUtf8 } from "buffer";
|
|
1765
|
-
import { createReadStream as createReadStream2 } from "fs";
|
|
1782
|
+
import { constants as fsConstants, createReadStream as createReadStream2 } from "fs";
|
|
1766
1783
|
import * as fs2 from "fs/promises";
|
|
1767
1784
|
import * as path3 from "path";
|
|
1768
1785
|
import { ZipArchive } from "archiver";
|
|
1769
|
-
import {
|
|
1786
|
+
import { globIterate } from "glob";
|
|
1770
1787
|
|
|
1771
1788
|
// src/utils/archive-preview.ts
|
|
1772
1789
|
import { createReadStream } from "node:fs";
|
|
@@ -1882,6 +1899,13 @@ function listTarEntries(filePath, gzipped) {
|
|
|
1882
1899
|
}
|
|
1883
1900
|
|
|
1884
1901
|
// src/routes/files.ts
|
|
1902
|
+
var MAX_FILE_TREE_DEPTH = 10;
|
|
1903
|
+
var MAX_FILE_TREE_NODES = 5e3;
|
|
1904
|
+
var FILE_TREE_TIMEOUT_MS = 5e3;
|
|
1905
|
+
var MAX_TEXT_FILE_BYTES = 10 * 1024 * 1024;
|
|
1906
|
+
var MAX_FILE_SEARCH_RESULTS = 5e3;
|
|
1907
|
+
var FILE_SEARCH_TIMEOUT_MS = 5e3;
|
|
1908
|
+
var FILE_SEARCH_IGNORES = ["**/.git/**", "**/node_modules/**"];
|
|
1885
1909
|
var imageMimeTypes = /* @__PURE__ */ new Map([
|
|
1886
1910
|
[".apng", "image/apng"],
|
|
1887
1911
|
[".avif", "image/avif"],
|
|
@@ -1995,7 +2019,7 @@ async function createTreeNode(dirPath, entry) {
|
|
|
1995
2019
|
const rawTarget = await fs2.readlink(fullPath);
|
|
1996
2020
|
linkTarget = path3.isAbsolute(rawTarget) ? rawTarget : path3.resolve(dirPath, rawTarget);
|
|
1997
2021
|
try {
|
|
1998
|
-
const targetStats = await fs2.stat(fullPath);
|
|
2022
|
+
const targetStats = await fs2.stat(await resolveAllowedExistingPath(fullPath));
|
|
1999
2023
|
if (targetStats.isDirectory()) {
|
|
2000
2024
|
isDirectory = true;
|
|
2001
2025
|
targetType = "directory";
|
|
@@ -2020,29 +2044,41 @@ async function createTreeNode(dirPath, entry) {
|
|
|
2020
2044
|
}
|
|
2021
2045
|
return node;
|
|
2022
2046
|
}
|
|
2023
|
-
async function buildFileTree(dirPath, depth, currentDepth
|
|
2024
|
-
includeHidden: false,
|
|
2025
|
-
excludeNames: /* @__PURE__ */ new Set(["node_modules"]),
|
|
2026
|
-
filterType: "all",
|
|
2027
|
-
sort: "name"
|
|
2028
|
-
}) {
|
|
2047
|
+
async function buildFileTree(dirPath, depth, currentDepth, options, budget) {
|
|
2029
2048
|
if (currentDepth >= depth) return [];
|
|
2030
|
-
const entries = await fs2.readdir(dirPath, { withFileTypes: true });
|
|
2031
2049
|
const nodes = [];
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2050
|
+
const accessDirPath = await resolveAllowedExistingPath(dirPath);
|
|
2051
|
+
const directory = await fs2.opendir(accessDirPath);
|
|
2052
|
+
try {
|
|
2053
|
+
let entry = await directory.read();
|
|
2054
|
+
while (entry !== null) {
|
|
2055
|
+
if (budget.remainingNodes <= 0 || Date.now() >= budget.deadline) {
|
|
2056
|
+
budget.truncated = true;
|
|
2057
|
+
break;
|
|
2058
|
+
}
|
|
2059
|
+
if (!options.includeHidden && entry.name.startsWith(".")) {
|
|
2060
|
+
entry = await directory.read();
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
if (options.excludeNames.has(entry.name)) {
|
|
2064
|
+
entry = await directory.read();
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
const node = await createTreeNode(dirPath, entry);
|
|
2068
|
+
if (options.sort === "modified") {
|
|
2069
|
+
node.mtime = (await fs2.lstat(path3.join(accessDirPath, entry.name))).mtimeMs;
|
|
2070
|
+
}
|
|
2071
|
+
if (options.filterType === "all" || options.filterType === node.type) {
|
|
2072
|
+
budget.remainingNodes--;
|
|
2073
|
+
if (node.type === "directory" && !node.isSymlink) {
|
|
2074
|
+
node.children = await buildFileTree(node.path, depth, currentDepth + 1, options, budget);
|
|
2075
|
+
}
|
|
2076
|
+
nodes.push(node);
|
|
2077
|
+
}
|
|
2078
|
+
entry = await directory.read();
|
|
2044
2079
|
}
|
|
2045
|
-
|
|
2080
|
+
} finally {
|
|
2081
|
+
await directory.close();
|
|
2046
2082
|
}
|
|
2047
2083
|
return nodes.sort((a, b) => {
|
|
2048
2084
|
if (options.sort === "modified") {
|
|
@@ -2053,7 +2089,7 @@ async function buildFileTree(dirPath, depth, currentDepth = 0, options = {
|
|
|
2053
2089
|
});
|
|
2054
2090
|
}
|
|
2055
2091
|
async function fileRoutes(app) {
|
|
2056
|
-
app.get("/tree", async (req) => {
|
|
2092
|
+
app.get("/tree", async (req, reply) => {
|
|
2057
2093
|
const {
|
|
2058
2094
|
path: dirPath,
|
|
2059
2095
|
depth,
|
|
@@ -2062,21 +2098,31 @@ async function fileRoutes(app) {
|
|
|
2062
2098
|
exclude,
|
|
2063
2099
|
sort
|
|
2064
2100
|
} = req.query;
|
|
2101
|
+
const requestedDepth = depth === void 0 ? 3 : Number(depth);
|
|
2102
|
+
if (!Number.isInteger(requestedDepth) || requestedDepth < 1 || requestedDepth > MAX_FILE_TREE_DEPTH) {
|
|
2103
|
+
return reply.code(400).send({ error: `depth must be an integer between 1 and ${MAX_FILE_TREE_DEPTH}` });
|
|
2104
|
+
}
|
|
2065
2105
|
const resolvedPath = await resolveAllowedPath(dirPath);
|
|
2066
2106
|
const excludeNames = new Set(
|
|
2067
2107
|
(exclude === void 0 ? "node_modules" : exclude).split(",").map((name) => name.trim()).filter(Boolean)
|
|
2068
2108
|
);
|
|
2069
|
-
const
|
|
2109
|
+
const budget = {
|
|
2110
|
+
remainingNodes: MAX_FILE_TREE_NODES,
|
|
2111
|
+
deadline: Date.now() + FILE_TREE_TIMEOUT_MS,
|
|
2112
|
+
truncated: false
|
|
2113
|
+
};
|
|
2114
|
+
const tree = await buildFileTree(resolvedPath, requestedDepth, 0, {
|
|
2070
2115
|
includeHidden: parseBoolean(hidden, false),
|
|
2071
2116
|
excludeNames,
|
|
2072
2117
|
filterType: parseFilterType(type),
|
|
2073
2118
|
sort: parseTreeSort(sort)
|
|
2074
|
-
});
|
|
2119
|
+
}, budget);
|
|
2075
2120
|
const parentPath = path3.dirname(resolvedPath);
|
|
2076
2121
|
return {
|
|
2077
2122
|
path: resolvedPath,
|
|
2078
2123
|
parentPath: parentPath === resolvedPath ? null : parentPath,
|
|
2079
|
-
tree
|
|
2124
|
+
tree,
|
|
2125
|
+
truncated: budget.truncated
|
|
2080
2126
|
};
|
|
2081
2127
|
});
|
|
2082
2128
|
app.get("/download", async (req, reply) => {
|
|
@@ -2119,9 +2165,11 @@ async function fileRoutes(app) {
|
|
|
2119
2165
|
app.get("/read", async (req, reply) => {
|
|
2120
2166
|
const { path: filePath } = req.query;
|
|
2121
2167
|
const resolvedPath = await resolveAllowedPath(filePath);
|
|
2168
|
+
let accessPath;
|
|
2122
2169
|
let stats;
|
|
2123
2170
|
try {
|
|
2124
|
-
|
|
2171
|
+
accessPath = await resolveAllowedExistingPath(resolvedPath);
|
|
2172
|
+
stats = await fs2.stat(accessPath);
|
|
2125
2173
|
} catch (error) {
|
|
2126
2174
|
if (error.code === "ENOENT") {
|
|
2127
2175
|
return reply.code(404).send({ error: "File not found", path: resolvedPath });
|
|
@@ -2133,7 +2181,7 @@ async function fileRoutes(app) {
|
|
|
2133
2181
|
try {
|
|
2134
2182
|
return {
|
|
2135
2183
|
path: resolvedPath,
|
|
2136
|
-
content: await archivePreview(
|
|
2184
|
+
content: await archivePreview(accessPath),
|
|
2137
2185
|
kind: "archive",
|
|
2138
2186
|
mtime: stats.mtimeMs
|
|
2139
2187
|
};
|
|
@@ -2165,24 +2213,52 @@ async function fileRoutes(app) {
|
|
|
2165
2213
|
mtime: stats.mtimeMs
|
|
2166
2214
|
});
|
|
2167
2215
|
}
|
|
2168
|
-
let
|
|
2216
|
+
let handle;
|
|
2169
2217
|
try {
|
|
2170
|
-
|
|
2218
|
+
handle = await fs2.open(accessPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
2219
|
+
const openedStats = await handle.stat();
|
|
2220
|
+
if (openedStats.size > MAX_TEXT_FILE_BYTES) {
|
|
2221
|
+
return reply.code(413).send({
|
|
2222
|
+
error: "File is too large to open as text",
|
|
2223
|
+
maxBytes: MAX_TEXT_FILE_BYTES,
|
|
2224
|
+
path: resolvedPath
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
const chunks = [];
|
|
2228
|
+
let totalBytes = 0;
|
|
2229
|
+
while (true) {
|
|
2230
|
+
const remainingBytes = MAX_TEXT_FILE_BYTES + 1 - totalBytes;
|
|
2231
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, remainingBytes));
|
|
2232
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, null);
|
|
2233
|
+
if (bytesRead === 0) break;
|
|
2234
|
+
totalBytes += bytesRead;
|
|
2235
|
+
if (totalBytes > MAX_TEXT_FILE_BYTES) {
|
|
2236
|
+
return reply.code(413).send({
|
|
2237
|
+
error: "File is too large to open as text",
|
|
2238
|
+
maxBytes: MAX_TEXT_FILE_BYTES,
|
|
2239
|
+
path: resolvedPath
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
2243
|
+
}
|
|
2244
|
+
const content = Buffer.concat(chunks, totalBytes);
|
|
2245
|
+
if (isBinaryFile(resolvedPath, content)) {
|
|
2246
|
+
return reply.code(415).send({
|
|
2247
|
+
error: "Unsupported file type",
|
|
2248
|
+
kind: "binary",
|
|
2249
|
+
path: resolvedPath,
|
|
2250
|
+
mtime: openedStats.mtimeMs
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
return { path: resolvedPath, content: content.toString("utf-8"), mtime: openedStats.mtimeMs };
|
|
2171
2254
|
} catch (error) {
|
|
2172
2255
|
if (error.code === "ENOENT") {
|
|
2173
2256
|
return reply.code(404).send({ error: "File not found", path: resolvedPath });
|
|
2174
2257
|
}
|
|
2175
2258
|
throw error;
|
|
2259
|
+
} finally {
|
|
2260
|
+
await handle?.close();
|
|
2176
2261
|
}
|
|
2177
|
-
if (isBinaryFile(resolvedPath, content)) {
|
|
2178
|
-
return reply.code(415).send({
|
|
2179
|
-
error: "Unsupported file type",
|
|
2180
|
-
kind: "binary",
|
|
2181
|
-
path: resolvedPath,
|
|
2182
|
-
mtime: stats.mtimeMs
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
return { path: resolvedPath, content: content.toString("utf-8"), mtime: stats.mtimeMs };
|
|
2186
2262
|
});
|
|
2187
2263
|
app.get("/preview-asset", async (req, reply) => {
|
|
2188
2264
|
const { root, path: relativePath } = req.query;
|
|
@@ -2223,14 +2299,22 @@ async function fileRoutes(app) {
|
|
|
2223
2299
|
if (!imageMimeType && !isPdf) {
|
|
2224
2300
|
return reply.code(415).send({ error: "Unsupported file type" });
|
|
2225
2301
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2302
|
+
let handle;
|
|
2303
|
+
try {
|
|
2304
|
+
const accessPath = await resolveAllowedExistingPath(resolvedPath);
|
|
2305
|
+
handle = await fs2.open(accessPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
2306
|
+
const stream = handle.createReadStream();
|
|
2307
|
+
handle = void 0;
|
|
2308
|
+
if (isPdf) {
|
|
2309
|
+
return reply.type("application/pdf").send(stream);
|
|
2310
|
+
}
|
|
2311
|
+
if (imageMimeType === "image/svg+xml") {
|
|
2312
|
+
reply.header("Content-Security-Policy", "sandbox; default-src 'none'");
|
|
2313
|
+
}
|
|
2314
|
+
return reply.type(imageMimeType).send(stream);
|
|
2315
|
+
} finally {
|
|
2316
|
+
await handle?.close();
|
|
2232
2317
|
}
|
|
2233
|
-
return reply.type(imageMimeType).send(content);
|
|
2234
2318
|
});
|
|
2235
2319
|
app.post("/write", async (req) => {
|
|
2236
2320
|
const { path: filePath, content } = req.body;
|
|
@@ -2384,11 +2468,45 @@ async function fileRoutes(app) {
|
|
|
2384
2468
|
});
|
|
2385
2469
|
return { success: true };
|
|
2386
2470
|
});
|
|
2387
|
-
app.get("/search", async (req) => {
|
|
2471
|
+
app.get("/search", async (req, reply) => {
|
|
2388
2472
|
const { pattern, path: searchPath } = req.query;
|
|
2473
|
+
if (!pattern?.trim()) {
|
|
2474
|
+
return reply.code(400).send({ error: "Search pattern is required" });
|
|
2475
|
+
}
|
|
2476
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
2477
|
+
const patternSegments = normalizedPattern.split("/");
|
|
2478
|
+
const unsafeMagic = ["{", "}", "[", "]"].some((character) => normalizedPattern.includes(character)) || /[?*+@!]\(/.test(normalizedPattern);
|
|
2479
|
+
const unsafeDotSegment = patternSegments.some((segment) => segment === ".." || segment.includes(".") && /^[.*?]+$/.test(segment));
|
|
2480
|
+
if (path3.isAbsolute(pattern) || unsafeMagic || unsafeDotSegment) {
|
|
2481
|
+
return reply.code(400).send({ error: "Search pattern must stay within the requested path" });
|
|
2482
|
+
}
|
|
2389
2483
|
const resolvedSearchPath = await resolveAllowedPath(searchPath || ".");
|
|
2390
|
-
const files =
|
|
2391
|
-
|
|
2484
|
+
const files = [];
|
|
2485
|
+
const signal = AbortSignal.timeout(FILE_SEARCH_TIMEOUT_MS);
|
|
2486
|
+
try {
|
|
2487
|
+
for await (const file of globIterate(normalizedPattern, {
|
|
2488
|
+
cwd: resolvedSearchPath,
|
|
2489
|
+
root: resolvedSearchPath,
|
|
2490
|
+
ignore: FILE_SEARCH_IGNORES,
|
|
2491
|
+
nodir: true,
|
|
2492
|
+
signal
|
|
2493
|
+
})) {
|
|
2494
|
+
const relativePath = path3.relative(resolvedSearchPath, path3.resolve(resolvedSearchPath, file));
|
|
2495
|
+
if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
|
|
2496
|
+
return reply.code(400).send({ error: "Search pattern must stay within the requested path" });
|
|
2497
|
+
}
|
|
2498
|
+
if (files.length === MAX_FILE_SEARCH_RESULTS) {
|
|
2499
|
+
return { files, truncated: true };
|
|
2500
|
+
}
|
|
2501
|
+
files.push(file);
|
|
2502
|
+
}
|
|
2503
|
+
} catch (error) {
|
|
2504
|
+
if (signal.aborted) {
|
|
2505
|
+
return reply.code(408).send({ error: "File search timed out" });
|
|
2506
|
+
}
|
|
2507
|
+
throw error;
|
|
2508
|
+
}
|
|
2509
|
+
return { files, truncated: false };
|
|
2392
2510
|
});
|
|
2393
2511
|
}
|
|
2394
2512
|
|
|
@@ -2674,6 +2792,205 @@ async function changelogRoutes(app) {
|
|
|
2674
2792
|
});
|
|
2675
2793
|
}
|
|
2676
2794
|
|
|
2795
|
+
// src/services/speech-to-text.ts
|
|
2796
|
+
var DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
2797
|
+
var DEFAULT_MODEL = "gpt-4o-mini-transcribe";
|
|
2798
|
+
var MAX_TRANSCRIPTION_ATTEMPTS = 3;
|
|
2799
|
+
var RETRY_DELAY_MS = 500;
|
|
2800
|
+
var MAX_AUDIO_BYTES = 25 * 1024 * 1024;
|
|
2801
|
+
function audioExtension(contentType) {
|
|
2802
|
+
if (contentType.includes("amr")) return "amr";
|
|
2803
|
+
if (contentType.includes("mp4")) return "m4a";
|
|
2804
|
+
if (contentType.includes("mpeg")) return "mp3";
|
|
2805
|
+
if (contentType.includes("ogg")) return "ogg";
|
|
2806
|
+
if (contentType.includes("wav")) return "wav";
|
|
2807
|
+
return "webm";
|
|
2808
|
+
}
|
|
2809
|
+
function isRetryableStatus(status) {
|
|
2810
|
+
return status === 429 || status >= 500;
|
|
2811
|
+
}
|
|
2812
|
+
function wait(ms) {
|
|
2813
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
2814
|
+
}
|
|
2815
|
+
function isSpeechToTextConfigured() {
|
|
2816
|
+
return Boolean(process.env.PI_CLOUD_STT_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim());
|
|
2817
|
+
}
|
|
2818
|
+
async function transcribeAudio(audio, contentType, options = {}) {
|
|
2819
|
+
const apiKey = process.env.PI_CLOUD_STT_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim();
|
|
2820
|
+
if (!apiKey) throw new Error("Speech-to-text is not configured.");
|
|
2821
|
+
const baseUrl = (process.env.PI_CLOUD_STT_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
2822
|
+
const model = process.env.PI_CLOUD_STT_MODEL?.trim() || DEFAULT_MODEL;
|
|
2823
|
+
const language = process.env.PI_CLOUD_STT_LANGUAGE?.trim();
|
|
2824
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
2825
|
+
const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
|
|
2826
|
+
const form = new FormData();
|
|
2827
|
+
form.append("file", new Blob([audio], { type: contentType }), `recording.${audioExtension(contentType)}`);
|
|
2828
|
+
form.append("model", model);
|
|
2829
|
+
if (language) form.append("language", language);
|
|
2830
|
+
for (let attempt = 1; attempt <= MAX_TRANSCRIPTION_ATTEMPTS; attempt += 1) {
|
|
2831
|
+
let response;
|
|
2832
|
+
try {
|
|
2833
|
+
response = await fetchImpl(`${baseUrl}/audio/transcriptions`, {
|
|
2834
|
+
method: "POST",
|
|
2835
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
2836
|
+
body: form
|
|
2837
|
+
});
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
if (attempt === MAX_TRANSCRIPTION_ATTEMPTS) {
|
|
2840
|
+
throw new Error("Unable to reach the speech-to-text provider.", { cause: error });
|
|
2841
|
+
}
|
|
2842
|
+
await wait(retryDelayMs * attempt);
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
const result = await response.json().catch(() => ({}));
|
|
2846
|
+
if (!response.ok) {
|
|
2847
|
+
if (isRetryableStatus(response.status) && attempt < MAX_TRANSCRIPTION_ATTEMPTS) {
|
|
2848
|
+
await wait(retryDelayMs * attempt);
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
throw new Error(typeof result.error?.message === "string" ? result.error.message : "Transcription failed.");
|
|
2852
|
+
}
|
|
2853
|
+
if (typeof result.text !== "string") throw new Error("The speech-to-text provider returned an invalid response.");
|
|
2854
|
+
return result.text;
|
|
2855
|
+
}
|
|
2856
|
+
throw new Error("Unable to reach the speech-to-text provider.");
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// src/services/text-to-speech.ts
|
|
2860
|
+
var DEFAULT_MODEL2 = "mlx-community/Kokoro-82M-bf16";
|
|
2861
|
+
var DEFAULT_VOICE = "af_heart";
|
|
2862
|
+
var DEFAULT_FORMAT = "wav";
|
|
2863
|
+
var MAX_SYNTHESIS_ATTEMPTS = 3;
|
|
2864
|
+
var RETRY_DELAY_MS2 = 500;
|
|
2865
|
+
var MAX_TTS_TEXT_LENGTH = 1e5;
|
|
2866
|
+
var FORMAT_CONTENT_TYPES = {
|
|
2867
|
+
aac: "audio/aac",
|
|
2868
|
+
flac: "audio/flac",
|
|
2869
|
+
mp3: "audio/mpeg",
|
|
2870
|
+
ogg: "audio/ogg",
|
|
2871
|
+
opus: "audio/ogg",
|
|
2872
|
+
pcm: "audio/L16",
|
|
2873
|
+
wav: "audio/wav"
|
|
2874
|
+
};
|
|
2875
|
+
function isRetryableStatus2(status) {
|
|
2876
|
+
return status === 429 || status >= 500;
|
|
2877
|
+
}
|
|
2878
|
+
function wait2(ms) {
|
|
2879
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
2880
|
+
}
|
|
2881
|
+
function languageCode(language) {
|
|
2882
|
+
if (language === "zh") return "z";
|
|
2883
|
+
if (language === "en") return "a";
|
|
2884
|
+
return language;
|
|
2885
|
+
}
|
|
2886
|
+
async function providerError(response) {
|
|
2887
|
+
const result = await response.json().catch(() => ({}));
|
|
2888
|
+
if (typeof result.error?.message === "string") return result.error.message;
|
|
2889
|
+
if (typeof result.detail === "string") return result.detail;
|
|
2890
|
+
return "Speech synthesis failed.";
|
|
2891
|
+
}
|
|
2892
|
+
function isTextToSpeechConfigured() {
|
|
2893
|
+
return Boolean(process.env.PI_CLOUD_TTS_BASE_URL?.trim());
|
|
2894
|
+
}
|
|
2895
|
+
async function synthesizeSpeech(text, options = {}) {
|
|
2896
|
+
const baseUrl = process.env.PI_CLOUD_TTS_BASE_URL?.trim().replace(/\/+$/, "");
|
|
2897
|
+
if (!baseUrl) throw new Error("Text-to-speech is not configured.");
|
|
2898
|
+
const model = process.env.PI_CLOUD_TTS_MODEL?.trim() || DEFAULT_MODEL2;
|
|
2899
|
+
const voice = process.env.PI_CLOUD_TTS_VOICE?.trim() || DEFAULT_VOICE;
|
|
2900
|
+
const language = process.env.PI_CLOUD_TTS_LANGUAGE?.trim();
|
|
2901
|
+
const format = process.env.PI_CLOUD_TTS_FORMAT?.trim().toLowerCase() || DEFAULT_FORMAT;
|
|
2902
|
+
const apiKey = process.env.PI_CLOUD_TTS_API_KEY?.trim();
|
|
2903
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
2904
|
+
const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS2;
|
|
2905
|
+
const body = {
|
|
2906
|
+
model,
|
|
2907
|
+
input: text,
|
|
2908
|
+
voice,
|
|
2909
|
+
response_format: format,
|
|
2910
|
+
...language ? { language } : {},
|
|
2911
|
+
...language ? { lang_code: languageCode(language) } : {}
|
|
2912
|
+
};
|
|
2913
|
+
for (let attempt = 1; attempt <= MAX_SYNTHESIS_ATTEMPTS; attempt += 1) {
|
|
2914
|
+
let response;
|
|
2915
|
+
try {
|
|
2916
|
+
response = await fetchImpl(`${baseUrl}/audio/speech`, {
|
|
2917
|
+
method: "POST",
|
|
2918
|
+
headers: {
|
|
2919
|
+
"Content-Type": "application/json",
|
|
2920
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
2921
|
+
},
|
|
2922
|
+
body: JSON.stringify(body)
|
|
2923
|
+
});
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
if (attempt === MAX_SYNTHESIS_ATTEMPTS) {
|
|
2926
|
+
throw new Error("Unable to reach the text-to-speech provider.", { cause: error });
|
|
2927
|
+
}
|
|
2928
|
+
await wait2(retryDelayMs * attempt);
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
if (!response.ok) {
|
|
2932
|
+
const message = await providerError(response);
|
|
2933
|
+
if (isRetryableStatus2(response.status) && attempt < MAX_SYNTHESIS_ATTEMPTS) {
|
|
2934
|
+
await wait2(retryDelayMs * attempt);
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
throw new Error(message);
|
|
2938
|
+
}
|
|
2939
|
+
const audio = Buffer.from(await response.arrayBuffer());
|
|
2940
|
+
if (audio.length === 0) throw new Error("The text-to-speech provider returned an empty response.");
|
|
2941
|
+
return {
|
|
2942
|
+
audio,
|
|
2943
|
+
contentType: response.headers.get("content-type")?.split(";")[0] || FORMAT_CONTENT_TYPES[format] || "application/octet-stream"
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
throw new Error("Unable to reach the text-to-speech provider.");
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// src/routes/speech.ts
|
|
2950
|
+
async function speechRoutes(app, options = {}) {
|
|
2951
|
+
app.addContentTypeParser(/^audio\//, { parseAs: "buffer", bodyLimit: MAX_AUDIO_BYTES }, (_req, body, done) => {
|
|
2952
|
+
done(null, body);
|
|
2953
|
+
});
|
|
2954
|
+
app.get("/status", async () => ({
|
|
2955
|
+
available: isSpeechToTextConfigured(),
|
|
2956
|
+
ttsAvailable: isTextToSpeechConfigured()
|
|
2957
|
+
}));
|
|
2958
|
+
app.post("/transcribe", async (req, reply) => {
|
|
2959
|
+
if (!isSpeechToTextConfigured()) {
|
|
2960
|
+
return reply.status(503).send({ error: "Speech-to-text is not configured." });
|
|
2961
|
+
}
|
|
2962
|
+
const contentType = req.headers["content-type"]?.split(";")[0]?.trim().toLowerCase() || "";
|
|
2963
|
+
if (!contentType.startsWith("audio/") || !Buffer.isBuffer(req.body) || req.body.length === 0) {
|
|
2964
|
+
return reply.status(400).send({ error: "A non-empty audio request body is required." });
|
|
2965
|
+
}
|
|
2966
|
+
try {
|
|
2967
|
+
return { text: await transcribeAudio(req.body, contentType, options) };
|
|
2968
|
+
} catch (error) {
|
|
2969
|
+
req.log.error({ err: error }, "Speech-to-text request failed");
|
|
2970
|
+
return reply.status(502).send({ error: error instanceof Error ? error.message : "Transcription failed." });
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
app.post("/synthesize", async (req, reply) => {
|
|
2974
|
+
if (!isTextToSpeechConfigured()) {
|
|
2975
|
+
return reply.status(503).send({ error: "Text-to-speech is not configured." });
|
|
2976
|
+
}
|
|
2977
|
+
const text = req.body?.text;
|
|
2978
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
2979
|
+
return reply.status(400).send({ error: "Non-empty text is required." });
|
|
2980
|
+
}
|
|
2981
|
+
if (text.length > MAX_TTS_TEXT_LENGTH) {
|
|
2982
|
+
return reply.status(413).send({ error: `Text must not exceed ${MAX_TTS_TEXT_LENGTH} characters.` });
|
|
2983
|
+
}
|
|
2984
|
+
try {
|
|
2985
|
+
const result = await synthesizeSpeech(text, options);
|
|
2986
|
+
return reply.type(result.contentType).send(result.audio);
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
req.log.error({ err: error }, "Text-to-speech request failed");
|
|
2989
|
+
return reply.status(502).send({ error: error instanceof Error ? error.message : "Speech synthesis failed." });
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2677
2994
|
// src/routes/git.ts
|
|
2678
2995
|
import { execFile } from "node:child_process";
|
|
2679
2996
|
import { createHash } from "node:crypto";
|
|
@@ -2798,6 +3115,17 @@ async function runGit(cwd, args, maxBuffer3 = MAX_SLASH_COMMAND_OUTPUT_BYTES) {
|
|
|
2798
3115
|
throw error;
|
|
2799
3116
|
}
|
|
2800
3117
|
}
|
|
3118
|
+
async function runGitNoIndex(cwd, args, maxBuffer3) {
|
|
3119
|
+
try {
|
|
3120
|
+
const { stdout } = await execFileAsync("git", ["diff", "--no-index", ...args], { cwd, maxBuffer: maxBuffer3 });
|
|
3121
|
+
return stdout;
|
|
3122
|
+
} catch (error) {
|
|
3123
|
+
if (isMaxBufferError(error)) throw new OversizedGitOutputError();
|
|
3124
|
+
const result = error;
|
|
3125
|
+
if (result.code === 1) return result.stdout || "";
|
|
3126
|
+
throw error;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
2801
3129
|
async function isGitRepository(cwd) {
|
|
2802
3130
|
try {
|
|
2803
3131
|
return (await runGit(cwd, ["rev-parse", "--is-inside-work-tree"])).trim() === "true";
|
|
@@ -2880,6 +3208,20 @@ function textFromAssistantMessage(message) {
|
|
|
2880
3208
|
function joinGitOutput(...parts) {
|
|
2881
3209
|
return parts.map((part) => part.replace(/^(?:\r?\n)+|(?:\r?\n)+$/g, "")).filter((part) => part.trim()).join("\n\n");
|
|
2882
3210
|
}
|
|
3211
|
+
async function appendUntrackedDiff(cwd, diff, args, path13, maxBytes) {
|
|
3212
|
+
const pathArgs = path13 ? ["--", path13] : [];
|
|
3213
|
+
const untracked = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", ...pathArgs]);
|
|
3214
|
+
const files = untracked.split("\0").filter(Boolean);
|
|
3215
|
+
let output = diff;
|
|
3216
|
+
for (const file of files) {
|
|
3217
|
+
const separatorBytes = output.trim() ? 2 : 0;
|
|
3218
|
+
const remainingBytes = maxBytes - Buffer.byteLength(output) - separatorBytes;
|
|
3219
|
+
if (remainingBytes <= 0) throw new OversizedGitOutputError();
|
|
3220
|
+
const fileDiff = await runGitNoIndex(cwd, [...args, "--", "/dev/null", file], remainingBytes);
|
|
3221
|
+
output = joinGitOutput(output, fileDiff);
|
|
3222
|
+
}
|
|
3223
|
+
return output;
|
|
3224
|
+
}
|
|
2883
3225
|
async function getCombinedDiff(cwd, args, maxBytes = MAX_SLASH_COMMAND_OUTPUT_BYTES) {
|
|
2884
3226
|
const unstaged = await runGit(cwd, ["diff", ...args], maxBytes);
|
|
2885
3227
|
const remainingBytes = maxBytes - Buffer.byteLength(unstaged);
|
|
@@ -3382,22 +3724,28 @@ async function gitRoutes(app, options = {}) {
|
|
|
3382
3724
|
}
|
|
3383
3725
|
});
|
|
3384
3726
|
app.get("/diff", async (req, reply) => {
|
|
3385
|
-
const { cwd, scope: rawScope, commit: rawCommit, path: path13 } = req.query;
|
|
3727
|
+
const { cwd, scope: rawScope, commit: rawCommit, path: path13, includeUntracked } = req.query;
|
|
3386
3728
|
const resolvedCwd = await resolveGitCwd(cwd);
|
|
3387
3729
|
try {
|
|
3388
3730
|
const commit = rawCommit ? parseCommit(rawCommit) : void 0;
|
|
3389
3731
|
if (commit) {
|
|
3390
3732
|
const diff2 = await runGit(resolvedCwd, ["show", "--format=", "--patch", commit]);
|
|
3391
3733
|
const remainingBytes2 = MAX_SLASH_COMMAND_OUTPUT_BYTES - Buffer.byteLength(diff2);
|
|
3392
|
-
const
|
|
3393
|
-
return { cwd: resolvedCwd, scope: `commit-${commit}`, commit, stat:
|
|
3734
|
+
const stat10 = remainingBytes2 > 0 ? await runGit(resolvedCwd, ["show", "--format=", "--stat", commit], remainingBytes2) : "";
|
|
3735
|
+
return { cwd: resolvedCwd, scope: `commit-${commit}`, commit, stat: stat10, diff: diff2 };
|
|
3394
3736
|
}
|
|
3395
3737
|
const scope = parseDiffScope(rawScope);
|
|
3396
3738
|
const pathArgs = path13 ? ["--", path13] : [];
|
|
3397
|
-
|
|
3739
|
+
let diff = await getDiff(resolvedCwd, pathArgs, scope);
|
|
3740
|
+
if (includeUntracked === "true" && scope !== "staged") {
|
|
3741
|
+
diff = await appendUntrackedDiff(resolvedCwd, diff, [], path13, MAX_SLASH_COMMAND_OUTPUT_BYTES);
|
|
3742
|
+
}
|
|
3398
3743
|
const remainingBytes = MAX_SLASH_COMMAND_OUTPUT_BYTES - Buffer.byteLength(diff);
|
|
3399
|
-
|
|
3400
|
-
|
|
3744
|
+
let stat9 = remainingBytes > 0 ? await getDiff(resolvedCwd, ["--stat", ...pathArgs], scope, remainingBytes) : "";
|
|
3745
|
+
if (includeUntracked === "true" && scope !== "staged" && remainingBytes > 0) {
|
|
3746
|
+
stat9 = await appendUntrackedDiff(resolvedCwd, stat9, ["--stat"], path13, remainingBytes);
|
|
3747
|
+
}
|
|
3748
|
+
return { cwd: resolvedCwd, scope, path: path13, stat: stat9, diff };
|
|
3401
3749
|
} catch (error) {
|
|
3402
3750
|
if (error instanceof OversizedGitOutputError) {
|
|
3403
3751
|
return {
|
|
@@ -3914,6 +4262,65 @@ function sendGatewayError(reply, error) {
|
|
|
3914
4262
|
return reply.status(400).send({ error: message });
|
|
3915
4263
|
}
|
|
3916
4264
|
|
|
4265
|
+
// src/routes/wecom-gateway.ts
|
|
4266
|
+
async function wecomGatewayRoutes(app, options) {
|
|
4267
|
+
app.addContentTypeParser(["application/xml", "text/xml"], { parseAs: "string" }, (_req, body, done) => done(null, body));
|
|
4268
|
+
app.get("/callback", async (req, reply) => {
|
|
4269
|
+
try {
|
|
4270
|
+
const challenge = options.service.handleVerification(req.query);
|
|
4271
|
+
return reply.type("text/plain; charset=utf-8").send(challenge);
|
|
4272
|
+
} catch (error) {
|
|
4273
|
+
return sendWecomError(reply, error);
|
|
4274
|
+
}
|
|
4275
|
+
});
|
|
4276
|
+
app.post("/callback", async (req, reply) => {
|
|
4277
|
+
try {
|
|
4278
|
+
const acknowledgement = options.service.handleCallback(
|
|
4279
|
+
req.query,
|
|
4280
|
+
typeof req.body === "string" ? req.body : ""
|
|
4281
|
+
);
|
|
4282
|
+
return reply.type("text/plain; charset=utf-8").send(acknowledgement);
|
|
4283
|
+
} catch (error) {
|
|
4284
|
+
return sendWecomError(reply, error);
|
|
4285
|
+
}
|
|
4286
|
+
});
|
|
4287
|
+
app.get("/status", async () => ({ status: options.service.status() }));
|
|
4288
|
+
app.put("/configuration", async (req, reply) => {
|
|
4289
|
+
try {
|
|
4290
|
+
return options.service.saveConfiguration(req.body);
|
|
4291
|
+
} catch (error) {
|
|
4292
|
+
return sendWecomError(reply, error);
|
|
4293
|
+
}
|
|
4294
|
+
});
|
|
4295
|
+
app.delete("/configuration", async (_req, reply) => {
|
|
4296
|
+
try {
|
|
4297
|
+
options.service.disconnect();
|
|
4298
|
+
return { status: options.service.status() };
|
|
4299
|
+
} catch (error) {
|
|
4300
|
+
return sendWecomError(reply, error);
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
app.post("/callback-secrets", async (_req, reply) => {
|
|
4304
|
+
try {
|
|
4305
|
+
return options.service.regenerateCallbackSecrets();
|
|
4306
|
+
} catch (error) {
|
|
4307
|
+
return sendWecomError(reply, error);
|
|
4308
|
+
}
|
|
4309
|
+
});
|
|
4310
|
+
app.post("/test", async (_req, reply) => {
|
|
4311
|
+
try {
|
|
4312
|
+
return { status: await options.service.testConnection() };
|
|
4313
|
+
} catch (error) {
|
|
4314
|
+
return sendWecomError(reply, error);
|
|
4315
|
+
}
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
function sendWecomError(reply, error) {
|
|
4319
|
+
const statusCode = Number(error?.statusCode) || 400;
|
|
4320
|
+
const message = error instanceof Error ? error.message : "WeCom gateway request failed";
|
|
4321
|
+
return reply.status(statusCode).send({ error: message });
|
|
4322
|
+
}
|
|
4323
|
+
|
|
3917
4324
|
// src/routes/git-hosting.ts
|
|
3918
4325
|
import { execFile as execFile2 } from "node:child_process";
|
|
3919
4326
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -4742,7 +5149,8 @@ async function authRoutes(app, options) {
|
|
|
4742
5149
|
autoExtractMemory: getPreferenceValue("memory.autoExtract") === "true",
|
|
4743
5150
|
theme: getTheme(),
|
|
4744
5151
|
language: getLanguage(),
|
|
4745
|
-
soundNotification: getSoundNotification()
|
|
5152
|
+
soundNotification: getSoundNotification(),
|
|
5153
|
+
autoSpeakAssistant: getPreferenceValue("ui.autoSpeakAssistant") === "true"
|
|
4746
5154
|
};
|
|
4747
5155
|
}
|
|
4748
5156
|
function requireAuth(req, reply) {
|
|
@@ -4855,8 +5263,9 @@ async function authRoutes(app, options) {
|
|
|
4855
5263
|
const hasTheme = hasPreference(body, "theme");
|
|
4856
5264
|
const hasLanguage = hasPreference(body, "language");
|
|
4857
5265
|
const hasSoundNotification = hasPreference(body, "soundNotification");
|
|
5266
|
+
const hasAutoSpeakAssistant = hasPreference(body, "autoSpeakAssistant");
|
|
4858
5267
|
const hasGitCloneParentPath = hasPreference(body, "gitCloneParentPath");
|
|
4859
|
-
if (!hasShowHintInfo && !hasShowCodeBlockLanguageHeaders && !hasStreamingMessageBehavior && !hasEditorAutoRefresh && !hasConfirmSessionDelete && !hasNewSessionShortcut && !hasFullscreenShortcut && !hasShowGoToTopButton && !hasShowChatViewOptionsButton && !hasAutoExtractMemory && !hasTheme && !hasLanguage && !hasSoundNotification && !hasGitCloneParentPath) {
|
|
5268
|
+
if (!hasShowHintInfo && !hasShowCodeBlockLanguageHeaders && !hasStreamingMessageBehavior && !hasEditorAutoRefresh && !hasConfirmSessionDelete && !hasNewSessionShortcut && !hasFullscreenShortcut && !hasShowGoToTopButton && !hasShowChatViewOptionsButton && !hasAutoExtractMemory && !hasTheme && !hasLanguage && !hasSoundNotification && !hasAutoSpeakAssistant && !hasGitCloneParentPath) {
|
|
4860
5269
|
return reply.status(400).send({ error: "At least one preference must be provided" });
|
|
4861
5270
|
}
|
|
4862
5271
|
if (hasShowHintInfo && typeof body.showHintInfo !== "boolean") {
|
|
@@ -4898,6 +5307,9 @@ async function authRoutes(app, options) {
|
|
|
4898
5307
|
if (hasSoundNotification && body.soundNotification !== "off" && body.soundNotification !== "beep" && body.soundNotification !== "chime" && body.soundNotification !== "ding") {
|
|
4899
5308
|
return reply.status(400).send({ error: "soundNotification must be off, beep, chime, or ding" });
|
|
4900
5309
|
}
|
|
5310
|
+
if (hasAutoSpeakAssistant && typeof body.autoSpeakAssistant !== "boolean") {
|
|
5311
|
+
return reply.status(400).send({ error: "autoSpeakAssistant must be a boolean" });
|
|
5312
|
+
}
|
|
4901
5313
|
if (hasGitCloneParentPath && (typeof body.gitCloneParentPath !== "string" || !body.gitCloneParentPath.trim())) {
|
|
4902
5314
|
return reply.status(400).send({ error: "gitCloneParentPath must be a non-empty string" });
|
|
4903
5315
|
}
|
|
@@ -4914,6 +5326,7 @@ async function authRoutes(app, options) {
|
|
|
4914
5326
|
if (hasTheme) setPreferenceValue("ui.theme", String(body.theme));
|
|
4915
5327
|
if (hasLanguage) setPreferenceValue("ui.language", String(body.language));
|
|
4916
5328
|
if (hasSoundNotification) setPreferenceValue("ui.soundNotification", String(body.soundNotification));
|
|
5329
|
+
if (hasAutoSpeakAssistant) setPreferenceValue("ui.autoSpeakAssistant", String(body.autoSpeakAssistant));
|
|
4917
5330
|
if (hasGitCloneParentPath) setPreferenceValue("ui.gitCloneParentPath", String(body.gitCloneParentPath).trim());
|
|
4918
5331
|
return getPreferences();
|
|
4919
5332
|
});
|
|
@@ -6081,11 +6494,52 @@ var projectHistorySchemaMigration = {
|
|
|
6081
6494
|
}
|
|
6082
6495
|
};
|
|
6083
6496
|
|
|
6497
|
+
// src/db/migrations/004-wecom-gateway-schema.ts
|
|
6498
|
+
var wecomGatewaySchemaMigration = {
|
|
6499
|
+
version: 4,
|
|
6500
|
+
name: "wecom-gateway-schema",
|
|
6501
|
+
up(db) {
|
|
6502
|
+
db.exec(`
|
|
6503
|
+
CREATE TABLE IF NOT EXISTS wecom_gateway_credentials (
|
|
6504
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
6505
|
+
corp_id TEXT NOT NULL,
|
|
6506
|
+
corp_secret TEXT NOT NULL,
|
|
6507
|
+
agent_id TEXT NOT NULL,
|
|
6508
|
+
callback_token TEXT NOT NULL,
|
|
6509
|
+
encoding_aes_key TEXT NOT NULL,
|
|
6510
|
+
allowed_users_json TEXT NOT NULL DEFAULT '[]',
|
|
6511
|
+
callback_verified_at TEXT,
|
|
6512
|
+
created_at TEXT NOT NULL,
|
|
6513
|
+
updated_at TEXT NOT NULL
|
|
6514
|
+
);
|
|
6515
|
+
|
|
6516
|
+
CREATE TABLE IF NOT EXISTS wecom_gateway_sessions (
|
|
6517
|
+
session_key TEXT PRIMARY KEY,
|
|
6518
|
+
client_id TEXT NOT NULL,
|
|
6519
|
+
pi_session_id TEXT NOT NULL,
|
|
6520
|
+
created_at TEXT NOT NULL,
|
|
6521
|
+
updated_at TEXT NOT NULL
|
|
6522
|
+
);
|
|
6523
|
+
|
|
6524
|
+
CREATE TABLE IF NOT EXISTS wecom_gateway_configs (
|
|
6525
|
+
client_id TEXT PRIMARY KEY,
|
|
6526
|
+
agent_profile TEXT,
|
|
6527
|
+
default_cwd TEXT,
|
|
6528
|
+
skill_mode TEXT,
|
|
6529
|
+
skill_preset_id TEXT,
|
|
6530
|
+
created_at TEXT NOT NULL,
|
|
6531
|
+
updated_at TEXT NOT NULL
|
|
6532
|
+
);
|
|
6533
|
+
`);
|
|
6534
|
+
}
|
|
6535
|
+
};
|
|
6536
|
+
|
|
6084
6537
|
// src/db/migrations/index.ts
|
|
6085
6538
|
var migrations = [
|
|
6086
6539
|
applicationSchemaMigration,
|
|
6087
6540
|
gatewaySchemaMigration,
|
|
6088
|
-
projectHistorySchemaMigration
|
|
6541
|
+
projectHistorySchemaMigration,
|
|
6542
|
+
wecomGatewaySchemaMigration
|
|
6089
6543
|
];
|
|
6090
6544
|
function runDatabaseMigrations(db) {
|
|
6091
6545
|
db.exec(`
|
|
@@ -11431,7 +11885,7 @@ function createMemoryRuntime(options) {
|
|
|
11431
11885
|
|
|
11432
11886
|
// src/services/worktree-manager.ts
|
|
11433
11887
|
import { execFile as execFileCallback } from "child_process";
|
|
11434
|
-
import { copyFile, mkdir as mkdir5, readdir as
|
|
11888
|
+
import { copyFile, mkdir as mkdir5, readdir as readdir3 } from "fs/promises";
|
|
11435
11889
|
import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute3, join as join13 } from "path";
|
|
11436
11890
|
import { promisify as promisify4 } from "util";
|
|
11437
11891
|
var defaultExecFile = promisify4(execFileCallback);
|
|
@@ -11460,7 +11914,7 @@ var WorktreeManager = class {
|
|
|
11460
11914
|
const baseRepoPath = await this.getRepoRoot(projectPath).catch(() => void 0);
|
|
11461
11915
|
if (!baseRepoPath) return [];
|
|
11462
11916
|
const files = [];
|
|
11463
|
-
const entries = await
|
|
11917
|
+
const entries = await readdir3(baseRepoPath, { withFileTypes: true });
|
|
11464
11918
|
for (const entry of entries) {
|
|
11465
11919
|
if (!entry.isFile() || !this.isRootFileName(entry.name)) continue;
|
|
11466
11920
|
if (await this.isIgnored(baseRepoPath, entry.name)) files.push(entry.name);
|
|
@@ -12562,9 +13016,9 @@ var FeishuGatewayService = class {
|
|
|
12562
13016
|
console.warn("[feishu-gateway] failed to decrypt encrypted callback payload", {
|
|
12563
13017
|
reason: error instanceof Error ? error.message : "unknown error"
|
|
12564
13018
|
});
|
|
12565
|
-
const
|
|
12566
|
-
|
|
12567
|
-
throw
|
|
13019
|
+
const gatewayError2 = new Error("Invalid Feishu encrypted callback payload");
|
|
13020
|
+
gatewayError2.statusCode = 400;
|
|
13021
|
+
throw gatewayError2;
|
|
12568
13022
|
}
|
|
12569
13023
|
}
|
|
12570
13024
|
verifyToken(payload, config) {
|
|
@@ -14077,6 +14531,704 @@ function extractLastAssistantText2(messages) {
|
|
|
14077
14531
|
return void 0;
|
|
14078
14532
|
}
|
|
14079
14533
|
|
|
14534
|
+
// src/services/wecom-gateway.ts
|
|
14535
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
14536
|
+
import { stat as stat8 } from "node:fs/promises";
|
|
14537
|
+
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
14538
|
+
|
|
14539
|
+
// src/services/wecom-crypto.ts
|
|
14540
|
+
import { createDecipheriv as createDecipheriv3, createHash as createHash8, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
14541
|
+
function verifyWecomSignature(input) {
|
|
14542
|
+
const expected = createHash8("sha1").update([input.token, input.timestamp, input.nonce, input.encrypted].sort().join("")).digest();
|
|
14543
|
+
if (!/^[0-9a-f]{40}$/i.test(input.signature)) return false;
|
|
14544
|
+
const actual = Buffer.from(input.signature, "hex");
|
|
14545
|
+
return timingSafeEqual2(expected, actual);
|
|
14546
|
+
}
|
|
14547
|
+
function decryptWecomPayload(encrypted, encodingAesKey, expectedReceiveId) {
|
|
14548
|
+
const key = parseEncodingAesKey(encodingAesKey);
|
|
14549
|
+
const ciphertext = Buffer.from(encrypted, "base64");
|
|
14550
|
+
if (!ciphertext.length || ciphertext.length % 16 !== 0) throw new Error("Invalid WeCom encrypted payload");
|
|
14551
|
+
const decipher = createDecipheriv3("aes-256-cbc", key, key.subarray(0, 16));
|
|
14552
|
+
decipher.setAutoPadding(false);
|
|
14553
|
+
const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
14554
|
+
const plaintext = removePkcs7Padding(padded);
|
|
14555
|
+
if (plaintext.length < 20) throw new Error("Invalid WeCom decrypted payload");
|
|
14556
|
+
const messageLength = plaintext.readUInt32BE(16);
|
|
14557
|
+
const messageStart = 20;
|
|
14558
|
+
const messageEnd = messageStart + messageLength;
|
|
14559
|
+
if (messageEnd > plaintext.length) throw new Error("Invalid WeCom message length");
|
|
14560
|
+
const receiveId = plaintext.subarray(messageEnd).toString("utf8");
|
|
14561
|
+
if (receiveId !== expectedReceiveId) throw new Error("Invalid WeCom callback receive ID");
|
|
14562
|
+
return plaintext.subarray(messageStart, messageEnd).toString("utf8");
|
|
14563
|
+
}
|
|
14564
|
+
function parseEncodingAesKey(value) {
|
|
14565
|
+
if (!/^[A-Za-z0-9]{43}$/.test(value)) throw new Error("WeCom EncodingAESKey must contain 43 letters or digits");
|
|
14566
|
+
const key = Buffer.from(`${value}=`, "base64");
|
|
14567
|
+
if (key.length !== 32) throw new Error("Invalid WeCom EncodingAESKey");
|
|
14568
|
+
return key;
|
|
14569
|
+
}
|
|
14570
|
+
function removePkcs7Padding(value) {
|
|
14571
|
+
const padding = value[value.length - 1];
|
|
14572
|
+
if (!padding || padding > 32 || padding > value.length) throw new Error("Invalid WeCom PKCS#7 padding");
|
|
14573
|
+
for (let index = value.length - padding; index < value.length; index += 1) {
|
|
14574
|
+
if (value[index] !== padding) throw new Error("Invalid WeCom PKCS#7 padding");
|
|
14575
|
+
}
|
|
14576
|
+
return value.subarray(0, value.length - padding);
|
|
14577
|
+
}
|
|
14578
|
+
|
|
14579
|
+
// src/services/wecom-gateway.ts
|
|
14580
|
+
var WECOM_API_BASE_URL = "https://qyapi.weixin.qq.com";
|
|
14581
|
+
var TOKEN_REFRESH_SKEW_MS2 = 5 * 60 * 1e3;
|
|
14582
|
+
var DEDUP_TTL_MS3 = 5 * 60 * 1e3;
|
|
14583
|
+
var TEXT_CHUNK_MAX_BYTES = 1900;
|
|
14584
|
+
function generateEncodingAesKey() {
|
|
14585
|
+
while (true) {
|
|
14586
|
+
const key = randomBytes3(32).toString("base64").slice(0, 43);
|
|
14587
|
+
if (/^[A-Za-z0-9]+$/.test(key)) return key;
|
|
14588
|
+
}
|
|
14589
|
+
}
|
|
14590
|
+
var WecomGatewayService = class {
|
|
14591
|
+
constructor(db, gatewaySettings, sessionService) {
|
|
14592
|
+
this.db = db;
|
|
14593
|
+
this.sessionService = sessionService;
|
|
14594
|
+
this.gatewaySettings = gatewaySettings || new GatewaySettingsStore(db);
|
|
14595
|
+
this.presetStore = new SkillPresetStore(db);
|
|
14596
|
+
}
|
|
14597
|
+
db;
|
|
14598
|
+
sessionService;
|
|
14599
|
+
gatewaySettings;
|
|
14600
|
+
presetStore;
|
|
14601
|
+
seen = /* @__PURE__ */ new Map();
|
|
14602
|
+
queues = /* @__PURE__ */ new Map();
|
|
14603
|
+
tokenCache;
|
|
14604
|
+
environmentCallbackVerified = false;
|
|
14605
|
+
status() {
|
|
14606
|
+
const config = this.loadConfig();
|
|
14607
|
+
const managedBy = this.isEnvironmentManaged() ? "environment" : this.loadSavedCredentials() ? "database" : "none";
|
|
14608
|
+
return {
|
|
14609
|
+
configured: Boolean(config.corpId && config.corpSecret && config.agentId && config.callbackToken && config.encodingAesKey),
|
|
14610
|
+
managedBy,
|
|
14611
|
+
...config.corpId ? { corpId: config.corpId } : {},
|
|
14612
|
+
...config.agentId ? { agentId: config.agentId } : {},
|
|
14613
|
+
allowedUsers: config.allowedUsers,
|
|
14614
|
+
callbackVerified: managedBy === "database" ? Boolean(this.loadSavedCredentials()?.callbackVerified) : this.environmentCallbackVerified
|
|
14615
|
+
};
|
|
14616
|
+
}
|
|
14617
|
+
saveConfiguration(input) {
|
|
14618
|
+
if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
|
|
14619
|
+
const corpId = input.corpId.trim();
|
|
14620
|
+
const corpSecret = input.corpSecret.trim();
|
|
14621
|
+
const agentId = input.agentId.trim();
|
|
14622
|
+
if (!corpId || !corpSecret || !/^\d+$/.test(agentId)) throw new Error("Corp ID, app secret, and a numeric Agent ID are required");
|
|
14623
|
+
const allowedUsers = normalizeList(input.allowedUsers);
|
|
14624
|
+
const existing = this.loadSavedCredentials();
|
|
14625
|
+
const callbackToken = existing?.callbackToken || randomBytes3(16).toString("hex");
|
|
14626
|
+
const encodingAesKey = existing?.encodingAesKey || generateEncodingAesKey();
|
|
14627
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14628
|
+
this.db.prepare(`
|
|
14629
|
+
INSERT INTO wecom_gateway_credentials (
|
|
14630
|
+
id, corp_id, corp_secret, agent_id, callback_token, encoding_aes_key,
|
|
14631
|
+
allowed_users_json, callback_verified_at, created_at, updated_at
|
|
14632
|
+
) VALUES (1, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
14633
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
14634
|
+
corp_id = excluded.corp_id,
|
|
14635
|
+
corp_secret = excluded.corp_secret,
|
|
14636
|
+
agent_id = excluded.agent_id,
|
|
14637
|
+
callback_token = excluded.callback_token,
|
|
14638
|
+
encoding_aes_key = excluded.encoding_aes_key,
|
|
14639
|
+
allowed_users_json = excluded.allowed_users_json,
|
|
14640
|
+
callback_verified_at = NULL,
|
|
14641
|
+
updated_at = excluded.updated_at
|
|
14642
|
+
`).run(corpId, corpSecret, agentId, callbackToken, encodingAesKey, JSON.stringify(allowedUsers), now, now);
|
|
14643
|
+
this.tokenCache = void 0;
|
|
14644
|
+
return { status: this.status(), callbackToken, encodingAesKey };
|
|
14645
|
+
}
|
|
14646
|
+
regenerateCallbackSecrets() {
|
|
14647
|
+
if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
|
|
14648
|
+
const saved = this.loadSavedCredentials();
|
|
14649
|
+
if (!saved) throw new Error("Save the WeCom configuration first");
|
|
14650
|
+
const callbackToken = randomBytes3(16).toString("hex");
|
|
14651
|
+
const encodingAesKey = generateEncodingAesKey();
|
|
14652
|
+
this.db.prepare(`
|
|
14653
|
+
UPDATE wecom_gateway_credentials
|
|
14654
|
+
SET callback_token = ?, encoding_aes_key = ?, callback_verified_at = NULL, updated_at = ?
|
|
14655
|
+
WHERE id = 1
|
|
14656
|
+
`).run(callbackToken, encodingAesKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
14657
|
+
return { status: this.status(), callbackToken, encodingAesKey };
|
|
14658
|
+
}
|
|
14659
|
+
disconnect() {
|
|
14660
|
+
if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
|
|
14661
|
+
const clients = this.db.prepare("SELECT client_id FROM wecom_gateway_sessions").all();
|
|
14662
|
+
for (const { client_id: clientId } of clients) this.sessionService.disposeSession(clientId);
|
|
14663
|
+
this.db.exec("DELETE FROM wecom_gateway_sessions; DELETE FROM wecom_gateway_configs; DELETE FROM wecom_gateway_credentials;");
|
|
14664
|
+
this.tokenCache = void 0;
|
|
14665
|
+
}
|
|
14666
|
+
async testConnection() {
|
|
14667
|
+
const config = this.requireConfig();
|
|
14668
|
+
this.tokenCache = void 0;
|
|
14669
|
+
await this.getAccessToken(config);
|
|
14670
|
+
return this.status();
|
|
14671
|
+
}
|
|
14672
|
+
handleVerification(query) {
|
|
14673
|
+
const config = this.requireConfig();
|
|
14674
|
+
const encrypted = stringValue3(query.echostr);
|
|
14675
|
+
this.verifySignature(query, encrypted, config);
|
|
14676
|
+
const challenge = decryptWecomPayload(encrypted, config.encodingAesKey, config.corpId);
|
|
14677
|
+
this.markCallbackVerified();
|
|
14678
|
+
return challenge;
|
|
14679
|
+
}
|
|
14680
|
+
handleCallback(query, body) {
|
|
14681
|
+
const config = this.requireConfig();
|
|
14682
|
+
const encrypted = xmlValue(body, "Encrypt");
|
|
14683
|
+
if (!encrypted) throw gatewayError("Invalid WeCom callback envelope", 400);
|
|
14684
|
+
this.verifySignature(query, encrypted, config);
|
|
14685
|
+
const message = parseWecomMessage(decryptWecomPayload(encrypted, config.encodingAesKey, config.corpId));
|
|
14686
|
+
this.markCallbackVerified();
|
|
14687
|
+
if (!message || message.agentId !== config.agentId) return "success";
|
|
14688
|
+
if (config.allowedUsers.length && !config.allowedUsers.includes(message.userId)) return "success";
|
|
14689
|
+
if (this.markDuplicate(message.messageId)) return "success";
|
|
14690
|
+
if (!["text", "image", "voice"].includes(message.messageType)) {
|
|
14691
|
+
void this.sendReply(config, message.userId, "This WeCom gateway supports text, image, and voice messages only.").catch((error) => {
|
|
14692
|
+
console.warn("[wecom-gateway] unsupported-message reply failed:", error instanceof Error ? error.message : error);
|
|
14693
|
+
});
|
|
14694
|
+
return "success";
|
|
14695
|
+
}
|
|
14696
|
+
this.enqueue({
|
|
14697
|
+
messageId: message.messageId,
|
|
14698
|
+
userId: message.userId,
|
|
14699
|
+
text: message.text,
|
|
14700
|
+
messageType: message.messageType,
|
|
14701
|
+
mediaId: message.mediaId,
|
|
14702
|
+
picUrl: message.picUrl
|
|
14703
|
+
}, config);
|
|
14704
|
+
return "success";
|
|
14705
|
+
}
|
|
14706
|
+
verifySignature(query, encrypted, config) {
|
|
14707
|
+
const valid = encrypted && verifyWecomSignature({
|
|
14708
|
+
token: config.callbackToken,
|
|
14709
|
+
timestamp: stringValue3(query.timestamp),
|
|
14710
|
+
nonce: stringValue3(query.nonce),
|
|
14711
|
+
encrypted,
|
|
14712
|
+
signature: stringValue3(query.msg_signature)
|
|
14713
|
+
});
|
|
14714
|
+
if (!valid) throw gatewayError("Invalid WeCom callback signature", 401);
|
|
14715
|
+
}
|
|
14716
|
+
requireConfig() {
|
|
14717
|
+
const config = this.loadConfig();
|
|
14718
|
+
if (!config.corpId || !config.corpSecret || !config.agentId || !config.callbackToken || !config.encodingAesKey) {
|
|
14719
|
+
throw gatewayError("WeCom gateway is not configured", 503);
|
|
14720
|
+
}
|
|
14721
|
+
return config;
|
|
14722
|
+
}
|
|
14723
|
+
loadConfig() {
|
|
14724
|
+
const gatewaySettings = this.gatewaySettings.get();
|
|
14725
|
+
const saved = this.loadSavedCredentials();
|
|
14726
|
+
const skillset = this.resolveSkillsetConfig(gatewaySettings.defaultSkillset);
|
|
14727
|
+
return {
|
|
14728
|
+
corpId: process.env.PI_CLOUD_WECOM_CORP_ID?.trim() || saved?.corpId || "",
|
|
14729
|
+
corpSecret: process.env.PI_CLOUD_WECOM_CORP_SECRET?.trim() || saved?.corpSecret || "",
|
|
14730
|
+
agentId: process.env.PI_CLOUD_WECOM_AGENT_ID?.trim() || saved?.agentId || "",
|
|
14731
|
+
callbackToken: process.env.PI_CLOUD_WECOM_CALLBACK_TOKEN?.trim() || saved?.callbackToken || "",
|
|
14732
|
+
encodingAesKey: process.env.PI_CLOUD_WECOM_ENCODING_AES_KEY?.trim() || saved?.encodingAesKey || "",
|
|
14733
|
+
allowedUsers: process.env.PI_CLOUD_WECOM_ALLOWED_USERS !== void 0 ? parseList2(process.env.PI_CLOUD_WECOM_ALLOWED_USERS) : saved?.allowedUsers || [],
|
|
14734
|
+
defaultCwd: gatewaySettings.cwds[0],
|
|
14735
|
+
cwdChoices: gatewaySettings.cwds,
|
|
14736
|
+
agentProfile: gatewaySettings.defaultProfile,
|
|
14737
|
+
modelProvider: gatewaySettings.defaultModelProvider,
|
|
14738
|
+
modelId: gatewaySettings.defaultModelId,
|
|
14739
|
+
...skillset
|
|
14740
|
+
};
|
|
14741
|
+
}
|
|
14742
|
+
isEnvironmentManaged() {
|
|
14743
|
+
return [
|
|
14744
|
+
"PI_CLOUD_WECOM_CORP_ID",
|
|
14745
|
+
"PI_CLOUD_WECOM_CORP_SECRET",
|
|
14746
|
+
"PI_CLOUD_WECOM_AGENT_ID",
|
|
14747
|
+
"PI_CLOUD_WECOM_CALLBACK_TOKEN",
|
|
14748
|
+
"PI_CLOUD_WECOM_ENCODING_AES_KEY"
|
|
14749
|
+
].some((key) => Boolean(process.env[key]?.trim()));
|
|
14750
|
+
}
|
|
14751
|
+
loadSavedCredentials() {
|
|
14752
|
+
const row = this.db.prepare(`
|
|
14753
|
+
SELECT corp_id, corp_secret, agent_id, callback_token, encoding_aes_key,
|
|
14754
|
+
allowed_users_json, callback_verified_at
|
|
14755
|
+
FROM wecom_gateway_credentials WHERE id = 1
|
|
14756
|
+
`).get();
|
|
14757
|
+
if (!row) return null;
|
|
14758
|
+
let allowedUsers = [];
|
|
14759
|
+
try {
|
|
14760
|
+
allowedUsers = normalizeList(JSON.parse(row.allowed_users_json));
|
|
14761
|
+
} catch {
|
|
14762
|
+
}
|
|
14763
|
+
return {
|
|
14764
|
+
corpId: row.corp_id,
|
|
14765
|
+
corpSecret: row.corp_secret,
|
|
14766
|
+
agentId: row.agent_id,
|
|
14767
|
+
callbackToken: row.callback_token,
|
|
14768
|
+
encodingAesKey: row.encoding_aes_key,
|
|
14769
|
+
allowedUsers,
|
|
14770
|
+
callbackVerified: Boolean(row.callback_verified_at)
|
|
14771
|
+
};
|
|
14772
|
+
}
|
|
14773
|
+
markCallbackVerified() {
|
|
14774
|
+
if (this.isEnvironmentManaged()) {
|
|
14775
|
+
this.environmentCallbackVerified = true;
|
|
14776
|
+
return;
|
|
14777
|
+
}
|
|
14778
|
+
this.db.prepare("UPDATE wecom_gateway_credentials SET callback_verified_at = ?, updated_at = ? WHERE id = 1").run((/* @__PURE__ */ new Date()).toISOString(), (/* @__PURE__ */ new Date()).toISOString());
|
|
14779
|
+
}
|
|
14780
|
+
markDuplicate(id) {
|
|
14781
|
+
const now = Date.now();
|
|
14782
|
+
for (const [key, expiresAt] of this.seen) {
|
|
14783
|
+
if (expiresAt <= now) this.seen.delete(key);
|
|
14784
|
+
}
|
|
14785
|
+
if (this.seen.has(id)) return true;
|
|
14786
|
+
this.seen.set(id, now + DEDUP_TTL_MS3);
|
|
14787
|
+
return false;
|
|
14788
|
+
}
|
|
14789
|
+
enqueue(message, config) {
|
|
14790
|
+
const clientId = `wecom:dm:${message.userId}`;
|
|
14791
|
+
const previous = this.queues.get(clientId) || Promise.resolve();
|
|
14792
|
+
const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message, config)).catch((error) => console.error("[wecom-gateway] message handling failed:", error)).finally(() => {
|
|
14793
|
+
if (this.queues.get(clientId) === next) this.queues.delete(clientId);
|
|
14794
|
+
});
|
|
14795
|
+
this.queues.set(clientId, next);
|
|
14796
|
+
}
|
|
14797
|
+
async processMessage(clientId, message, config) {
|
|
14798
|
+
const effectiveConfig = this.resolveChatConfig(clientId, config);
|
|
14799
|
+
if (await this.handleCommand(clientId, message, config, effectiveConfig)) return;
|
|
14800
|
+
if (!effectiveConfig.defaultCwd) {
|
|
14801
|
+
await this.sendReply(config, message.userId, "No gateway folders configured. Add at least one allowed folder in Web UI Settings > Gateway.");
|
|
14802
|
+
return;
|
|
14803
|
+
}
|
|
14804
|
+
let promptText = message.text;
|
|
14805
|
+
if (message.messageType === "voice") {
|
|
14806
|
+
if (!message.mediaId) {
|
|
14807
|
+
await this.sendReply(config, message.userId, "The WeCom voice message did not include downloadable media. Try sending it again.");
|
|
14808
|
+
return;
|
|
14809
|
+
}
|
|
14810
|
+
try {
|
|
14811
|
+
promptText = (await transcribeAudio(await this.downloadVoice(config, message.mediaId), "audio/amr")).trim();
|
|
14812
|
+
} catch (error) {
|
|
14813
|
+
console.warn("[wecom-gateway] failed to transcribe inbound voice:", error instanceof Error ? error.message : error);
|
|
14814
|
+
await this.sendReply(config, message.userId, "The voice message could not be transcribed. Check the speech-to-text configuration and try again.");
|
|
14815
|
+
return;
|
|
14816
|
+
}
|
|
14817
|
+
if (!promptText) {
|
|
14818
|
+
await this.sendReply(config, message.userId, "The voice message did not contain recognizable speech. Try sending it again.");
|
|
14819
|
+
return;
|
|
14820
|
+
}
|
|
14821
|
+
}
|
|
14822
|
+
const session = await this.ensureSession(clientId, effectiveConfig);
|
|
14823
|
+
let images = [];
|
|
14824
|
+
if (message.messageType === "image") {
|
|
14825
|
+
if (!message.mediaId) {
|
|
14826
|
+
await this.sendReply(config, message.userId, "The WeCom image message did not include downloadable media. Try sending it again.");
|
|
14827
|
+
return;
|
|
14828
|
+
}
|
|
14829
|
+
try {
|
|
14830
|
+
images = [await this.downloadImage(config, message.mediaId)];
|
|
14831
|
+
} catch (error) {
|
|
14832
|
+
console.warn("[wecom-gateway] failed to download inbound image:", error instanceof Error ? error.message : error);
|
|
14833
|
+
await this.sendReply(config, message.userId, "The image could not be downloaded from WeCom. Try sending it again.");
|
|
14834
|
+
return;
|
|
14835
|
+
}
|
|
14836
|
+
}
|
|
14837
|
+
const imageResult = validateImages(images, session.model);
|
|
14838
|
+
if (!imageResult.ok) {
|
|
14839
|
+
await this.sendReply(config, message.userId, imageResult.message);
|
|
14840
|
+
return;
|
|
14841
|
+
}
|
|
14842
|
+
promptText ||= imageResult.images.length ? "Please analyze the attached image." : "";
|
|
14843
|
+
const chunks = [];
|
|
14844
|
+
const unsubscribe = session.subscribe((event) => {
|
|
14845
|
+
const update = event?.type === "message_update" ? event.assistantMessageEvent : void 0;
|
|
14846
|
+
if (update?.type === "text_delta" && typeof update.delta === "string") chunks.push(update.delta);
|
|
14847
|
+
});
|
|
14848
|
+
try {
|
|
14849
|
+
await this.sessionService.runForegroundWithClientProfileProxy(clientId, () => session.prompt(promptText, imageResult.images.length ? { images: imageResult.images } : void 0));
|
|
14850
|
+
} finally {
|
|
14851
|
+
unsubscribe();
|
|
14852
|
+
}
|
|
14853
|
+
const response = chunks.join("").trim() || extractLastAssistantText3(session.messages) || "(Pi finished without a text response.)";
|
|
14854
|
+
await this.sendReply(config, message.userId, response);
|
|
14855
|
+
}
|
|
14856
|
+
async ensureSession(clientId, config) {
|
|
14857
|
+
await this.sessionService.setClientAgentProfile(clientId, config.agentProfile || "default");
|
|
14858
|
+
const active = this.sessionService.getSession(clientId);
|
|
14859
|
+
if (active) return active;
|
|
14860
|
+
const mapped = this.db.prepare("SELECT pi_session_id FROM wecom_gateway_sessions WHERE client_id = ?").get(clientId);
|
|
14861
|
+
if (mapped) {
|
|
14862
|
+
const persisted = await this.sessionService.findPersistedSession(clientId, mapped.pi_session_id);
|
|
14863
|
+
if (persisted) return this.sessionService.resumeSession(clientId, persisted.path);
|
|
14864
|
+
}
|
|
14865
|
+
const preset = config.skillPresetId ? this.presetStore.getById(config.skillPresetId) : null;
|
|
14866
|
+
const result = await this.sessionService.createSession(clientId, {
|
|
14867
|
+
cwd: config.defaultCwd,
|
|
14868
|
+
modelProvider: config.modelProvider,
|
|
14869
|
+
modelId: config.modelId,
|
|
14870
|
+
enabledSkills: preset?.mode === "enabled" ? preset.skills : void 0,
|
|
14871
|
+
disabledSkills: preset?.mode === "disabled" ? preset.skills : void 0,
|
|
14872
|
+
presetId: preset?.id,
|
|
14873
|
+
skillMode: preset?.mode || config.skillMode || "all"
|
|
14874
|
+
});
|
|
14875
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14876
|
+
this.db.prepare(`
|
|
14877
|
+
INSERT INTO wecom_gateway_sessions (session_key, client_id, pi_session_id, created_at, updated_at)
|
|
14878
|
+
VALUES (?, ?, ?, ?, ?)
|
|
14879
|
+
ON CONFLICT(session_key) DO UPDATE SET pi_session_id = excluded.pi_session_id, updated_at = excluded.updated_at
|
|
14880
|
+
`).run(clientId, clientId, result.session.sessionId, now, now);
|
|
14881
|
+
return result.session;
|
|
14882
|
+
}
|
|
14883
|
+
async handleCommand(clientId, message, baseConfig, effectiveConfig) {
|
|
14884
|
+
const commandText = normalizeGatewayCommandText(message.text);
|
|
14885
|
+
const [command = "", ...args] = commandText.split(/\s+/);
|
|
14886
|
+
const normalized = command.toLowerCase();
|
|
14887
|
+
if (!normalized.startsWith("/")) return false;
|
|
14888
|
+
try {
|
|
14889
|
+
if (normalized === "/new" || normalized === "/reset") {
|
|
14890
|
+
this.resetSession(clientId);
|
|
14891
|
+
await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this WeCom chat. A fresh Pi session will start with your next message."));
|
|
14892
|
+
return true;
|
|
14893
|
+
}
|
|
14894
|
+
if (normalized === "/help") {
|
|
14895
|
+
await this.sendReply(baseConfig, message.userId, this.formatHelp());
|
|
14896
|
+
return true;
|
|
14897
|
+
}
|
|
14898
|
+
if (normalized === "/status") {
|
|
14899
|
+
await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, effectiveConfig));
|
|
14900
|
+
return true;
|
|
14901
|
+
}
|
|
14902
|
+
if (normalized === "/profiles") {
|
|
14903
|
+
const profiles = await this.sessionService.listAgentProfiles();
|
|
14904
|
+
await this.sendReply(baseConfig, message.userId, ["Available profiles:", ...profiles.map((profile) => `${profile.id === (effectiveConfig.agentProfile || "default") ? "*" : " "} ${profile.id}`)].join("\n"));
|
|
14905
|
+
return true;
|
|
14906
|
+
}
|
|
14907
|
+
if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
|
|
14908
|
+
const profile = args[0]?.trim();
|
|
14909
|
+
if (!profile) throw new Error("Usage: /profile <profile-id>");
|
|
14910
|
+
const profiles = await this.sessionService.listAgentProfiles();
|
|
14911
|
+
if (!profiles.some((item) => item.id === profile)) throw new Error(`Unknown profile: ${profile}`);
|
|
14912
|
+
this.saveChatConfig(clientId, { agentProfile: profile });
|
|
14913
|
+
this.resetSession(clientId);
|
|
14914
|
+
await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
|
|
14915
|
+
return true;
|
|
14916
|
+
}
|
|
14917
|
+
if (normalized === "/cwds" || normalized === "/pwds") {
|
|
14918
|
+
await this.sendReply(baseConfig, message.userId, baseConfig.cwdChoices.length ? baseConfig.cwdChoices.map((cwd, index) => `${index + 1}: ${cwd}`).join("\n") : "No gateway folders configured.");
|
|
14919
|
+
return true;
|
|
14920
|
+
}
|
|
14921
|
+
if (normalized === "/cwd") {
|
|
14922
|
+
const slot = Number(args[0]);
|
|
14923
|
+
if (!Number.isInteger(slot) || slot < 1 || slot > baseConfig.cwdChoices.length) throw new Error(`Usage: /cwd <1-${baseConfig.cwdChoices.length}>`);
|
|
14924
|
+
const target = baseConfig.cwdChoices[slot - 1];
|
|
14925
|
+
if (!isAbsolute6(target) || !(await stat8(target)).isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing absolute directory`);
|
|
14926
|
+
this.saveChatConfig(clientId, { defaultCwd: target });
|
|
14927
|
+
this.resetSession(clientId);
|
|
14928
|
+
await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
|
|
14929
|
+
return true;
|
|
14930
|
+
}
|
|
14931
|
+
if (normalized === "/skillsets") {
|
|
14932
|
+
await this.sendReply(baseConfig, message.userId, this.formatSkillsets(effectiveConfig.skillPresetId));
|
|
14933
|
+
return true;
|
|
14934
|
+
}
|
|
14935
|
+
if (normalized === "/skillset") {
|
|
14936
|
+
const selected = args.join(" ").trim();
|
|
14937
|
+
if (!selected) throw new Error("Usage: /skillset <all|preset-name>");
|
|
14938
|
+
this.saveChatConfig(clientId, this.resolveSkillsetConfig(selected, true));
|
|
14939
|
+
this.resetSession(clientId);
|
|
14940
|
+
await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
|
|
14941
|
+
return true;
|
|
14942
|
+
}
|
|
14943
|
+
if (normalized === "/clear-config") {
|
|
14944
|
+
this.db.prepare("DELETE FROM wecom_gateway_configs WHERE client_id = ?").run(clientId);
|
|
14945
|
+
this.resetSession(clientId);
|
|
14946
|
+
await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "WeCom chat config cleared."));
|
|
14947
|
+
return true;
|
|
14948
|
+
}
|
|
14949
|
+
} catch (error) {
|
|
14950
|
+
await this.sendReply(baseConfig, message.userId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
|
|
14951
|
+
|
|
14952
|
+
${this.formatHelp()}`);
|
|
14953
|
+
return true;
|
|
14954
|
+
}
|
|
14955
|
+
await this.sendReply(baseConfig, message.userId, `Unknown command: ${command}
|
|
14956
|
+
|
|
14957
|
+
${this.formatHelp()}`);
|
|
14958
|
+
return true;
|
|
14959
|
+
}
|
|
14960
|
+
resolveChatConfig(clientId, base) {
|
|
14961
|
+
const chat = this.loadChatConfig(clientId);
|
|
14962
|
+
const useBaseModel = !chat.agentProfile || chat.agentProfile === base.agentProfile;
|
|
14963
|
+
return {
|
|
14964
|
+
...base,
|
|
14965
|
+
agentProfile: chat.agentProfile || base.agentProfile,
|
|
14966
|
+
defaultCwd: chat.defaultCwd && base.cwdChoices.includes(chat.defaultCwd) ? chat.defaultCwd : base.defaultCwd,
|
|
14967
|
+
modelProvider: useBaseModel ? base.modelProvider : void 0,
|
|
14968
|
+
modelId: useBaseModel ? base.modelId : void 0,
|
|
14969
|
+
skillMode: chat.skillMode || base.skillMode,
|
|
14970
|
+
skillPresetId: chat.skillMode ? chat.skillPresetId : base.skillPresetId
|
|
14971
|
+
};
|
|
14972
|
+
}
|
|
14973
|
+
loadChatConfig(clientId) {
|
|
14974
|
+
const row = this.db.prepare("SELECT agent_profile, default_cwd, skill_mode, skill_preset_id FROM wecom_gateway_configs WHERE client_id = ?").get(clientId);
|
|
14975
|
+
return row ? {
|
|
14976
|
+
agentProfile: row.agent_profile || void 0,
|
|
14977
|
+
defaultCwd: row.default_cwd || void 0,
|
|
14978
|
+
skillMode: row.skill_mode || void 0,
|
|
14979
|
+
skillPresetId: row.skill_preset_id
|
|
14980
|
+
} : {};
|
|
14981
|
+
}
|
|
14982
|
+
saveChatConfig(clientId, patch) {
|
|
14983
|
+
const next = { ...this.loadChatConfig(clientId), ...patch };
|
|
14984
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14985
|
+
this.db.prepare(`
|
|
14986
|
+
INSERT INTO wecom_gateway_configs (client_id, agent_profile, default_cwd, skill_mode, skill_preset_id, created_at, updated_at)
|
|
14987
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
14988
|
+
ON CONFLICT(client_id) DO UPDATE SET
|
|
14989
|
+
agent_profile = excluded.agent_profile,
|
|
14990
|
+
default_cwd = excluded.default_cwd,
|
|
14991
|
+
skill_mode = excluded.skill_mode,
|
|
14992
|
+
skill_preset_id = excluded.skill_preset_id,
|
|
14993
|
+
updated_at = excluded.updated_at
|
|
14994
|
+
`).run(clientId, next.agentProfile || null, next.defaultCwd || null, next.skillMode || null, next.skillPresetId || null, now, now);
|
|
14995
|
+
}
|
|
14996
|
+
resetSession(clientId) {
|
|
14997
|
+
this.sessionService.disposeSession(clientId);
|
|
14998
|
+
this.db.prepare("DELETE FROM wecom_gateway_sessions WHERE client_id = ?").run(clientId);
|
|
14999
|
+
}
|
|
15000
|
+
async formatStatus(clientId, config, prefix) {
|
|
15001
|
+
const profiles = await this.sessionService.listAgentProfiles();
|
|
15002
|
+
const profile = profiles.find((item) => item.id === (config.agentProfile || "default")) || profiles[0];
|
|
15003
|
+
const active = this.sessionService.getSession(clientId);
|
|
15004
|
+
return [
|
|
15005
|
+
prefix,
|
|
15006
|
+
"Pi session status",
|
|
15007
|
+
"",
|
|
15008
|
+
`- Profile: ${profile?.id || "default"}`,
|
|
15009
|
+
`- CWD: ${config.defaultCwd || "(not configured)"}`,
|
|
15010
|
+
`- Provider: ${active?.model?.provider || config.modelProvider || profile?.defaultProvider || "(profile default)"}`,
|
|
15011
|
+
`- Model: ${active?.model?.id || config.modelId || profile?.defaultModel || "(profile default)"}`,
|
|
15012
|
+
`- Skillset: ${this.formatSkillsetName(config.skillPresetId)}`,
|
|
15013
|
+
`- Session: ${active ? `active (${active.sessionId})` : "not started"}`
|
|
15014
|
+
].filter((line) => line !== void 0).join("\n");
|
|
15015
|
+
}
|
|
15016
|
+
formatHelp() {
|
|
15017
|
+
return [
|
|
15018
|
+
"Pi Cloud WeCom commands",
|
|
15019
|
+
"/new - reset this chat session",
|
|
15020
|
+
"/status - show current configuration",
|
|
15021
|
+
"/profiles - list profiles",
|
|
15022
|
+
"/profile <id> - switch profile",
|
|
15023
|
+
"/cwds - list allowed folders",
|
|
15024
|
+
"/cwd <number> - switch folder",
|
|
15025
|
+
"/skillsets - list skillsets",
|
|
15026
|
+
"/skillset <name> - switch skillset",
|
|
15027
|
+
"/clear-config - restore gateway defaults",
|
|
15028
|
+
"/help - show this help",
|
|
15029
|
+
"",
|
|
15030
|
+
GATEWAY_COMMON_ALIAS_HELP
|
|
15031
|
+
].join("\n");
|
|
15032
|
+
}
|
|
15033
|
+
resolveSkillsetConfig(skillset, strict = false) {
|
|
15034
|
+
const selected = skillset?.trim();
|
|
15035
|
+
if (!selected || selected.toLowerCase() === "all") return { skillMode: "all", skillPresetId: null };
|
|
15036
|
+
const preset = this.findSkillPreset(selected);
|
|
15037
|
+
if (preset) return { skillMode: preset.mode, skillPresetId: preset.id };
|
|
15038
|
+
if (strict) throw new Error(`Unknown skillset: ${selected}`);
|
|
15039
|
+
console.warn("[wecom-gateway] default gateway skillset was not found; using all skills", { skillset: selected });
|
|
15040
|
+
return { skillMode: "all", skillPresetId: null };
|
|
15041
|
+
}
|
|
15042
|
+
formatSkillsets(currentPresetId) {
|
|
15043
|
+
const presets = this.presetStore.list();
|
|
15044
|
+
return [
|
|
15045
|
+
"Available skillsets:",
|
|
15046
|
+
`${currentPresetId ? " " : "*"} all - use all skills`,
|
|
15047
|
+
...presets.map((preset) => `${preset.id === currentPresetId ? "*" : " "} ${preset.name} (${preset.mode}: ${preset.skills.join(", ") || "none"})`)
|
|
15048
|
+
].join("\n");
|
|
15049
|
+
}
|
|
15050
|
+
formatSkillsetName(presetId) {
|
|
15051
|
+
if (!presetId) return "all";
|
|
15052
|
+
const preset = this.presetStore.getById(presetId);
|
|
15053
|
+
return preset ? `${preset.name} (${preset.mode})` : `unknown preset (${presetId})`;
|
|
15054
|
+
}
|
|
15055
|
+
findSkillPreset(nameOrId) {
|
|
15056
|
+
const normalized = nameOrId.trim().toLowerCase();
|
|
15057
|
+
return this.presetStore.list().find((preset) => preset.id === nameOrId || preset.name.toLowerCase() === normalized) || null;
|
|
15058
|
+
}
|
|
15059
|
+
async sendReply(config, userId, text) {
|
|
15060
|
+
for (const chunk of splitUtf8Text(formatWecomMarkdown(text), TEXT_CHUNK_MAX_BYTES)) {
|
|
15061
|
+
let token = await this.getAccessToken(config);
|
|
15062
|
+
let data = await this.sendApplicationMarkdown(config, token, userId, chunk);
|
|
15063
|
+
if ([40014, 42001].includes(Number(data.errcode))) {
|
|
15064
|
+
this.tokenCache = void 0;
|
|
15065
|
+
token = await this.getAccessToken(config);
|
|
15066
|
+
data = await this.sendApplicationMarkdown(config, token, userId, chunk);
|
|
15067
|
+
}
|
|
15068
|
+
if (Number(data.errcode) !== 0) throw new Error(`WeCom send message failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
|
|
15069
|
+
}
|
|
15070
|
+
}
|
|
15071
|
+
async sendApplicationMarkdown(config, token, userId, text) {
|
|
15072
|
+
const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/message/send?access_token=${encodeURIComponent(token)}`, {
|
|
15073
|
+
method: "POST",
|
|
15074
|
+
headers: { "content-type": "application/json" },
|
|
15075
|
+
body: JSON.stringify({
|
|
15076
|
+
touser: userId,
|
|
15077
|
+
msgtype: "markdown",
|
|
15078
|
+
agentid: Number(config.agentId),
|
|
15079
|
+
markdown: { content: text },
|
|
15080
|
+
safe: 0,
|
|
15081
|
+
enable_duplicate_check: 1,
|
|
15082
|
+
duplicate_check_interval: 1800
|
|
15083
|
+
})
|
|
15084
|
+
});
|
|
15085
|
+
return parseWecomResponse(response);
|
|
15086
|
+
}
|
|
15087
|
+
async downloadImage(config, mediaId) {
|
|
15088
|
+
const bytes = await this.downloadMedia(config, mediaId, MAX_IMAGE_BYTES, "An image is larger than 10 MB.");
|
|
15089
|
+
const mimeType = sniffImageMimeType(bytes);
|
|
15090
|
+
if (!mimeType) throw new Error("WeCom media response was not a supported image");
|
|
15091
|
+
return { type: "image", data: bytes.toString("base64"), mimeType };
|
|
15092
|
+
}
|
|
15093
|
+
downloadVoice(config, mediaId) {
|
|
15094
|
+
return this.downloadMedia(config, mediaId, MAX_AUDIO_BYTES, "A voice message is larger than 25 MB.");
|
|
15095
|
+
}
|
|
15096
|
+
async downloadMedia(config, mediaId, maxBytes, tooLargeMessage) {
|
|
15097
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
15098
|
+
const token = await this.getAccessToken(config);
|
|
15099
|
+
const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/media/get?access_token=${encodeURIComponent(token)}&media_id=${encodeURIComponent(mediaId)}`);
|
|
15100
|
+
const contentType = response.headers.get("content-type") || "";
|
|
15101
|
+
if (contentType.toLowerCase().includes("application/json")) {
|
|
15102
|
+
const data = await parseWecomResponse(response);
|
|
15103
|
+
const errorCode = Number(data.errcode);
|
|
15104
|
+
if (attempt === 0 && [40014, 42001].includes(errorCode)) {
|
|
15105
|
+
this.tokenCache = void 0;
|
|
15106
|
+
continue;
|
|
15107
|
+
}
|
|
15108
|
+
throw new Error(`WeCom media download failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
|
|
15109
|
+
}
|
|
15110
|
+
if (!response.ok) throw new Error(`WeCom media download failed: HTTP ${response.status}`);
|
|
15111
|
+
return readResponseBytes(response, maxBytes, tooLargeMessage);
|
|
15112
|
+
}
|
|
15113
|
+
throw new Error("WeCom media download failed after refreshing the access token");
|
|
15114
|
+
}
|
|
15115
|
+
async getAccessToken(config) {
|
|
15116
|
+
const now = Date.now();
|
|
15117
|
+
if (this.tokenCache && this.tokenCache.expiresAt - TOKEN_REFRESH_SKEW_MS2 > now) return this.tokenCache.token;
|
|
15118
|
+
const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/gettoken?corpid=${encodeURIComponent(config.corpId)}&corpsecret=${encodeURIComponent(config.corpSecret)}`);
|
|
15119
|
+
const data = await parseWecomResponse(response);
|
|
15120
|
+
const token = stringValue3(data.access_token);
|
|
15121
|
+
if (Number(data.errcode) !== 0 || !token) throw new Error(`WeCom access token failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
|
|
15122
|
+
this.tokenCache = { token, expiresAt: now + (Number(data.expires_in) || 7200) * 1e3 };
|
|
15123
|
+
return token;
|
|
15124
|
+
}
|
|
15125
|
+
};
|
|
15126
|
+
async function readResponseBytes(response, maxBytes, tooLargeMessage) {
|
|
15127
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
15128
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
15129
|
+
await response.body?.cancel();
|
|
15130
|
+
throw new Error(tooLargeMessage);
|
|
15131
|
+
}
|
|
15132
|
+
if (!response.body) return Buffer.alloc(0);
|
|
15133
|
+
const reader = response.body.getReader();
|
|
15134
|
+
const chunks = [];
|
|
15135
|
+
let totalBytes = 0;
|
|
15136
|
+
try {
|
|
15137
|
+
while (true) {
|
|
15138
|
+
const { done, value } = await reader.read();
|
|
15139
|
+
if (done) break;
|
|
15140
|
+
totalBytes += value.byteLength;
|
|
15141
|
+
if (totalBytes > maxBytes) {
|
|
15142
|
+
await reader.cancel();
|
|
15143
|
+
throw new Error(tooLargeMessage);
|
|
15144
|
+
}
|
|
15145
|
+
chunks.push(Buffer.from(value));
|
|
15146
|
+
}
|
|
15147
|
+
} finally {
|
|
15148
|
+
reader.releaseLock();
|
|
15149
|
+
}
|
|
15150
|
+
return Buffer.concat(chunks, totalBytes);
|
|
15151
|
+
}
|
|
15152
|
+
function parseWecomMessage(xml) {
|
|
15153
|
+
const messageType = xmlValue(xml, "MsgType");
|
|
15154
|
+
const userId = xmlValue(xml, "FromUserName");
|
|
15155
|
+
const messageId = xmlValue(xml, "MsgId");
|
|
15156
|
+
const agentId = xmlValue(xml, "AgentID");
|
|
15157
|
+
if (!messageType || !userId || !messageId || !agentId) return null;
|
|
15158
|
+
return {
|
|
15159
|
+
messageType,
|
|
15160
|
+
userId,
|
|
15161
|
+
messageId,
|
|
15162
|
+
agentId,
|
|
15163
|
+
text: xmlValue(xml, "Content").trim(),
|
|
15164
|
+
mediaId: xmlValue(xml, "MediaId") || void 0,
|
|
15165
|
+
picUrl: xmlValue(xml, "PicUrl") || void 0
|
|
15166
|
+
};
|
|
15167
|
+
}
|
|
15168
|
+
function xmlValue(xml, tag) {
|
|
15169
|
+
const match = new RegExp(`<${tag}>(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([\\s\\S]*?))<\\/${tag}>`, "i").exec(xml);
|
|
15170
|
+
return decodeXmlEntities((match?.[1] ?? match?.[2] ?? "").trim());
|
|
15171
|
+
}
|
|
15172
|
+
function decodeXmlEntities(value) {
|
|
15173
|
+
return value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
15174
|
+
}
|
|
15175
|
+
function parseList2(value) {
|
|
15176
|
+
return Array.from(new Set((value || "").split(",").map((item) => item.trim()).filter(Boolean)));
|
|
15177
|
+
}
|
|
15178
|
+
function normalizeList(value) {
|
|
15179
|
+
return Array.from(new Set((Array.isArray(value) ? value : []).filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)));
|
|
15180
|
+
}
|
|
15181
|
+
function stringValue3(value) {
|
|
15182
|
+
return typeof value === "string" ? value : value === void 0 || value === null ? "" : String(value);
|
|
15183
|
+
}
|
|
15184
|
+
function gatewayError(message, statusCode) {
|
|
15185
|
+
const error = new Error(message);
|
|
15186
|
+
error.statusCode = statusCode;
|
|
15187
|
+
return error;
|
|
15188
|
+
}
|
|
15189
|
+
async function parseWecomResponse(response) {
|
|
15190
|
+
const text = await response.text();
|
|
15191
|
+
let data = {};
|
|
15192
|
+
try {
|
|
15193
|
+
data = JSON.parse(text);
|
|
15194
|
+
} catch {
|
|
15195
|
+
}
|
|
15196
|
+
if (!response.ok) throw new Error(`WeCom HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
15197
|
+
return data;
|
|
15198
|
+
}
|
|
15199
|
+
function extractLastAssistantText3(messages) {
|
|
15200
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
15201
|
+
const message = messages[index];
|
|
15202
|
+
if (message?.role !== "assistant") continue;
|
|
15203
|
+
if (typeof message.content === "string" && message.content.trim()) return message.content.trim();
|
|
15204
|
+
if (!Array.isArray(message.content)) continue;
|
|
15205
|
+
const text = message.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("").trim();
|
|
15206
|
+
if (text) return text;
|
|
15207
|
+
}
|
|
15208
|
+
return void 0;
|
|
15209
|
+
}
|
|
15210
|
+
function formatWecomMarkdown(text) {
|
|
15211
|
+
return text.replace(/^([ \t]*)[-+*][ \t]+/gm, (_match, indentation) => `${indentation}${indentation ? "\u25E6" : "\u2022"} `);
|
|
15212
|
+
}
|
|
15213
|
+
function splitUtf8Text(text, maxBytes) {
|
|
15214
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return [text];
|
|
15215
|
+
const chunks = [];
|
|
15216
|
+
let current = "";
|
|
15217
|
+
let currentBytes = 0;
|
|
15218
|
+
for (const character of text) {
|
|
15219
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
15220
|
+
if (current && currentBytes + characterBytes > maxBytes) {
|
|
15221
|
+
chunks.push(current);
|
|
15222
|
+
current = "";
|
|
15223
|
+
currentBytes = 0;
|
|
15224
|
+
}
|
|
15225
|
+
current += character;
|
|
15226
|
+
currentBytes += characterBytes;
|
|
15227
|
+
}
|
|
15228
|
+
if (current) chunks.push(current);
|
|
15229
|
+
return chunks;
|
|
15230
|
+
}
|
|
15231
|
+
|
|
14080
15232
|
// src/services/review-source-store.ts
|
|
14081
15233
|
import * as os8 from "os";
|
|
14082
15234
|
import * as path11 from "path";
|
|
@@ -14124,7 +15276,7 @@ var ClaudeCodeReviewSourceAdapter = class {
|
|
|
14124
15276
|
try {
|
|
14125
15277
|
const records = this.readRecords(filePath);
|
|
14126
15278
|
if (!records.length) return [];
|
|
14127
|
-
const
|
|
15279
|
+
const stat9 = fs5.statSync(filePath);
|
|
14128
15280
|
const sessionRecord = records.find((record) => record.sessionId || record.session_id);
|
|
14129
15281
|
const sessionId = sessionRecord?.sessionId || sessionRecord?.session_id || path7.basename(filePath, ".jsonl");
|
|
14130
15282
|
const cwd = records.find((record) => record.cwd)?.cwd;
|
|
@@ -14140,8 +15292,8 @@ var ClaudeCodeReviewSourceAdapter = class {
|
|
|
14140
15292
|
name: title,
|
|
14141
15293
|
path: filePath,
|
|
14142
15294
|
cwd,
|
|
14143
|
-
created: new Date(timestamps.length ? Math.min(...timestamps) :
|
|
14144
|
-
modified: new Date(timestamps.length ? Math.max(...timestamps) :
|
|
15295
|
+
created: new Date(timestamps.length ? Math.min(...timestamps) : stat9.birthtimeMs).toISOString(),
|
|
15296
|
+
modified: new Date(timestamps.length ? Math.max(...timestamps) : stat9.mtimeMs).toISOString(),
|
|
14145
15297
|
messageCount: messages.length,
|
|
14146
15298
|
firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
|
|
14147
15299
|
}
|
|
@@ -14287,7 +15439,7 @@ var CodexReviewSourceAdapter = class {
|
|
|
14287
15439
|
try {
|
|
14288
15440
|
const records = this.readRecords(filePath);
|
|
14289
15441
|
if (!records.length) return [];
|
|
14290
|
-
const
|
|
15442
|
+
const stat9 = fs6.statSync(filePath);
|
|
14291
15443
|
const metadata = records.find((record) => record.type === "session_meta")?.payload;
|
|
14292
15444
|
const messages = this.recordsToMessages(records);
|
|
14293
15445
|
const timestamps = records.map((record) => this.timestamp(record.timestamp)).filter((value) => value !== void 0);
|
|
@@ -14299,8 +15451,8 @@ var CodexReviewSourceAdapter = class {
|
|
|
14299
15451
|
sourceId: "",
|
|
14300
15452
|
path: filePath,
|
|
14301
15453
|
cwd: this.stringValue(metadata?.cwd),
|
|
14302
|
-
created: new Date(timestamps.length ? Math.min(...timestamps) :
|
|
14303
|
-
modified: new Date(timestamps.length ? Math.max(...timestamps) :
|
|
15454
|
+
created: new Date(timestamps.length ? Math.min(...timestamps) : stat9.birthtimeMs).toISOString(),
|
|
15455
|
+
modified: new Date(timestamps.length ? Math.max(...timestamps) : stat9.mtimeMs).toISOString(),
|
|
14304
15456
|
messageCount: messages.length,
|
|
14305
15457
|
firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
|
|
14306
15458
|
}
|
|
@@ -14866,7 +16018,7 @@ function createDefaultConfig() {
|
|
|
14866
16018
|
if (process.env.NODE_ENV === "test") return;
|
|
14867
16019
|
if (process.env.PI_CLOUD_AUTH_USERNAME || process.env.PI_CLOUD_AUTH_PASSWORD || process.env.PI_CLOUD_AUTH_PASSWORD_HASH) return;
|
|
14868
16020
|
const username = "admin";
|
|
14869
|
-
const password =
|
|
16021
|
+
const password = randomBytes4(6).toString("base64url");
|
|
14870
16022
|
const samplePath = path12.resolve(__dirname, "../../.env.example");
|
|
14871
16023
|
const content = renderDefaultConfig(readFileSync4(samplePath, "utf8"), username, password);
|
|
14872
16024
|
mkdirSync2(userConfigDir, { recursive: true, mode: 448 });
|
|
@@ -14887,6 +16039,7 @@ var CONTENT_SECURITY_POLICY = [
|
|
|
14887
16039
|
"object-src 'none'",
|
|
14888
16040
|
"frame-ancestors 'none'",
|
|
14889
16041
|
"img-src 'self' data: blob:",
|
|
16042
|
+
"media-src 'self' blob:",
|
|
14890
16043
|
"font-src 'self' data:",
|
|
14891
16044
|
"style-src 'self' 'unsafe-inline'",
|
|
14892
16045
|
"script-src 'self'",
|
|
@@ -14909,6 +16062,7 @@ function isPublicRoute(req) {
|
|
|
14909
16062
|
if (url === "/api/health") return true;
|
|
14910
16063
|
if (url.startsWith("/api/auth")) return true;
|
|
14911
16064
|
if (url === "/api/gateways/feishu/events") return true;
|
|
16065
|
+
if (url === "/api/gateways/wecom/callback") return true;
|
|
14912
16066
|
if (req.method === "GET" && !url.startsWith("/api/")) return true;
|
|
14913
16067
|
return false;
|
|
14914
16068
|
}
|
|
@@ -14989,6 +16143,7 @@ async function buildApp() {
|
|
|
14989
16143
|
const githubSettings = new GithubSettingsStore(db);
|
|
14990
16144
|
const feishuGateway = new FeishuGatewayService(db, gatewaySettings, piSessionService);
|
|
14991
16145
|
const weixinGateway = new WeixinGatewayService(db, gatewaySettings, piSessionService);
|
|
16146
|
+
const wecomGateway = new WecomGatewayService(db, gatewaySettings, piSessionService);
|
|
14992
16147
|
weixinGateway.start();
|
|
14993
16148
|
const repositoryCloner = new RepositoryCloner({
|
|
14994
16149
|
spawnGit: (args, options) => spawn3("git", args, options),
|
|
@@ -15040,7 +16195,7 @@ async function buildApp() {
|
|
|
15040
16195
|
reply.header("X-Content-Type-Options", "nosniff");
|
|
15041
16196
|
reply.header("X-Frame-Options", "DENY");
|
|
15042
16197
|
reply.header("Referrer-Policy", "no-referrer");
|
|
15043
|
-
reply.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
|
16198
|
+
reply.header("Permissions-Policy", "camera=(), microphone=(self), geolocation=()");
|
|
15044
16199
|
if (authConfig.cookieSecure) {
|
|
15045
16200
|
reply.header("Strict-Transport-Security", "max-age=31536000");
|
|
15046
16201
|
}
|
|
@@ -15089,6 +16244,7 @@ async function buildApp() {
|
|
|
15089
16244
|
await app.register(taskRoutes, { prefix: "/api/tasks", store: projectTaskStore, starter: projectTaskStarter, activityStore: sessionActivityStore });
|
|
15090
16245
|
await app.register(gatewayRoutes, { prefix: "/api/gateways", settings: gatewaySettings, weixin: weixinGateway });
|
|
15091
16246
|
await app.register(feishuGatewayRoutes, { prefix: "/api/gateways/feishu", service: feishuGateway });
|
|
16247
|
+
await app.register(wecomGatewayRoutes, { prefix: "/api/gateways/wecom", service: wecomGateway });
|
|
15092
16248
|
const gitHostingRouteOptions = {
|
|
15093
16249
|
settings: giteaSettings,
|
|
15094
16250
|
githubSettings,
|
|
@@ -15108,6 +16264,7 @@ async function buildApp() {
|
|
|
15108
16264
|
});
|
|
15109
16265
|
await app.register(slashCommandRoutes, { prefix: "/api/slash-commands" });
|
|
15110
16266
|
await app.register(changelogRoutes, { prefix: "/api/changelog" });
|
|
16267
|
+
await app.register(speechRoutes, { prefix: "/api/speech" });
|
|
15111
16268
|
await app.register(reviewSourceRoutes, { prefix: "/api/review-sources", reviewSourceService, pinStore: sessionPinStore });
|
|
15112
16269
|
await app.register(chatWebSocket);
|
|
15113
16270
|
await app.register(terminalWebSocket);
|