@cc-monitoring/proxy-core 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/__tests__/commands.test.js +19 -0
- package/dist/cli/commands/repair.d.ts +1 -0
- package/dist/cli/commands/repair.js +15 -0
- package/dist/cli/index.js +30 -0
- package/dist/docs/404.html +1 -1
- package/dist/docs/assets/{app.DtYNT5zS.js → app.vxoC6jUT.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.lKm6N3Ik.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DpXF9ZAI.js → VPLocalSearchBox.DrfGpz86.js} +1 -1
- package/dist/docs/assets/chunks/{theme.TS_gUyc7.js → theme.CCmzbSn9.js} +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/overview.html +2 -2
- package/dist/docs/packages-utils.html +2 -2
- package/dist/docs/proxy-admin/env-and-build.html +2 -2
- package/dist/docs/proxy-admin/index.html +2 -2
- package/dist/docs/proxy-admin/layout-and-menu.html +2 -2
- package/dist/docs/proxy-admin/pages.html +2 -2
- package/dist/docs/proxy-admin/routing-and-auth.html +2 -2
- package/dist/docs/proxy-admin/services-and-mock.html +2 -2
- package/dist/docs/proxy-cli/api-layer.html +2 -2
- package/dist/docs/proxy-cli/app-shell.html +2 -2
- package/dist/docs/proxy-cli/components-common.html +2 -2
- package/dist/docs/proxy-cli/components-config.html +2 -2
- package/dist/docs/proxy-cli/components-requests.html +2 -2
- package/dist/docs/proxy-cli/components-stats.html +2 -2
- package/dist/docs/proxy-cli/components-status.html +2 -2
- package/dist/docs/proxy-cli/data-hooks.html +2 -2
- package/dist/docs/proxy-cli/index.html +2 -2
- package/dist/docs/proxy-cli/utils-and-i18n.html +2 -2
- package/dist/docs/proxy-core/chat-api.html +2 -2
- package/dist/docs/proxy-core/cli.html +2 -2
- package/dist/docs/proxy-core/config.html +2 -2
- package/dist/docs/proxy-core/data-layer.html +2 -2
- package/dist/docs/proxy-core/entry.html +2 -2
- package/dist/docs/proxy-core/index.html +2 -2
- package/dist/docs/proxy-core/pm2.html +2 -2
- package/dist/docs/proxy-core/providers.html +2 -2
- package/dist/docs/proxy-core/proxy.html +2 -2
- package/dist/docs/proxy-core/stats.html +2 -2
- package/dist/docs/proxy-core/tool.html +2 -2
- package/dist/docs/proxy-core/ui-routes.html +2 -2
- package/dist/docs/proxy-core/utilities.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/request-flow.html +2 -2
- package/dist/server/__tests__/data-push.test.js +44 -0
- package/dist/server/__tests__/proxy.test.js +365 -2
- package/dist/server/__tests__/server.test.js +15 -10
- package/dist/server/__tests__/ui-route.test.js +43 -0
- package/dist/server/chat-route.js +3 -3
- package/dist/server/data-push.d.ts +2 -0
- package/dist/server/data-push.js +22 -0
- package/dist/server/index.js +4 -3
- package/dist/server/proxy.js +127 -23
- package/dist/server/ui-route.js +8 -1
- package/dist/ui/assets/{1c-CCx-FOtz.js → 1c-BdWVxVKv.js} +1 -1
- package/dist/ui/assets/{abnf-Bwbi42Z8.js → abnf-hHBtR6kN.js} +1 -1
- package/dist/ui/assets/{abnfDiagram-VRR7QNED-Dm1-8q0E.js → abnfDiagram-VRR7QNED-B0E8zzLB.js} +1 -1
- package/dist/ui/assets/{accesslog-DCytz_-m.js → accesslog-BZmvPz-q.js} +1 -1
- package/dist/ui/assets/{actionscript-3udf15oh.js → actionscript-XtUFCVQF.js} +1 -1
- package/dist/ui/assets/{ada-M5Xa74xG.js → ada-DDsWH3P3.js} +1 -1
- package/dist/ui/assets/{angelscript-qlrFxfW-.js → angelscript-CGLPsuae.js} +1 -1
- package/dist/ui/assets/{apache-B-sF-DQH.js → apache-eGe6GEfx.js} +1 -1
- package/dist/ui/assets/{applescript-4FdcBnbI.js → applescript-Ch2Hdkz_.js} +1 -1
- package/dist/ui/assets/{arc-BB3K1enp.js → arc-Bi_rJJly.js} +1 -1
- package/dist/ui/assets/{arcade-B_yqWaaG.js → arcade-BAeq_5jU.js} +1 -1
- package/dist/ui/assets/{architectureDiagram-ZJ3FMSHR-B39vkY8V.js → architectureDiagram-ZJ3FMSHR-D3rUGwDS.js} +1 -1
- package/dist/ui/assets/{arduino-yAcBoTWU.js → arduino-DJR8nayp.js} +1 -1
- package/dist/ui/assets/{armasm-wIcY84bI.js → armasm-DqDD_ZDN.js} +1 -1
- package/dist/ui/assets/{asciidoc-Bxnu28-Z.js → asciidoc-CgBdhmR5.js} +1 -1
- package/dist/ui/assets/{aspectj-B2cKJA_O.js → aspectj-Dbm_6mY0.js} +1 -1
- package/dist/ui/assets/{autohotkey-B_rMWTtE.js → autohotkey-BkbluSS8.js} +1 -1
- package/dist/ui/assets/{autoit-DwcuvMwv.js → autoit-qUepyZBT.js} +1 -1
- package/dist/ui/assets/{avrasm-heGX1PXK.js → avrasm-2snPxE5L.js} +1 -1
- package/dist/ui/assets/{awk-BiqCQAEf.js → awk-oPcQTdJK.js} +1 -1
- package/dist/ui/assets/{axapta-Cv8M-Ih6.js → axapta-CCu7hfQ-.js} +1 -1
- package/dist/ui/assets/{bash-Divgn3JV.js → bash-CmWN3rPR.js} +1 -1
- package/dist/ui/assets/{basic-B9dnAP55.js → basic-Dg8KcxuC.js} +1 -1
- package/dist/ui/assets/{blockDiagram-677ZJIJ3-DQ1mf7A_.js → blockDiagram-677ZJIJ3-Dusa5zr8.js} +1 -1
- package/dist/ui/assets/{bnf-kIL5mSsP.js → bnf-Zmfygl9A.js} +1 -1
- package/dist/ui/assets/{brainfuck-DqB7xDjH.js → brainfuck-K4-sBD6i.js} +1 -1
- package/dist/ui/assets/{coq-6aKUn3zn.js → c-BGtacyfP.js} +1 -1
- package/dist/ui/assets/{c-like-De5MhE7G.js → c-like-FRDCqB2X.js} +1 -1
- package/dist/ui/assets/{c4Diagram-LMCZKHZV-DQxtYsAx.js → c4Diagram-LMCZKHZV-C4Vo5RAI.js} +1 -1
- package/dist/ui/assets/{cal-mYdfz9en.js → cal-BibfnL8Q.js} +1 -1
- package/dist/ui/assets/{capnproto-D04CRlOt.js → capnproto-D4sJA0ax.js} +1 -1
- package/dist/ui/assets/{ceylon-pi9lFCja.js → ceylon-DuI5ot57.js} +1 -1
- package/dist/ui/assets/channel-CviGbONW.js +1 -0
- package/dist/ui/assets/{chunk-2Q5K7J3B-BDD1nPt8.js → chunk-2Q5K7J3B-C0UTs_LW.js} +1 -1
- package/dist/ui/assets/{chunk-32BRIVSS-DgeQFBb5.js → chunk-32BRIVSS-BMqPhehD.js} +1 -1
- package/dist/ui/assets/{chunk-5VM5RSS4-C7Xf2SnI.js → chunk-5VM5RSS4-CKN4Vw1w.js} +1 -1
- package/dist/ui/assets/{chunk-EX3LRPZG-B_3p1HEv.js → chunk-EX3LRPZG-BYHH22fs.js} +1 -1
- package/dist/ui/assets/{chunk-JWPE2WC7-Dvs7u8eY.js → chunk-JWPE2WC7-b55Vo5mo.js} +1 -1
- package/dist/ui/assets/{chunk-MOJQB5TN-By21wDCN.js → chunk-MOJQB5TN-CvtnHxi4.js} +1 -1
- package/dist/ui/assets/{chunk-RYQCIY6F-6NC700r1.js → chunk-RYQCIY6F-Bi9_eYfb.js} +1 -1
- package/dist/ui/assets/{chunk-V7JOEXUC-vEUqZ3rh.js → chunk-V7JOEXUC-C3ujZT8O.js} +1 -1
- package/dist/ui/assets/{chunk-VR4S4FIN-6tX2dNN9.js → chunk-VR4S4FIN-D4pfDh-j.js} +1 -1
- package/dist/ui/assets/{chunk-XXDRQBXY-qDb2jxAR.js → chunk-XXDRQBXY-DQAjg3aO.js} +1 -1
- package/dist/ui/assets/classDiagram-OUVF2IWQ-BlFcpmEd.js +1 -0
- package/dist/ui/assets/classDiagram-v2-EOCWNBFH-BlFcpmEd.js +1 -0
- package/dist/ui/assets/{clean-CLCsDUl1.js → clean-BVmfQBBV.js} +1 -1
- package/dist/ui/assets/{clojure-dVi5Xifj.js → clojure-BN3C1fnU.js} +1 -1
- package/dist/ui/assets/{clojure-repl-BO_FpFmF.js → clojure-repl-C7Gg0UPF.js} +1 -1
- package/dist/ui/assets/{cmake-CP5aJbyK.js → cmake-CTQFRyul.js} +1 -1
- package/dist/ui/assets/{coffeescript-ZEfu_9pF.js → coffeescript-DGWU3k3W.js} +1 -1
- package/dist/ui/assets/{c-HmsCM6sx.js → coq-Bv9CXGly.js} +1 -1
- package/dist/ui/assets/{cos-BohCnRN2.js → cos-JXNeNyLn.js} +1 -1
- package/dist/ui/assets/{cose-bilkent-JH36ORCC-DyXCbbro.js → cose-bilkent-JH36ORCC-B2e-rPVN.js} +1 -1
- package/dist/ui/assets/{cpp-FOgPWPEt.js → cpp-DVS4QZP-.js} +1 -1
- package/dist/ui/assets/{crmsh-B0OYb-Oe.js → crmsh-CFD7lrxk.js} +1 -1
- package/dist/ui/assets/{crystal-CuWb_dXm.js → crystal-D7vS7dho.js} +1 -1
- package/dist/ui/assets/{csharp-BbcqM09j.js → csharp-BgeVxKaT.js} +1 -1
- package/dist/ui/assets/{csp-rgHnTaDq.js → csp-CmAkjXvi.js} +1 -1
- package/dist/ui/assets/{css-C-ibh7ZG.js → css-DtEkZRCq.js} +1 -1
- package/dist/ui/assets/{cynefin-VYW2F7L2-CkuBhgC4.js → cynefin-VYW2F7L2-bhneX2QQ.js} +1 -1
- package/dist/ui/assets/{cynefinDiagram-TSTJHNR4-eycFyDao.js → cynefinDiagram-TSTJHNR4-BvtRhsVM.js} +1 -1
- package/dist/ui/assets/{d-C0gXxJ0O.js → d-ov1MlWJx.js} +1 -1
- package/dist/ui/assets/{dagre-VKFMJZFB-DzqX9e49.js → dagre-VKFMJZFB-nxN1qAKi.js} +1 -1
- package/dist/ui/assets/{dart-C-fTpuUV.js → dart-DX6s604e.js} +1 -1
- package/dist/ui/assets/{delphi-Chm0B9xv.js → delphi-D0oU57Jt.js} +1 -1
- package/dist/ui/assets/{diagram-FQU43EPY-BEgCqfg1.js → diagram-FQU43EPY-CWkdysy2.js} +1 -1
- package/dist/ui/assets/{diagram-G47NLZAW-Cw11Vs2v.js → diagram-G47NLZAW-L7jPJ-r1.js} +1 -1
- package/dist/ui/assets/{diagram-NH7WQ7WH-DSG_UWpP.js → diagram-NH7WQ7WH-fhOp1mr9.js} +1 -1
- package/dist/ui/assets/{diagram-OA4YK3LP-CJz-WOaJ.js → diagram-OA4YK3LP-B7UMZ-5d.js} +1 -1
- package/dist/ui/assets/{diagram-WEI45ONY-CBXjBCvc.js → diagram-WEI45ONY-B-toJrZu.js} +1 -1
- package/dist/ui/assets/{diff-C-vyuMSL.js → diff-CAzYRr8i.js} +1 -1
- package/dist/ui/assets/{django-CJK7lReT.js → django-DPFPMDLP.js} +1 -1
- package/dist/ui/assets/{dns-D02SHjSy.js → dns-kf25JjIl.js} +1 -1
- package/dist/ui/assets/{dockerfile-trKL1Bqu.js → dockerfile-GP7x-Ptc.js} +1 -1
- package/dist/ui/assets/{dos-C52dy-sK.js → dos-DlXSYH18.js} +1 -1
- package/dist/ui/assets/{dsconfig-MzHgzVrt.js → dsconfig-DrXWPwZN.js} +1 -1
- package/dist/ui/assets/{dts-B137dHyX.js → dts-B9QdwGCS.js} +1 -1
- package/dist/ui/assets/{dust-BcbM0r5C.js → dust-CdplAbVJ.js} +1 -1
- package/dist/ui/assets/{ebnf-DkbpI7bK.js → ebnf-BdCAaKiH.js} +1 -1
- package/dist/ui/assets/{ebnfDiagram-CCIWWBDH-aI5uQ7w1.js → ebnfDiagram-CCIWWBDH-l1kXQDgJ.js} +1 -1
- package/dist/ui/assets/{elixir-BOtDuqQy.js → elixir-WekcTu8c.js} +1 -1
- package/dist/ui/assets/{elm-DS8Tyyu-.js → elm-DKbQ6Ryz.js} +1 -1
- package/dist/ui/assets/{erDiagram-Q63AITRT-2vCbd4Mm.js → erDiagram-Q63AITRT-CCeU8uXM.js} +1 -1
- package/dist/ui/assets/{erb-CSXECRbn.js → erb-DvQ2S9Q6.js} +1 -1
- package/dist/ui/assets/{erlang-BMxlgKPJ.js → erlang-C4cAvWW8.js} +1 -1
- package/dist/ui/assets/{erlang-repl-DPzlolPg.js → erlang-repl-BMKmccqv.js} +1 -1
- package/dist/ui/assets/{excel-CW2Uxed3.js → excel-Cz9AeP71.js} +1 -1
- package/dist/ui/assets/{fix-UW84SlCs.js → fix-BFQxvtLz.js} +1 -1
- package/dist/ui/assets/{flix-4UGiPULd.js → flix-D3TgVgHZ.js} +1 -1
- package/dist/ui/assets/{flowDiagram-23GEKE2U-R5PbCbZI.js → flowDiagram-23GEKE2U-CWYmXYgW.js} +1 -1
- package/dist/ui/assets/{fortran-nhHsngdu.js → fortran-CWmklLY8.js} +1 -1
- package/dist/ui/assets/{fsharp-CDyZ1ST5.js → fsharp-V2Eau__H.js} +1 -1
- package/dist/ui/assets/{gams-DHzkkUie.js → gams-KwiGZby9.js} +1 -1
- package/dist/ui/assets/{ganttDiagram-NO4QXBWP-CGcq4Ie_.js → ganttDiagram-NO4QXBWP-Bw1obkG5.js} +1 -1
- package/dist/ui/assets/{gauss-DdhFGyBv.js → gauss-CRI2FQhz.js} +1 -1
- package/dist/ui/assets/{gcode-ImHh8OLA.js → gcode-Bh0f80Zp.js} +1 -1
- package/dist/ui/assets/{gherkin-CTWAW-XR.js → gherkin-DEjcfzYc.js} +1 -1
- package/dist/ui/assets/{gitGraphDiagram-IHSO6WYX-8nM2Z9lD.js → gitGraphDiagram-IHSO6WYX-OfPpsImd.js} +1 -1
- package/dist/ui/assets/{glsl-0_W_27G2.js → glsl-BrYpeP6h.js} +1 -1
- package/dist/ui/assets/{gml-BoDggu2z.js → gml-m818HPGO.js} +1 -1
- package/dist/ui/assets/{go-DYfFygwl.js → go-BX431CyL.js} +1 -1
- package/dist/ui/assets/{golo-Mlq-TCg9.js → golo-D4m-zAId.js} +1 -1
- package/dist/ui/assets/{gradle-BALAinEB.js → gradle-Cq6wabEA.js} +1 -1
- package/dist/ui/assets/{groovy-Dht--dWU.js → groovy-TjcpUbQS.js} +1 -1
- package/dist/ui/assets/{haml-DpMrxg0d.js → haml-DrCR34E5.js} +1 -1
- package/dist/ui/assets/{handlebars-DqHzujNj.js → handlebars-CVS3O2t9.js} +1 -1
- package/dist/ui/assets/{haskell-DZSchhkY.js → haskell-B8q0nv1_.js} +1 -1
- package/dist/ui/assets/{haxe-C0zBRjdy.js → haxe-DXPsSA4C.js} +1 -1
- package/dist/ui/assets/{hsp-D_PwbSv2.js → hsp-CzpLpdV9.js} +1 -1
- package/dist/ui/assets/{htmlbars-DxAFMG8u.js → htmlbars-Cs_-HclQ.js} +1 -1
- package/dist/ui/assets/{http-DvTRF0Do.js → http-CHD9pG0v.js} +1 -1
- package/dist/ui/assets/{hy-lVO-kNyp.js → hy-B-a7tYC4.js} +1 -1
- package/dist/ui/assets/{index-Ci23SMK2.js → index-CJEzcYeX.js} +21 -21
- package/dist/ui/assets/{index-BbMRVVGt.js → index-DBPwq3d-.js} +3 -3
- package/dist/ui/assets/{infoDiagram-FWYZ7A6U-C7WYDlvc.js → infoDiagram-FWYZ7A6U-DNrZutb1.js} +1 -1
- package/dist/ui/assets/{inform7-B8Tf2n8v.js → inform7-47bZAprM.js} +1 -1
- package/dist/ui/assets/{ini-DSoYMaLF.js → ini-D00zSJOc.js} +1 -1
- package/dist/ui/assets/{irpf90-RCPdkB14.js → irpf90-Cb5adfDu.js} +1 -1
- package/dist/ui/assets/{isbl-C2p75PPj.js → isbl-NcFXKDCH.js} +1 -1
- package/dist/ui/assets/{ishikawaDiagram-FXEZZL3T-Cm6jsPlc.js → ishikawaDiagram-FXEZZL3T-DodxL_A3.js} +1 -1
- package/dist/ui/assets/{java-DeY4Yuia.js → java-DdNdnIDI.js} +1 -1
- package/dist/ui/assets/{javascript-3uX1CO-H.js → javascript-CauBsBfA.js} +1 -1
- package/dist/ui/assets/{jboss-cli-DgnD8mEc.js → jboss-cli-Ce_9aKRs.js} +1 -1
- package/dist/ui/assets/{journeyDiagram-5HDEW3XC-DEGV1uA5.js → journeyDiagram-5HDEW3XC-CaegxpYw.js} +1 -1
- package/dist/ui/assets/{json-BV2PmJJp.js → json-BKT_pfo1.js} +1 -1
- package/dist/ui/assets/{julia-B-m8kQm7.js → julia-bWGt_b_o.js} +1 -1
- package/dist/ui/assets/{julia-repl-C51atU2X.js → julia-repl-D-rjMsRC.js} +1 -1
- package/dist/ui/assets/{kanban-definition-HUTT4EX6-BJ4k5iTX.js → kanban-definition-HUTT4EX6-DocIVGMy.js} +1 -1
- package/dist/ui/assets/{kotlin-CYBziIgl.js → kotlin-CW5ChHJw.js} +1 -1
- package/dist/ui/assets/{lasso-Dw2SBMDW.js → lasso-1z4Xsh4l.js} +1 -1
- package/dist/ui/assets/{latex-Dpd35yke.js → latex-DRw0gvLT.js} +1 -1
- package/dist/ui/assets/{ldif-Ce9EZU__.js → ldif-CC6n1ZWZ.js} +1 -1
- package/dist/ui/assets/{leaf-CpkZzQtC.js → leaf-DtycGYP2.js} +1 -1
- package/dist/ui/assets/{less-Uzo7M0FD.js → less-DHfDdpLT.js} +1 -1
- package/dist/ui/assets/{lisp-DWXDlJYm.js → lisp-F7y5QBiG.js} +1 -1
- package/dist/ui/assets/{livecodeserver-uEyPEgVJ.js → livecodeserver-B4rL97ZE.js} +1 -1
- package/dist/ui/assets/{livescript-BQ5f07oK.js → livescript-C1A66vR3.js} +1 -1
- package/dist/ui/assets/{llvm-BAvP-skE.js → llvm-DrJc4jDQ.js} +1 -1
- package/dist/ui/assets/{lsl-DhbiIKu6.js → lsl-ByygfQlb.js} +1 -1
- package/dist/ui/assets/{lua-COxeUeLx.js → lua-C93sHZoB.js} +1 -1
- package/dist/ui/assets/{makefile-DhDx-IgL.js → makefile-CymUCipv.js} +1 -1
- package/dist/ui/assets/{markdown-DX5_kSxq.js → markdown-BLwVli7a.js} +1 -1
- package/dist/ui/assets/{mathematica-D351ZddB.js → mathematica-DMmVw4oA.js} +1 -1
- package/dist/ui/assets/{matlab-CELqIiwF.js → matlab-DJPyR0-0.js} +1 -1
- package/dist/ui/assets/{maxima-B_YljrMm.js → maxima-B_5tEBD-.js} +1 -1
- package/dist/ui/assets/{mel-DltsbNS8.js → mel-B5AJmhET.js} +1 -1
- package/dist/ui/assets/{mercury-axX-5Vjj.js → mercury-DDm_HlP5.js} +1 -1
- package/dist/ui/assets/{mindmap-definition-LN4V7U3C-Bl1EJo6p.js → mindmap-definition-LN4V7U3C-Ck5xDFhg.js} +1 -1
- package/dist/ui/assets/{mipsasm-BxmerVwX.js → mipsasm-BBf0KZxo.js} +1 -1
- package/dist/ui/assets/{mizar-XNHfbpL2.js → mizar-BvkTEip4.js} +1 -1
- package/dist/ui/assets/{mojolicious-BR0U_7zk.js → mojolicious-t81pYOLs.js} +1 -1
- package/dist/ui/assets/{monkey-lLuu9_f-.js → monkey-B5URMcLy.js} +1 -1
- package/dist/ui/assets/{moonscript-DoaZ3DRQ.js → moonscript-Bbi3L3V5.js} +1 -1
- package/dist/ui/assets/{n1ql-C6TId6Iu.js → n1ql-DE8gguiN.js} +1 -1
- package/dist/ui/assets/{nginx-BezSG-JT.js → nginx-ytmzvrnA.js} +1 -1
- package/dist/ui/assets/{nim-Bb0rfXie.js → nim-28PFAP2a.js} +1 -1
- package/dist/ui/assets/{nix-o3sJ5-54.js → nix-DPGZqLl2.js} +1 -1
- package/dist/ui/assets/{node-repl-Sv5hhcTf.js → node-repl-77Ag73Mp.js} +1 -1
- package/dist/ui/assets/{nsis-B45BJFhF.js → nsis-Bd-Jh2mn.js} +1 -1
- package/dist/ui/assets/{objectivec-CzS6zVgh.js → objectivec-Dut0JGtJ.js} +1 -1
- package/dist/ui/assets/{ocaml-Bqux7PyP.js → ocaml-B8NbbL7d.js} +1 -1
- package/dist/ui/assets/{openscad-4Wf6mF84.js → openscad-3haKn8_6.js} +1 -1
- package/dist/ui/assets/{oxygene-Fvwqgkkt.js → oxygene-CzZcXgOW.js} +1 -1
- package/dist/ui/assets/{parser3-zEfzT5wv.js → parser3-B9_LTx13.js} +1 -1
- package/dist/ui/assets/{pegDiagram-2B236MQR-Dd4rIztM.js → pegDiagram-2B236MQR-CnkbIPdt.js} +1 -1
- package/dist/ui/assets/{perl-CsS1Gs6o.js → perl-CJHWWgIf.js} +1 -1
- package/dist/ui/assets/{pf-BNH1jO0R.js → pf-BpbQ2FKx.js} +1 -1
- package/dist/ui/assets/{pgsql-DqPigHrq.js → pgsql-DNFfp1qF.js} +1 -1
- package/dist/ui/assets/{php-CdOl9bRs.js → php-BHGIfWOP.js} +1 -1
- package/dist/ui/assets/{php-template-DPfirJg2.js → php-template-CajBP9Ht.js} +1 -1
- package/dist/ui/assets/{pieDiagram-ENE6RG2P-DGaJNf4S.js → pieDiagram-ENE6RG2P-BOBkdC1f.js} +1 -1
- package/dist/ui/assets/{plaintext-EHbgBKzw.js → plaintext-CdRaKh_-.js} +1 -1
- package/dist/ui/assets/{pony-DmJajtp2.js → pony-BTFZduq1.js} +1 -1
- package/dist/ui/assets/{powershell-DfGzdWR6.js → powershell-gWcxiMpD.js} +1 -1
- package/dist/ui/assets/{processing-BT0ETgPG.js → processing-DbryYP4B.js} +1 -1
- package/dist/ui/assets/{profile-B3I-x5Lt.js → profile-CHL6RTDR.js} +1 -1
- package/dist/ui/assets/{prolog-B_4QJIvI.js → prolog-CAz3V1M6.js} +1 -1
- package/dist/ui/assets/{properties-BFVI8weS.js → properties-BLvgyNqZ.js} +1 -1
- package/dist/ui/assets/{protobuf-PaF39oZZ.js → protobuf-C-bEtiAJ.js} +1 -1
- package/dist/ui/assets/{puppet-BDC6Q8VF.js → puppet-67xqXQgg.js} +1 -1
- package/dist/ui/assets/{purebasic-B2ejrOZK.js → purebasic-DAwxmhyv.js} +1 -1
- package/dist/ui/assets/{python-repl-BslBh_Mx.js → python-repl-BJmGMIGi.js} +1 -1
- package/dist/ui/assets/{python-BOxtIk9H.js → python-z-wrDm9i.js} +1 -1
- package/dist/ui/assets/{q-BPe8WLGW.js → q-CDMPAvF9.js} +1 -1
- package/dist/ui/assets/{qml-BazRR6fz.js → qml-CbE9R2YT.js} +1 -1
- package/dist/ui/assets/{quadrantDiagram-ABIIQ3AL-BQ7aPkHk.js → quadrantDiagram-ABIIQ3AL-BHAhzSkD.js} +1 -1
- package/dist/ui/assets/{r-C09pGNJC.js → r-B1k7RrWh.js} +1 -1
- package/dist/ui/assets/{railroadDiagram-RFXS5EU6-sCO9VGvE.js → railroadDiagram-RFXS5EU6-CsQep5fB.js} +1 -1
- package/dist/ui/assets/{reasonml-S7dzwVL9.js → reasonml-CghNJ9wO.js} +1 -1
- package/dist/ui/assets/{requirementDiagram-TGXJPOKE-CfT5aVZg.js → requirementDiagram-TGXJPOKE-BTTRG_C-.js} +1 -1
- package/dist/ui/assets/{rib-fM2EwQd4.js → rib-BzZqpngB.js} +1 -1
- package/dist/ui/assets/{roboconf-D7AaeyrW.js → roboconf-Bn6vqIcm.js} +1 -1
- package/dist/ui/assets/{routeros-c6Q7mKuq.js → routeros-CWiIB6Sc.js} +1 -1
- package/dist/ui/assets/{rsl-sCpEQBxh.js → rsl-8XzLd-ex.js} +1 -1
- package/dist/ui/assets/{ruby-lcKhz2w9.js → ruby-CMsox328.js} +1 -1
- package/dist/ui/assets/{ruleslanguage-BW4ATOuX.js → ruleslanguage-sJ93c6T4.js} +1 -1
- package/dist/ui/assets/{rust-BbpU-0VY.js → rust-q4W6ksGp.js} +1 -1
- package/dist/ui/assets/{sankeyDiagram-HTMAVEWB-CaweCpvN.js → sankeyDiagram-HTMAVEWB-_xUP3uiO.js} +1 -1
- package/dist/ui/assets/{sas-BPt5R0dx.js → sas-JzGZkjB8.js} +1 -1
- package/dist/ui/assets/{scala-DxW_vqXs.js → scala-BBRUB1bT.js} +1 -1
- package/dist/ui/assets/{scheme-T2Q_jWzT.js → scheme-gFzNu8q5.js} +1 -1
- package/dist/ui/assets/{scilab-DMORg9lV.js → scilab-0XjfEPk4.js} +1 -1
- package/dist/ui/assets/{scss-t0VsosqT.js → scss-CuET4vU_.js} +1 -1
- package/dist/ui/assets/{sequenceDiagram-DBY2YBRQ-o-H_R4zb.js → sequenceDiagram-DBY2YBRQ-DYM-Lf_t.js} +1 -1
- package/dist/ui/assets/{shell-CnCDa1s9.js → shell-DozhgmQz.js} +1 -1
- package/dist/ui/assets/{sizeCapture-X5ZJPWSS-BNLkHGDS.js → sizeCapture-X5ZJPWSS-enn2ctEl.js} +1 -1
- package/dist/ui/assets/{smali-_n-BxJyG.js → smali-DCS5Fxk1.js} +1 -1
- package/dist/ui/assets/{smalltalk-B90PXJwK.js → smalltalk-BHQ0G1rO.js} +1 -1
- package/dist/ui/assets/{sml-fuY7bj-n.js → sml-Bj9p_O7l.js} +1 -1
- package/dist/ui/assets/{sqf-COpwkyuo.js → sqf-VjHINPVc.js} +1 -1
- package/dist/ui/assets/{sql-CXIT0j-x.js → sql-DNYdjhOa.js} +1 -1
- package/dist/ui/assets/{sql_more-C1Oxq4rx.js → sql_more-DF9pwFii.js} +1 -1
- package/dist/ui/assets/{stan-BZEpaHeC.js → stan-DQ7LiXhT.js} +1 -1
- package/dist/ui/assets/{stata-CkBwaiZM.js → stata-4eQacDMD.js} +1 -1
- package/dist/ui/assets/{stateDiagram-2N3HPSRC-BkdnNF7I.js → stateDiagram-2N3HPSRC-CX-Qz4n7.js} +1 -1
- package/dist/ui/assets/stateDiagram-v2-6OUMAXLB-Ch54U9zs.js +1 -0
- package/dist/ui/assets/{step21-fs9N_U3O.js → step21-BgYnjmFD.js} +1 -1
- package/dist/ui/assets/{stylus-Bs_9cvKZ.js → stylus-ZZ5CxZ05.js} +1 -1
- package/dist/ui/assets/{subunit-DvgcOCqJ.js → subunit-Ci5aUEE2.js} +1 -1
- package/dist/ui/assets/{swift-BE4ONsur.js → swift-DQJ4QPRG.js} +1 -1
- package/dist/ui/assets/{swimlanes-5IMT3BWC-uqTAc7o7.js → swimlanes-5IMT3BWC-vO-7dG2V.js} +2 -2
- package/dist/ui/assets/swimlanesDiagram-G3AALYLV-DxwJcNf4.js +8 -0
- package/dist/ui/assets/{taggerscript-BhmPJ64C.js → taggerscript-DM07ScL_.js} +1 -1
- package/dist/ui/assets/{tap-DkueIVKV.js → tap-C6aPKgww.js} +1 -1
- package/dist/ui/assets/{tcl-DyXOOkFi.js → tcl-DjtvddtN.js} +1 -1
- package/dist/ui/assets/{thrift-Dhgbk56T.js → thrift-BoJuHfsC.js} +1 -1
- package/dist/ui/assets/{timeline-definition-FHXFAJF6-DBAk9Ouc.js → timeline-definition-FHXFAJF6-Cn7z-Bqh.js} +1 -1
- package/dist/ui/assets/{tp-BsXyrRGH.js → tp-7mrfAfpj.js} +1 -1
- package/dist/ui/assets/{twig-J0HqIuHw.js → twig-DOOUfGqf.js} +1 -1
- package/dist/ui/assets/{typescript-Dkwvul39.js → typescript-BbC9hUFK.js} +1 -1
- package/dist/ui/assets/{vala-CRGnf8aY.js → vala-XjKwsdD0.js} +1 -1
- package/dist/ui/assets/{vbnet-BtJAUe3z.js → vbnet-Da3MfWx9.js} +1 -1
- package/dist/ui/assets/{vbscript-RgFFNuM3.js → vbscript-DT_81k7v.js} +1 -1
- package/dist/ui/assets/{vbscript-html-DO32Io9Y.js → vbscript-html-DUFh3J5N.js} +1 -1
- package/dist/ui/assets/{vennDiagram-L72KCM5P-BJi6y0hh.js → vennDiagram-L72KCM5P-BnjZCady.js} +1 -1
- package/dist/ui/assets/{verilog-C4h6MLcI.js → verilog-D5Okzav9.js} +1 -1
- package/dist/ui/assets/{vhdl-DOafePyU.js → vhdl-_hcd5jXC.js} +1 -1
- package/dist/ui/assets/{vim-BAdTA9TY.js → vim-BuKTsAic.js} +1 -1
- package/dist/ui/assets/{wardleyDiagram-EHGQE667-C7c_4IJ6.js → wardleyDiagram-EHGQE667-Bgp49v0-.js} +1 -1
- package/dist/ui/assets/{x86asm-BCL6__QH.js → x86asm-DWOIgEnP.js} +1 -1
- package/dist/ui/assets/{xl-qIzE4HOe.js → xl-D6TFf1q5.js} +1 -1
- package/dist/ui/assets/{xml-DE2sJXYU.js → xml-_5JZFtHD.js} +1 -1
- package/dist/ui/assets/{xquery-CQ35t92y.js → xquery-BC0pvaic.js} +1 -1
- package/dist/ui/assets/{xychartDiagram-FW5EYKEG-D2ILn0jL.js → xychartDiagram-FW5EYKEG-DmyzmXyI.js} +1 -1
- package/dist/ui/assets/{yaml-5KaAvG5C.js → yaml-CLzRjl8_.js} +1 -1
- package/dist/ui/assets/{zephir-CbR3Ryfq.js → zephir-nZ6mLYuU.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/utils/__tests__/db-maintenance.test.d.ts +1 -0
- package/dist/utils/__tests__/db-maintenance.test.js +55 -0
- package/dist/utils/__tests__/db-migration.test.js +79 -1
- package/dist/utils/__tests__/request-writer.test.js +264 -3
- package/dist/utils/__tests__/stats-aggregator.test.d.ts +1 -0
- package/dist/utils/__tests__/stats-aggregator.test.js +114 -0
- package/dist/utils/db-maintenance.d.ts +8 -0
- package/dist/utils/db-maintenance.js +18 -0
- package/dist/utils/db.d.ts +1 -0
- package/dist/utils/db.js +62 -0
- package/dist/utils/request-writer.d.ts +23 -1
- package/dist/utils/request-writer.js +200 -14
- package/dist/utils/stats-aggregator.d.ts +1 -1
- package/dist/utils/stats-aggregator.js +44 -27
- package/ecosystem.config.cjs +2 -1
- package/package.json +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.C4sjikMO.js +0 -1
- package/dist/server/middleware/__tests__/request-id.test.js +0 -28
- package/dist/server/middleware/request-id.d.ts +0 -2
- package/dist/server/middleware/request-id.js +0 -12
- package/dist/ui/assets/channel-ChTtxQDg.js +0 -1
- package/dist/ui/assets/classDiagram-OUVF2IWQ-Ccbvamlu.js +0 -1
- package/dist/ui/assets/classDiagram-v2-EOCWNBFH-Ccbvamlu.js +0 -1
- package/dist/ui/assets/stateDiagram-v2-6OUMAXLB-0JnZ1VSQ.js +0 -1
- package/dist/ui/assets/swimlanesDiagram-G3AALYLV-DQRIIEmi.js +0 -8
- /package/dist/server/{middleware/__tests__/request-id.test.d.ts → __tests__/data-push.test.d.ts} +0 -0
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
import { getErrorStatusCode, getErrorMessage, resolveProxyTarget, filterForwardHeaders } from '../proxy';
|
|
1
|
+
import { getErrorStatusCode, getErrorMessage, resolveProxyTarget, filterForwardHeaders, setupProxy } from '../proxy';
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
import http from 'node:http';
|
|
2
4
|
import Database from 'better-sqlite3';
|
|
5
|
+
import { WebSocket } from 'ws';
|
|
6
|
+
import { addDataClient, removeDataClient } from '../data-push';
|
|
3
7
|
jest.mock('@utils/db', () => ({
|
|
4
8
|
__esModule: true,
|
|
5
9
|
getDatabase: jest.fn(),
|
|
6
10
|
initDatabase: jest.fn(),
|
|
7
11
|
closeDatabase: jest.fn()
|
|
8
12
|
}));
|
|
9
|
-
|
|
13
|
+
jest.mock('@config/index', () => ({
|
|
14
|
+
loadConfig: jest.fn().mockResolvedValue({ storage: { maxDays: 7 } })
|
|
15
|
+
}));
|
|
16
|
+
jest.mock('../../utils/mcp-manager', () => ({
|
|
17
|
+
mcpManager: {
|
|
18
|
+
init: jest.fn().mockResolvedValue(undefined),
|
|
19
|
+
close: jest.fn().mockResolvedValue(undefined)
|
|
20
|
+
}
|
|
21
|
+
}));
|
|
22
|
+
jest.mock('../../utils/machine-id', () => ({
|
|
23
|
+
getMachineId: jest.fn().mockResolvedValue('test-machine-id'),
|
|
24
|
+
getMachineIdSync: jest.fn().mockReturnValue('test-machine-id'),
|
|
25
|
+
preloadMachineId: jest.fn().mockResolvedValue(undefined)
|
|
26
|
+
}));
|
|
27
|
+
jest.mock('../../utils/stats-aggregator', () => ({
|
|
28
|
+
aggregateStats: jest.fn()
|
|
29
|
+
}));
|
|
30
|
+
const { getDatabase, initDatabase } = jest.requireMock('@utils/db');
|
|
10
31
|
function makeError(code, message) {
|
|
11
32
|
const error = new Error(message);
|
|
12
33
|
error.code = code;
|
|
@@ -135,3 +156,345 @@ describe('filterForwardHeaders with authStyle', () => {
|
|
|
135
156
|
expect(out['x-api-key']).toBe('inbound-key');
|
|
136
157
|
});
|
|
137
158
|
});
|
|
159
|
+
// --- G2: 逻辑终止识别 + 收割接入(setupProxy 集成测试)---
|
|
160
|
+
// 构造集成测试用内存库:仅建 proxy.ts 运行路径触及的表
|
|
161
|
+
function createIntegrationDb() {
|
|
162
|
+
const db = new Database(':memory:');
|
|
163
|
+
db.exec(`
|
|
164
|
+
CREATE TABLE requests (
|
|
165
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
166
|
+
request_id TEXT UNIQUE NOT NULL,
|
|
167
|
+
session_id TEXT NOT NULL,
|
|
168
|
+
timestamp TEXT NOT NULL,
|
|
169
|
+
method TEXT NOT NULL,
|
|
170
|
+
path TEXT NOT NULL,
|
|
171
|
+
status_code INTEGER NOT NULL,
|
|
172
|
+
response_time INTEGER NOT NULL,
|
|
173
|
+
api_key TEXT,
|
|
174
|
+
proxy_api_key TEXT,
|
|
175
|
+
base_url TEXT,
|
|
176
|
+
model TEXT,
|
|
177
|
+
machine_id TEXT,
|
|
178
|
+
user_agent TEXT,
|
|
179
|
+
input_tokens INTEGER,
|
|
180
|
+
output_tokens INTEGER,
|
|
181
|
+
cache_read_tokens INTEGER,
|
|
182
|
+
cache_creation_tokens INTEGER,
|
|
183
|
+
request_headers TEXT,
|
|
184
|
+
request_body TEXT,
|
|
185
|
+
response_headers TEXT,
|
|
186
|
+
response_events TEXT,
|
|
187
|
+
completed_at TEXT,
|
|
188
|
+
closed_at TEXT,
|
|
189
|
+
error TEXT,
|
|
190
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
191
|
+
);
|
|
192
|
+
CREATE TABLE response_events (
|
|
193
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
194
|
+
request_id TEXT NOT NULL,
|
|
195
|
+
session_id TEXT NOT NULL,
|
|
196
|
+
event_index INTEGER NOT NULL,
|
|
197
|
+
event_type TEXT,
|
|
198
|
+
event_data TEXT NOT NULL
|
|
199
|
+
);
|
|
200
|
+
CREATE TABLE learn_tasks (
|
|
201
|
+
task_id TEXT PRIMARY KEY,
|
|
202
|
+
slug TEXT NOT NULL,
|
|
203
|
+
source_type TEXT NOT NULL,
|
|
204
|
+
label TEXT NOT NULL,
|
|
205
|
+
status TEXT NOT NULL,
|
|
206
|
+
error TEXT,
|
|
207
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
208
|
+
ended_at TEXT
|
|
209
|
+
);
|
|
210
|
+
CREATE TABLE providers (
|
|
211
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
212
|
+
name TEXT UNIQUE NOT NULL,
|
|
213
|
+
base_url TEXT,
|
|
214
|
+
api_key TEXT,
|
|
215
|
+
model TEXT,
|
|
216
|
+
haiku_model TEXT,
|
|
217
|
+
sonnet_model TEXT,
|
|
218
|
+
opus_model TEXT,
|
|
219
|
+
subagent_model TEXT,
|
|
220
|
+
provider TEXT NOT NULL,
|
|
221
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
222
|
+
is_openai_compatible INTEGER NOT NULL DEFAULT 0,
|
|
223
|
+
auth_style TEXT NOT NULL DEFAULT 'bearer',
|
|
224
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
225
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
226
|
+
);
|
|
227
|
+
`);
|
|
228
|
+
return db;
|
|
229
|
+
}
|
|
230
|
+
function sseFrame(event, data) {
|
|
231
|
+
const body = typeof data === 'string' ? data : JSON.stringify(data);
|
|
232
|
+
return event !== null ? `event: ${event}\ndata: ${body}\n\n` : `data: ${body}\n\n`;
|
|
233
|
+
}
|
|
234
|
+
const completeClaudeFrames = [
|
|
235
|
+
sseFrame('message_start', { type: 'message_start', usage: { input_tokens: 10, cache_read_input_tokens: 2 } }),
|
|
236
|
+
sseFrame('ping', { type: 'ping' }),
|
|
237
|
+
sseFrame('message_delta', { type: 'message_delta', usage: { output_tokens: 4 } }),
|
|
238
|
+
sseFrame('message_stop', { type: 'message_stop' })
|
|
239
|
+
];
|
|
240
|
+
const incompleteClaudeFrames = [
|
|
241
|
+
sseFrame('message_start', { type: 'message_start', usage: { input_tokens: 3 } }),
|
|
242
|
+
sseFrame('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }),
|
|
243
|
+
sseFrame('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hel' } })
|
|
244
|
+
];
|
|
245
|
+
// 上游 Web 流:逐帧异步入队;endAfter=false 时入队后保持悬挂(不 close)。
|
|
246
|
+
// singleChunk=true 时所有帧合并为一次 enqueue(模拟真实 TCP 合包,如
|
|
247
|
+
// finish_reason chunk 与紧随的 [DONE] 通常在同一缓冲到达)。
|
|
248
|
+
function createUpstreamWebStream(frames, endAfter = false, singleChunk = false) {
|
|
249
|
+
let controller;
|
|
250
|
+
const webStream = new ReadableStream({
|
|
251
|
+
start(c) { controller = c; }
|
|
252
|
+
});
|
|
253
|
+
void (async () => {
|
|
254
|
+
try {
|
|
255
|
+
if (singleChunk) {
|
|
256
|
+
controller.enqueue(Buffer.from(frames.join(''), 'utf-8'));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
for (const frame of frames) {
|
|
260
|
+
controller.enqueue(Buffer.from(frame, 'utf-8'));
|
|
261
|
+
await new Promise(r => setTimeout(r, 3));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (endAfter)
|
|
265
|
+
controller.close();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// nodeStream.destroy 取消 web 流后 controller 已关闭,忽略后续入队
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
return webStream;
|
|
272
|
+
}
|
|
273
|
+
async function waitFor(predicate, { timeout = 3000, interval = 20 } = {}) {
|
|
274
|
+
const start = Date.now();
|
|
275
|
+
while (Date.now() - start < timeout) {
|
|
276
|
+
const value = predicate();
|
|
277
|
+
if (value !== undefined)
|
|
278
|
+
return value;
|
|
279
|
+
await new Promise(r => setTimeout(r, interval));
|
|
280
|
+
}
|
|
281
|
+
throw new Error('waitFor timed out');
|
|
282
|
+
}
|
|
283
|
+
function mockUpstreamFetch(webStream, requestId) {
|
|
284
|
+
return jest.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response(webStream, {
|
|
285
|
+
status: 200,
|
|
286
|
+
headers: { 'content-type': 'text/event-stream', 'x-request-id': requestId }
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
// 挂一个假数据客户端捕获 WS 广播(request new/update 等)
|
|
290
|
+
let mockWs;
|
|
291
|
+
function attachMockWs() {
|
|
292
|
+
const sent = [];
|
|
293
|
+
mockWs = {
|
|
294
|
+
readyState: WebSocket.OPEN,
|
|
295
|
+
send: jest.fn((payload) => { sent.push(JSON.parse(payload)); })
|
|
296
|
+
};
|
|
297
|
+
addDataClient(mockWs);
|
|
298
|
+
return sent;
|
|
299
|
+
}
|
|
300
|
+
const proxyConfig = {
|
|
301
|
+
target: 'https://upstream.example.com/apps/anthropic',
|
|
302
|
+
changeOrigin: true
|
|
303
|
+
};
|
|
304
|
+
const requestPayload = JSON.stringify({
|
|
305
|
+
model: 'claude-sonnet-4-20250514',
|
|
306
|
+
max_tokens: 16,
|
|
307
|
+
messages: [{ role: 'user', content: 'hi' }]
|
|
308
|
+
});
|
|
309
|
+
describe('setupProxy SSE lifecycle (G2)', () => {
|
|
310
|
+
let db;
|
|
311
|
+
let fastify;
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
jest.clearAllMocks();
|
|
314
|
+
db = createIntegrationDb();
|
|
315
|
+
getDatabase.mockReturnValue(db);
|
|
316
|
+
initDatabase.mockReturnValue(db);
|
|
317
|
+
fastify = Fastify();
|
|
318
|
+
});
|
|
319
|
+
afterEach(async () => {
|
|
320
|
+
delete process.env.CCM_STREAM_IDLE_TIMEOUT_MS;
|
|
321
|
+
if (mockWs) {
|
|
322
|
+
removeDataClient(mockWs);
|
|
323
|
+
mockWs = undefined;
|
|
324
|
+
}
|
|
325
|
+
await fastify.close();
|
|
326
|
+
db.close();
|
|
327
|
+
});
|
|
328
|
+
test('message_stop 已发出后上游悬挂:行落 completed_at,不标中断,客户端不悬挂', async () => {
|
|
329
|
+
await setupProxy(fastify, proxyConfig);
|
|
330
|
+
mockUpstreamFetch(createUpstreamWebStream(completeClaudeFrames), 'cloud-req-1');
|
|
331
|
+
const response = await fastify.inject({
|
|
332
|
+
method: 'POST',
|
|
333
|
+
url: '/v1/messages',
|
|
334
|
+
headers: { 'content-type': 'application/json' },
|
|
335
|
+
payload: requestPayload
|
|
336
|
+
});
|
|
337
|
+
expect(response.statusCode).toBe(200);
|
|
338
|
+
expect(response.payload).toContain('message_stop');
|
|
339
|
+
const row = db.prepare('SELECT * FROM requests WHERE request_id = ?').get('cloud-req-1');
|
|
340
|
+
expect(row).toBeDefined();
|
|
341
|
+
expect(row.completed_at).not.toBeNull();
|
|
342
|
+
expect(row.closed_at).toBeNull();
|
|
343
|
+
expect(row.error).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
test('无 message_stop 的 idle 超时仍标中断(closed_at + error)', async () => {
|
|
346
|
+
// 测试专用时长覆盖:真实定时器,只缩短看门狗超时
|
|
347
|
+
process.env.CCM_STREAM_IDLE_TIMEOUT_MS = '80';
|
|
348
|
+
await setupProxy(fastify, proxyConfig);
|
|
349
|
+
mockUpstreamFetch(createUpstreamWebStream(incompleteClaudeFrames), 'cloud-req-2');
|
|
350
|
+
// 走真实 TCP:中断分支 pipe 不结束客户端(保持既有语义),客户端自行判超时
|
|
351
|
+
await fastify.listen({ port: 0, host: '127.0.0.1' });
|
|
352
|
+
const port = fastify.server.address().port;
|
|
353
|
+
const clientReq = http.request({
|
|
354
|
+
port,
|
|
355
|
+
host: '127.0.0.1',
|
|
356
|
+
method: 'POST',
|
|
357
|
+
path: '/v1/messages',
|
|
358
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(requestPayload) }
|
|
359
|
+
});
|
|
360
|
+
clientReq.on('error', () => { });
|
|
361
|
+
clientReq.end(requestPayload);
|
|
362
|
+
// 客户端收到数据后看门狗触发,不再有数据;保底 1s 后自毁连接
|
|
363
|
+
const clientTimer = setTimeout(() => clientReq.destroy(), 1000);
|
|
364
|
+
clientReq.on('close', () => clearTimeout(clientTimer));
|
|
365
|
+
const row = await waitFor(() => {
|
|
366
|
+
const r = db.prepare('SELECT * FROM requests LIMIT 1').get();
|
|
367
|
+
return r && r.closed_at ? r : undefined;
|
|
368
|
+
});
|
|
369
|
+
expect(row.completed_at).toBeNull();
|
|
370
|
+
expect(row.closed_at).not.toBeNull();
|
|
371
|
+
expect(row.error).toContain('idle timeout');
|
|
372
|
+
expect(row.status_code).toBe(200);
|
|
373
|
+
});
|
|
374
|
+
test('OpenAI 流 finish_reason + [DONE] 后上游悬挂:行落 completed_at', async () => {
|
|
375
|
+
db.prepare(`
|
|
376
|
+
INSERT INTO providers (name, base_url, api_key, model, provider, is_default, is_openai_compatible, auth_style)
|
|
377
|
+
VALUES ('openai-test', 'https://openai.example.com/v1', 'k', 'gpt-4o', 'openai', 1, 1, 'bearer')
|
|
378
|
+
`).run();
|
|
379
|
+
await setupProxy(fastify, proxyConfig);
|
|
380
|
+
const openaiFrames = [
|
|
381
|
+
sseFrame(null, { id: 'chat-1', choices: [{ delta: { role: 'assistant' }, finish_reason: null }] }),
|
|
382
|
+
sseFrame(null, { id: 'chat-1', choices: [{ delta: { content: 'hi' }, finish_reason: null }] }),
|
|
383
|
+
sseFrame(null, { id: 'chat-1', choices: [{ delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: 5, completion_tokens: 7 } }),
|
|
384
|
+
sseFrame(null, '[DONE]')
|
|
385
|
+
];
|
|
386
|
+
const fetchSpy = mockUpstreamFetch(createUpstreamWebStream(openaiFrames, false, true), 'cloud-openai-1');
|
|
387
|
+
const response = await fastify.inject({
|
|
388
|
+
method: 'POST',
|
|
389
|
+
url: '/v1/messages',
|
|
390
|
+
headers: { 'content-type': 'application/json' },
|
|
391
|
+
payload: requestPayload
|
|
392
|
+
});
|
|
393
|
+
expect(response.statusCode).toBe(200);
|
|
394
|
+
expect(response.payload).toContain('message_stop');
|
|
395
|
+
expect(fetchSpy.mock.calls[0][0]).toBe('https://openai.example.com/v1/chat/completions');
|
|
396
|
+
const row = db.prepare('SELECT * FROM requests WHERE request_id = ?').get('cloud-openai-1');
|
|
397
|
+
expect(row).toBeDefined();
|
|
398
|
+
expect(row.completed_at).not.toBeNull();
|
|
399
|
+
expect(row.closed_at).toBeNull();
|
|
400
|
+
expect(row.error).toBeNull();
|
|
401
|
+
});
|
|
402
|
+
test('message_stop 到达后客户端断开:内容已完整交付,仍走 complete', async () => {
|
|
403
|
+
await setupProxy(fastify, proxyConfig);
|
|
404
|
+
mockUpstreamFetch(createUpstreamWebStream(completeClaudeFrames), 'cloud-req-3');
|
|
405
|
+
await fastify.listen({ port: 0, host: '127.0.0.1' });
|
|
406
|
+
const port = fastify.server.address().port;
|
|
407
|
+
await new Promise((resolve, reject) => {
|
|
408
|
+
const clientReq = http.request({
|
|
409
|
+
port,
|
|
410
|
+
host: '127.0.0.1',
|
|
411
|
+
method: 'POST',
|
|
412
|
+
path: '/v1/messages',
|
|
413
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(requestPayload) }
|
|
414
|
+
});
|
|
415
|
+
clientReq.on('error', () => { });
|
|
416
|
+
clientReq.on('response', (res) => {
|
|
417
|
+
res.on('data', (chunk) => {
|
|
418
|
+
if (chunk.toString().includes('message_stop'))
|
|
419
|
+
clientReq.destroy();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
clientReq.on('close', () => resolve());
|
|
423
|
+
setTimeout(() => reject(new Error('client did not close')), 3000).unref();
|
|
424
|
+
clientReq.end(requestPayload);
|
|
425
|
+
});
|
|
426
|
+
const row = await waitFor(() => {
|
|
427
|
+
const r = db.prepare('SELECT * FROM requests WHERE request_id = ?').get('cloud-req-3');
|
|
428
|
+
return r && r.completed_at ? r : undefined;
|
|
429
|
+
});
|
|
430
|
+
expect(row.closed_at).toBeNull();
|
|
431
|
+
expect(row.error).toBeNull();
|
|
432
|
+
});
|
|
433
|
+
test('上游只返回 request-id 头(无 x-request-id):completed 行取该值,WS update 的 pendingId 指向 local 行', async () => {
|
|
434
|
+
await setupProxy(fastify, proxyConfig);
|
|
435
|
+
const upstreamId = 'req_upstream_abc';
|
|
436
|
+
jest.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response(createUpstreamWebStream(completeClaudeFrames), { status: 200, headers: { 'content-type': 'text/event-stream', 'request-id': upstreamId } }));
|
|
437
|
+
const sent = attachMockWs();
|
|
438
|
+
const response = await fastify.inject({
|
|
439
|
+
method: 'POST',
|
|
440
|
+
url: '/v1/messages',
|
|
441
|
+
headers: { 'content-type': 'application/json' },
|
|
442
|
+
payload: requestPayload
|
|
443
|
+
});
|
|
444
|
+
expect(response.statusCode).toBe(200);
|
|
445
|
+
const row = db.prepare('SELECT * FROM requests WHERE request_id = ?').get(upstreamId);
|
|
446
|
+
expect(row).toBeDefined();
|
|
447
|
+
expect(row.completed_at).not.toBeNull();
|
|
448
|
+
const localId = sent.find(m => m.type === 'request' && m.action === 'new').data.requestId;
|
|
449
|
+
const update = sent.find(m => m.type === 'request' && m.action === 'update' && m.data.requestId === upstreamId);
|
|
450
|
+
expect(update).toBeDefined();
|
|
451
|
+
expect(update.data.pendingId).toBe(`unknown-session:${localId}`);
|
|
452
|
+
});
|
|
453
|
+
test('Claude 模式上游 204 且 body 为 null:行落终态、WS 收到 completed、不挂起', async () => {
|
|
454
|
+
await setupProxy(fastify, proxyConfig);
|
|
455
|
+
jest.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response(null, {
|
|
456
|
+
status: 204,
|
|
457
|
+
headers: { 'x-request-id': 'cloud-204-1' }
|
|
458
|
+
}));
|
|
459
|
+
const sent = attachMockWs();
|
|
460
|
+
const response = await fastify.inject({
|
|
461
|
+
method: 'POST',
|
|
462
|
+
url: '/v1/messages',
|
|
463
|
+
headers: { 'content-type': 'application/json' },
|
|
464
|
+
payload: requestPayload
|
|
465
|
+
});
|
|
466
|
+
expect(response.statusCode).toBe(204);
|
|
467
|
+
const row = db.prepare('SELECT * FROM requests WHERE request_id = ?').get('cloud-204-1');
|
|
468
|
+
expect(row).toBeDefined();
|
|
469
|
+
expect(row.completed_at).not.toBeNull();
|
|
470
|
+
expect(row.closed_at).toBeNull();
|
|
471
|
+
expect(row.error).toBeNull();
|
|
472
|
+
const update = sent.find(m => m.type === 'request' && m.action === 'update' && m.data.requestId === 'cloud-204-1');
|
|
473
|
+
expect(update).toBeDefined();
|
|
474
|
+
});
|
|
475
|
+
test('OpenAI 模式上游 204 且 body 为 null:行落终态、WS 收到 completed、不挂起', async () => {
|
|
476
|
+
db.prepare(`
|
|
477
|
+
INSERT INTO providers (name, base_url, api_key, model, provider, is_default, is_openai_compatible, auth_style)
|
|
478
|
+
VALUES ('openai-test', 'https://openai.example.com/v1', 'k', 'gpt-4o', 'openai', 1, 1, 'bearer')
|
|
479
|
+
`).run();
|
|
480
|
+
await setupProxy(fastify, proxyConfig);
|
|
481
|
+
jest.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response(null, {
|
|
482
|
+
status: 204,
|
|
483
|
+
headers: { 'x-request-id': 'cloud-204-openai' }
|
|
484
|
+
}));
|
|
485
|
+
const sent = attachMockWs();
|
|
486
|
+
const response = await fastify.inject({
|
|
487
|
+
method: 'POST',
|
|
488
|
+
url: '/v1/messages',
|
|
489
|
+
headers: { 'content-type': 'application/json' },
|
|
490
|
+
payload: requestPayload
|
|
491
|
+
});
|
|
492
|
+
expect(response.statusCode).toBe(204);
|
|
493
|
+
const row = db.prepare('SELECT * FROM requests WHERE request_id = ?').get('cloud-204-openai');
|
|
494
|
+
expect(row).toBeDefined();
|
|
495
|
+
expect(row.completed_at).not.toBeNull();
|
|
496
|
+
expect(row.closed_at).toBeNull();
|
|
497
|
+
const update = sent.find(m => m.type === 'request' && m.action === 'update' && m.data.requestId === 'cloud-204-openai');
|
|
498
|
+
expect(update).toBeDefined();
|
|
499
|
+
});
|
|
500
|
+
});
|
|
@@ -105,22 +105,27 @@ describe('createServer', () => {
|
|
|
105
105
|
expect(response).toBeDefined();
|
|
106
106
|
expect(typeof response.statusCode).toBe('number');
|
|
107
107
|
});
|
|
108
|
-
it('
|
|
108
|
+
it('uses x-request-id header value as request id when header present', async () => {
|
|
109
109
|
server = await createServer();
|
|
110
|
+
server.get('/request-id-echo', async (request) => ({ id: request.id }));
|
|
110
111
|
const response = await server.inject({
|
|
111
112
|
method: 'GET',
|
|
112
|
-
url: '/
|
|
113
|
-
headers: { 'x-request-id': '
|
|
113
|
+
url: '/request-id-echo',
|
|
114
|
+
headers: { 'x-request-id': 'my-trace-123' }
|
|
114
115
|
});
|
|
115
|
-
expect(response).
|
|
116
|
+
expect(JSON.parse(response.body).id).toBe('my-trace-123');
|
|
116
117
|
});
|
|
117
|
-
it('
|
|
118
|
+
it('generates non-default nanoid request id without header, unique per request', async () => {
|
|
118
119
|
server = await createServer();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
server.get('/request-id-echo', async (request) => ({ id: request.id }));
|
|
121
|
+
const response1 = await server.inject({ method: 'GET', url: '/request-id-echo' });
|
|
122
|
+
const response2 = await server.inject({ method: 'GET', url: '/request-id-echo' });
|
|
123
|
+
const id1 = JSON.parse(response1.body).id;
|
|
124
|
+
const id2 = JSON.parse(response2.body).id;
|
|
125
|
+
expect(id1).not.toMatch(/^req-\d+$/);
|
|
126
|
+
expect(id2).not.toMatch(/^req-\d+$/);
|
|
127
|
+
expect(id1).toHaveLength(16);
|
|
128
|
+
expect(id1).not.toBe(id2);
|
|
124
129
|
});
|
|
125
130
|
it('server closes cleanly', async () => {
|
|
126
131
|
server = await createServer();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
|
+
import * as statsAggregator from '@utils/stats-aggregator';
|
|
2
3
|
import { setupUiRoutes } from '../ui-route';
|
|
3
4
|
// Mock PM2 utils
|
|
4
5
|
jest.mock('@utils/pm2', () => ({
|
|
@@ -372,4 +373,46 @@ describe('setupUiRoutes', () => {
|
|
|
372
373
|
expect(body.error).toContain('Disk write failed');
|
|
373
374
|
});
|
|
374
375
|
});
|
|
376
|
+
describe('GET /ui/api/stats', () => {
|
|
377
|
+
let aggregateSpy;
|
|
378
|
+
let getStatsSpy;
|
|
379
|
+
beforeEach(() => {
|
|
380
|
+
aggregateSpy = jest.spyOn(statsAggregator, 'aggregateStats').mockImplementation(() => { });
|
|
381
|
+
getStatsSpy = jest.spyOn(statsAggregator, 'getStatsData').mockReturnValue({ hourly: {}, lastUpdated: null });
|
|
382
|
+
});
|
|
383
|
+
afterEach(() => {
|
|
384
|
+
aggregateSpy.mockRestore();
|
|
385
|
+
getStatsSpy.mockRestore();
|
|
386
|
+
});
|
|
387
|
+
it('读前触发 aggregateStats 并返回 200', async () => {
|
|
388
|
+
const response = await server.inject({ method: 'GET', url: '/ui/api/stats' });
|
|
389
|
+
expect(aggregateSpy).toHaveBeenCalledTimes(1);
|
|
390
|
+
expect(aggregateSpy).toHaveBeenCalledWith(7);
|
|
391
|
+
expect(getStatsSpy).toHaveBeenCalledTimes(1);
|
|
392
|
+
// 必须先聚合再读取
|
|
393
|
+
expect(aggregateSpy.mock.invocationCallOrder[0]).toBeLessThan(getStatsSpy.mock.invocationCallOrder[0]);
|
|
394
|
+
expect(response.statusCode).toBe(200);
|
|
395
|
+
expect(JSON.parse(response.body)).toEqual({ hourly: {}, lastUpdated: null });
|
|
396
|
+
});
|
|
397
|
+
it('聚合失败只记录不阻断读取,仍返回 200', async () => {
|
|
398
|
+
aggregateSpy.mockImplementation(() => { throw new Error('agg boom'); });
|
|
399
|
+
// inject 请求的 request.log 来自 server.log.child(),替换为可观测的 mock
|
|
400
|
+
const logError = jest.fn();
|
|
401
|
+
jest.spyOn(server.log, 'child').mockReturnValue({
|
|
402
|
+
level: 'info',
|
|
403
|
+
fatal: jest.fn(),
|
|
404
|
+
error: logError,
|
|
405
|
+
warn: jest.fn(),
|
|
406
|
+
info: jest.fn(),
|
|
407
|
+
debug: jest.fn(),
|
|
408
|
+
trace: jest.fn(),
|
|
409
|
+
child: jest.fn()
|
|
410
|
+
});
|
|
411
|
+
const response = await server.inject({ method: 'GET', url: '/ui/api/stats' });
|
|
412
|
+
expect(response.statusCode).toBe(200);
|
|
413
|
+
expect(getStatsSpy).toHaveBeenCalledTimes(1);
|
|
414
|
+
// 聚合异常必须记录日志
|
|
415
|
+
expect(logError).toHaveBeenCalledTimes(1);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
375
418
|
});
|
|
@@ -115,7 +115,7 @@ export async function setupChatRoute(fastify, proxyConfig) {
|
|
|
115
115
|
});
|
|
116
116
|
request.log.info(`[chat] response status: ${response.status}`);
|
|
117
117
|
// Extract cloud request ID
|
|
118
|
-
const cloudRequestId = response.headers.get('x-request-id') || request.id;
|
|
118
|
+
const cloudRequestId = response.headers.get('x-request-id') || response.headers.get('request-id') || request.id;
|
|
119
119
|
// Check response type
|
|
120
120
|
const contentType = response.headers.get('content-type') || '';
|
|
121
121
|
const isSSE = contentType.includes('text/event-stream');
|
|
@@ -156,7 +156,7 @@ export async function setupChatRoute(fastify, proxyConfig) {
|
|
|
156
156
|
// Record each loop iteration as a separate request
|
|
157
157
|
await recordIteration({
|
|
158
158
|
sessionId,
|
|
159
|
-
localRequestId: r.headers.get('x-request-id') || `loop_${request.id}_${Date.now()}`,
|
|
159
|
+
localRequestId: r.headers.get('x-request-id') || r.headers.get('request-id') || `loop_${request.id}_${Date.now()}`,
|
|
160
160
|
timestamp: new Date().toISOString(),
|
|
161
161
|
apiKey,
|
|
162
162
|
proxyApiKey,
|
|
@@ -241,7 +241,7 @@ export async function setupChatRoute(fastify, proxyConfig) {
|
|
|
241
241
|
body: JSON.stringify(b),
|
|
242
242
|
signal: createTimeoutSignal(300000, clientSignal)
|
|
243
243
|
});
|
|
244
|
-
const loopRequestId = r.headers.get('x-request-id') || `loop_${request.id}_${Date.now()}`;
|
|
244
|
+
const loopRequestId = r.headers.get('x-request-id') || r.headers.get('request-id') || `loop_${request.id}_${Date.now()}`;
|
|
245
245
|
return { response: r, requestId: loopRequestId };
|
|
246
246
|
},
|
|
247
247
|
streamAndParse: async (loopResponse) => {
|
|
@@ -2,5 +2,7 @@ import { WebSocket } from 'ws';
|
|
|
2
2
|
export declare function addDataClient(ws: WebSocket): void;
|
|
3
3
|
export declare function removeDataClient(ws: WebSocket): void;
|
|
4
4
|
export declare function getDataClientCount(): number;
|
|
5
|
+
export declare function scheduleStatsNotify(delayMs?: number): void;
|
|
6
|
+
export declare function clearStatsNotify(): void;
|
|
5
7
|
export declare function pushNewRequest(summary: any, status?: 'pending' | 'completed'): void;
|
|
6
8
|
export declare function broadcastToDataClients(msg: any): void;
|
package/dist/server/data-push.js
CHANGED
|
@@ -10,6 +10,24 @@ export function removeDataClient(ws) {
|
|
|
10
10
|
export function getDataClientCount() {
|
|
11
11
|
return dataClients.size;
|
|
12
12
|
}
|
|
13
|
+
// stats notify debounce timer(模块级单个,trailing:重复调用重置延迟)
|
|
14
|
+
let statsNotifyTimer = null;
|
|
15
|
+
// 请求终态后延迟广播一次 stats notify,并发终态合并为一次
|
|
16
|
+
export function scheduleStatsNotify(delayMs = 1500) {
|
|
17
|
+
if (statsNotifyTimer)
|
|
18
|
+
clearTimeout(statsNotifyTimer);
|
|
19
|
+
statsNotifyTimer = setTimeout(() => {
|
|
20
|
+
statsNotifyTimer = null;
|
|
21
|
+
broadcast({ type: 'stats', action: 'notify' });
|
|
22
|
+
}, delayMs);
|
|
23
|
+
}
|
|
24
|
+
// 服务关闭时清理 pending 的 debounce timer
|
|
25
|
+
export function clearStatsNotify() {
|
|
26
|
+
if (statsNotifyTimer) {
|
|
27
|
+
clearTimeout(statsNotifyTimer);
|
|
28
|
+
statsNotifyTimer = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
13
31
|
// Broadcast helpers
|
|
14
32
|
export function pushNewRequest(summary, status = 'completed') {
|
|
15
33
|
broadcast({
|
|
@@ -17,6 +35,10 @@ export function pushNewRequest(summary, status = 'completed') {
|
|
|
17
35
|
action: status === 'pending' ? 'new' : 'update',
|
|
18
36
|
data: { ...summary, status }
|
|
19
37
|
});
|
|
38
|
+
// 终态(complete/interrupt/error)后通知前端重取 stats;pending=请求开始不触发
|
|
39
|
+
if (status !== 'pending') {
|
|
40
|
+
scheduleStatsNotify();
|
|
41
|
+
}
|
|
20
42
|
}
|
|
21
43
|
function broadcast(msg) {
|
|
22
44
|
const payload = JSON.stringify(msg);
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
2
3
|
import { loadConfig } from '../config/index.js';
|
|
3
4
|
import { createLogger } from '../logger/index.js';
|
|
4
5
|
import { configureDbPath } from '../utils/db.js';
|
|
@@ -6,7 +7,6 @@ import { setupProxy } from './proxy.js';
|
|
|
6
7
|
import { setupUiRoutes } from './ui-route.js';
|
|
7
8
|
import { setupChatRoute } from './chat-route.js';
|
|
8
9
|
import { setupDocsRoutes } from './docs-route.js';
|
|
9
|
-
import { requestIdMiddleware } from './middleware/request-id.js';
|
|
10
10
|
export async function createServer() {
|
|
11
11
|
const config = await loadConfig();
|
|
12
12
|
if (config.storage?.path) {
|
|
@@ -16,9 +16,10 @@ export async function createServer() {
|
|
|
16
16
|
const fastify = Fastify({
|
|
17
17
|
logger: logger,
|
|
18
18
|
trustProxy: true,
|
|
19
|
-
requestTimeout: 310000
|
|
19
|
+
requestTimeout: 310000,
|
|
20
|
+
genReqId: () => nanoid(16),
|
|
21
|
+
requestIdHeader: 'x-request-id'
|
|
20
22
|
});
|
|
21
|
-
fastify.register(requestIdMiddleware);
|
|
22
23
|
await setupUiRoutes(fastify); // UI routes registered BEFORE proxy to avoid being proxied
|
|
23
24
|
await setupProxy(fastify, config.proxy);
|
|
24
25
|
await setupChatRoute(fastify, config.proxy); // MCP-enhanced chat endpoint
|