@qwen-code/qwen-code 0.19.6 → 0.19.7-nightly.20260708.394c1a289
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/bundled/qc-helper/docs/configuration/model-providers.md +5 -1
- package/bundled/qc-helper/docs/configuration/settings.md +33 -1
- package/bundled/qc-helper/docs/features/channels/_meta.ts +1 -0
- package/bundled/qc-helper/docs/features/channels/overview.md +41 -31
- package/bundled/qc-helper/docs/features/channels/wecom.md +111 -0
- package/bundled/qc-helper/docs/features/code-review.md +16 -6
- package/bundled/qc-helper/docs/features/commands.md +1 -1
- package/bundled/qc-helper/docs/features/hooks.md +6 -0
- package/bundled/qc-helper/docs/features/skills.md +5 -3
- package/bundled/qc-helper/docs/integration-github-action.md +1 -1
- package/bundled/review/DESIGN.md +50 -18
- package/bundled/review/SKILL.md +102 -12
- package/chunks/{MaxSizedBox-SK7CUWZQ.js → MaxSizedBox-XS37LWTD.js} +31 -29
- package/chunks/{StandaloneSessionPicker-G44HGSAP.js → StandaloneSessionPicker-LPALJTKN.js} +44 -41
- package/chunks/{acpAgent-PKCTYWJV.js → acpAgent-4VE3FGJX.js} +769 -366
- package/chunks/{agent-IYMHUXWB.js → agent-CCHERW6L.js} +24 -24
- package/chunks/{agent-headless-L2DAW6I5.js → agent-headless-55N6UBUU.js} +24 -24
- package/chunks/{anthropicContentGenerator-H5ZOEENQ.js → anthropicContentGenerator-AKYPSZIV.js} +7 -7
- package/chunks/{artifact-tool-SLYPWG3C.js → artifact-tool-D233YJFH.js} +27 -8
- package/chunks/{askUserQuestion-OKPTECON.js → askUserQuestion-MKL5ZTMC.js} +2 -2
- package/chunks/{bridge-DF3IJQ5V.js → bridge-U6HXOHE7.js} +34 -32
- package/chunks/{channel-worker-supervisor-JUYQ5Z4R.js → channel-worker-supervisor-CRCVYQIE.js} +1 -1
- package/chunks/{chunk-AVQDCKNF.js → chunk-22MNNNXE.js} +4 -0
- package/chunks/chunk-2DYY5XUZ.js +8223 -0
- package/chunks/chunk-2FHWCKWA.js +262 -0
- package/chunks/{chunk-6BEOH4KK.js → chunk-2NPS2UDH.js} +767 -122
- package/chunks/{chunk-N2IJLN2L.js → chunk-2SWMCXVR.js} +13 -5
- package/chunks/{chunk-OBVBTAX5.js → chunk-2YELHTUE.js} +2 -2
- package/chunks/chunk-332PWN27.js +19 -0
- package/chunks/{chunk-CLQPBI4H.js → chunk-3B26UVCX.js} +132 -26
- package/chunks/{chunk-HCFSKO6K.js → chunk-3QDZDAJG.js} +1 -1
- package/chunks/{chunk-G7OQ3RFJ.js → chunk-42XGFRJS.js} +3 -3
- package/chunks/{chunk-SL2P3JUA.js → chunk-43W4YSO6.js} +1 -12
- package/chunks/{chunk-JA5KWZTJ.js → chunk-44NROQYV.js} +5 -3
- package/chunks/{chunk-BOAWGUY6.js → chunk-4AFOWB47.js} +121 -49
- package/chunks/{chunk-MOUXUCXQ.js → chunk-4KJ2BPIT.js} +1 -1
- package/chunks/{chunk-YTWX5TT2.js → chunk-5FL2K3ZB.js} +1 -1
- package/chunks/{chunk-UNXGEDX2.js → chunk-5OSSOBZC.js} +133 -25
- package/chunks/{chunk-JN2VJ33O.js → chunk-7I4MGFBN.js} +966 -121
- package/chunks/{chunk-T42ACHFI.js → chunk-7LTB54MK.js} +1 -1
- package/chunks/chunk-A2X3KLC3.js +3807 -0
- package/chunks/{chunk-EWALXUAD.js → chunk-AGHU5VRK.js} +8 -0
- package/chunks/{chunk-GLPV75ES.js → chunk-AHNJGLFV.js} +2 -2
- package/chunks/{chunk-IUSKVKH5.js → chunk-AXTPFMRS.js} +1 -1
- package/chunks/{chunk-T22H4V4R.js → chunk-B34OV7K2.js} +1 -1
- package/chunks/{chunk-FZ5NVRVK.js → chunk-B5FOCTEZ.js} +3 -3
- package/chunks/{chunk-3UC7ZC43.js → chunk-B6WUEODT.js} +7 -7
- package/chunks/{chunk-MJKEQSZI.js → chunk-BGL43SJ3.js} +1 -1
- package/chunks/{chunk-OAYOHSGP.js → chunk-BHRU4SDF.js} +4 -4
- package/chunks/{chunk-YPCPZSZB.js → chunk-CCGOFQV3.js} +56 -4
- package/chunks/{chunk-FDY3XOTM.js → chunk-CR5IU2RX.js} +4 -4
- package/chunks/{chunk-LWGLBVWA.js → chunk-CSOTA7KX.js} +1 -1
- package/chunks/{chunk-AGZF43UV.js → chunk-E7HM76PR.js} +7153 -4406
- package/chunks/{chunk-NIOPRZNN.js → chunk-E7KNELIZ.js} +1 -1
- package/chunks/{chunk-YSU33ACT.js → chunk-ELMPRVCB.js} +5 -13
- package/chunks/{chunk-SEZC2725.js → chunk-ER3BKOLB.js} +13 -0
- package/chunks/{chunk-3VJ7MT3N.js → chunk-EXPMGZZV.js} +14 -3
- package/chunks/{chunk-DBF3HTLT.js → chunk-FB2ULV4M.js} +4 -4
- package/chunks/{chunk-RZORUN6X.js → chunk-FEJ2FZ3U.js} +1 -1
- package/chunks/chunk-FK77ILF5.js +2477 -0
- package/chunks/{chunk-DMSNJDWG.js → chunk-FV7425LN.js} +38 -3
- package/chunks/{chunk-ALX6BUYW.js → chunk-G7ZG7CA6.js} +186 -27
- package/chunks/{chunk-2INJBYLJ.js → chunk-GIY6JTTC.js} +6 -6
- package/chunks/{chunk-D5KISTLW.js → chunk-H6EMK6QK.js} +1 -1
- package/chunks/{chunk-KYTQOCDE.js → chunk-HW4N45EE.js} +17 -46
- package/chunks/{chunk-VXGPYF7U.js → chunk-IFEWJXFS.js} +1 -1
- package/chunks/chunk-II73RK2S.js +158 -0
- package/chunks/{chunk-TENOUWY2.js → chunk-IJRL2S3D.js} +3 -3
- package/chunks/{chunk-GQBRGBLS.js → chunk-J43WPR6U.js} +9 -9
- package/chunks/{chunk-SRNPMG7J.js → chunk-JYOWO2F7.js} +16 -10
- package/chunks/{chunk-57NRQM3P.js → chunk-K34DD7FH.js} +2 -2
- package/chunks/{chunk-O2RJARYU.js → chunk-KKWO4IX2.js} +1409 -4088
- package/chunks/{chunk-SOT7OMGL.js → chunk-KT3SI3CQ.js} +2 -2
- package/chunks/{chunk-NUK2PFSU.js → chunk-L7IWK7JY.js} +1 -1
- package/chunks/{chunk-MHQKYQDS.js → chunk-LC4SMGYY.js} +158 -3
- package/chunks/chunk-LGFAYIKX.js +115 -0
- package/chunks/{chunk-MBWLP5OT.js → chunk-LKXDC3AW.js} +162 -40
- package/chunks/{chunk-7Z6UEJA4.js → chunk-ME344634.js} +5 -2
- package/chunks/{chunk-V6NH3CUC.js → chunk-MOQOAH7N.js} +3 -3
- package/chunks/{chunk-FPHA7WIZ.js → chunk-MYZWWRVW.js} +3 -3
- package/chunks/{chunk-YHTNWGRR.js → chunk-MZ7BABX3.js} +1 -1
- package/chunks/chunk-N74BA4VP.js +949 -0
- package/chunks/{chunk-B236XPGF.js → chunk-NQKLOAVJ.js} +29 -2
- package/chunks/{chunk-LXFCHX4X.js → chunk-OU6FPTJZ.js} +312 -25
- package/chunks/{chunk-HTWHAN44.js → chunk-PC4ZXHU2.js} +1 -1
- package/chunks/{chunk-3X76L4UF.js → chunk-PLGONYEL.js} +5 -5
- package/chunks/{chunk-WXUDZHOJ.js → chunk-PVXPDMGP.js} +10 -2
- package/chunks/{chunk-ZO5EA3XJ.js → chunk-QCSLZO6J.js} +1 -1
- package/chunks/{chunk-CEYA7K7C.js → chunk-QHT5USLK.js} +62 -12
- package/chunks/{chunk-NPQWQYU6.js → chunk-QYNNN7BK.js} +1 -1
- package/chunks/{chunk-N4YJQTRL.js → chunk-RFIL6REB.js} +71 -2
- package/chunks/{chunk-KSPVDFE4.js → chunk-SAIILPQ3.js} +0 -3
- package/chunks/{chunk-OIFSBXSP.js → chunk-SJHAUELE.js} +3 -3
- package/chunks/{chunk-BZHLWC6T.js → chunk-SUAKHHK7.js} +2 -2
- package/chunks/{chunk-QOKV6AO4.js → chunk-T2EJHSE3.js} +184 -147
- package/chunks/{chunk-ZI4MGBB5.js → chunk-TBAZMRW3.js} +1 -1
- package/chunks/{chunk-6QVRLKTZ.js → chunk-TBLJNMI7.js} +7 -7
- package/chunks/{chunk-J6ABRKAB.js → chunk-TDGF35EN.js} +2 -2
- package/chunks/chunk-TEYGKLIZ.js +167 -0
- package/chunks/{chunk-TOAUDIS7.js → chunk-TLWA7TPR.js} +6 -2
- package/chunks/chunk-UIDXQNMV.js +13 -0
- package/chunks/{chunk-MX7CTJHR.js → chunk-UZCT5OLN.js} +9 -9
- package/chunks/{chunk-LM3YCCZJ.js → chunk-VEB3NYSO.js} +1 -1
- package/chunks/{chunk-UTL7KABL.js → chunk-VQJ64BIQ.js} +7 -7
- package/chunks/{chunk-C2QKQZEQ.js → chunk-VTCPREC7.js} +3 -3
- package/chunks/{chunk-5ES5YPED.js → chunk-VZ7HY3YU.js} +1 -1
- package/chunks/{chunk-QOMAHFYI.js → chunk-W7EEOR7D.js} +3 -3
- package/chunks/{chunk-NVFJSIAX.js → chunk-WMPVYQ4P.js} +1 -1
- package/chunks/{chunk-V5VTULL5.js → chunk-X265R5C6.js} +3 -3
- package/chunks/{chunk-LKWAVAB5.js → chunk-X2XNQQJI.js} +2 -2
- package/chunks/{chunk-MJKMIQMK.js → chunk-X3YGAX7V.js} +1 -1
- package/chunks/{chunk-5WQF27GW.js → chunk-XQ2GVLNJ.js} +1 -1
- package/chunks/chunk-XUSONNJ4.js +28 -0
- package/chunks/{chunk-DXXNMPJH.js → chunk-YMWIKAEE.js} +460 -93
- package/chunks/{chunk-A34EPIUV.js → chunk-YYU6H66M.js} +18 -15
- package/chunks/{chunk-XH7H6YG4.js → chunk-ZK5EI6NR.js} +4 -107
- package/chunks/{computer-use-5S3NVXRA.js → computer-use-4CTF6WS3.js} +24 -24
- package/chunks/{contextCommand-DPBALZFK.js → contextCommand-Z3XWQCY6.js} +28 -27
- package/chunks/{cron-create-7JYUDTUY.js → cron-create-JSXEIPVS.js} +4 -4
- package/chunks/{cron-delete-YPULQQAK.js → cron-delete-OIFVEPGU.js} +4 -4
- package/chunks/{cron-list-HJIW5MHP.js → cron-list-D4ZX4IOE.js} +11 -7
- package/chunks/{daemon-7XYW4BC3.js → daemon-F3I2RXP6.js} +208 -63
- package/chunks/daemon-status-provider-6A4QCG6Q.js +74 -0
- package/chunks/dist-573HNSVO.js +3217 -0
- package/chunks/{dist-ABQ7VWRR.js → dist-FQ5WRJPL.js} +2 -2
- package/chunks/{dist-JPSVN367.js → dist-IMFFBMAT.js} +6 -3792
- package/chunks/{dist-BV56HW3W.js → dist-OT7G2WNM.js} +250 -1
- package/chunks/{dist-AW5ZYMUI.js → dist-PKFTNUHO.js} +10 -11
- package/chunks/{dist-AAO65DPC.js → dist-YTRKAVUB.js} +1 -1
- package/chunks/{earlyInputCapture-GGPV4IFT.js → earlyInputCapture-7O47HW62.js} +25 -25
- package/chunks/{edit-HJ7ISN4C.js → edit-E32KWLF2.js} +25 -25
- package/chunks/{en-XR3S5VWY.js → en-GA2RMXY5.js} +6 -1
- package/chunks/{enter-worktree-2BQT675D.js → enter-worktree-ZKFFMHUD.js} +24 -24
- package/chunks/{enterPlanMode-SJXHJ3WD.js → enterPlanMode-HLDC6NGX.js} +24 -24
- package/chunks/{errors-EVGER4AZ.js → errors-MTIDDXX7.js} +26 -26
- package/chunks/{exit-worktree-RPDDOSPM.js → exit-worktree-YHY72XA2.js} +24 -24
- package/chunks/{exitPlanMode-BQEROAJ7.js → exitPlanMode-JNXKBZ2C.js} +24 -24
- package/chunks/{fast-path-FX2ICJ2O.js → fast-path-DVAGBROV.js} +5 -2
- package/chunks/{fr-MBMNQSQ6.js → fr-GWNQG4XH.js} +4 -0
- package/chunks/{gemini-EHUXCOUI.js → gemini-2TMHJVRC.js} +444 -256
- package/chunks/{geminiContentGenerator-YDUUKAM2.js → geminiContentGenerator-WCH4GWD3.js} +4 -4
- package/chunks/{glob-3VIGV2DL.js → glob-I6Y4AGKO.js} +52 -28
- package/chunks/{grep-4D3S4NUC.js → grep-3P2J3LU5.js} +24 -24
- package/chunks/handleAutoUpdate-4IVRMRG4.js +65 -0
- package/chunks/{initializer-QZTZSSLW.js → initializer-DVNCPVTC.js} +31 -29
- package/chunks/{ja-HFV5QCHR.js → ja-2HOTA47X.js} +4 -0
- package/chunks/{keychain-token-storage-GPAKRDKQ.js → keychain-token-storage-UJV5XP4Z.js} +2 -2
- package/chunks/{list-WQ24EAMK.js → list-4D6QK2KZ.js} +30 -30
- package/chunks/{loadedSettingsAdapter-XV74KOSR.js → loadedSettingsAdapter-GNGELH6Q.js} +28 -28
- package/chunks/{loop-wakeup-34YK7T7O.js → loop-wakeup-OHWEMFP5.js} +5 -5
- package/chunks/{ls-MPD5OJUX.js → ls-L654WOAT.js} +4 -4
- package/chunks/{lsp-72PWTDW7.js → lsp-M35KE54Y.js} +2 -2
- package/chunks/{mcp-CO3MKM7N.js → mcp-2IL72YJP.js} +28 -28
- package/chunks/{monitor-GB56T65A.js → monitor-BLVCZAVX.js} +32 -27
- package/chunks/nonInteractiveCli-UD3V7UOO.js +116 -0
- package/chunks/{notebook-edit-VMXAAE3R.js → notebook-edit-5TVMWIWY.js} +25 -25
- package/chunks/{openaiContentGenerator-GM77FLII.js → openaiContentGenerator-5LJ5A7YY.js} +12 -12
- package/chunks/{pidfile-HFXSHR64.js → pidfile-AN5JQJ2T.js} +25 -25
- package/chunks/{qwenContentGenerator-QSCXQIKJ.js → qwenContentGenerator-E2JSB4FJ.js} +26 -26
- package/chunks/{qwenOAuth2-LWLA6MQG.js → qwenOAuth2-ZV7YLMND.js} +5 -5
- package/chunks/{read-file-SVH6CGQY.js → read-file-N5DEFUDU.js} +8 -8
- package/chunks/{read-mcp-resource-OWOBJY72.js → read-mcp-resource-CNQHBSJM.js} +2 -2
- package/chunks/{record-artifact-URK3YUDJ.js → record-artifact-PP3LKHTS.js} +2 -2
- package/chunks/{ripGrep-JD5O2FLX.js → ripGrep-BLJHTHKE.js} +24 -24
- package/chunks/{run-qwen-serve-FADLRJUN.js → run-qwen-serve-4JDO64RN.js} +785 -35
- package/chunks/{scheduler-Q65SB7DJ.js → scheduler-FQR4XA7J.js} +24 -24
- package/chunks/{send-message-L7OLQEKH.js → send-message-OA6MV4EU.js} +3 -3
- package/chunks/{serve-6ZXAMQ4M.js → serve-ZOCFC7YM.js} +29 -29
- package/chunks/{server-WKDQA6XR.js → server-GEOYEPYT.js} +2504 -439
- package/chunks/{session-STWOX7RA.js → session-NJCONHGX.js} +66 -61
- package/chunks/{settings-LC4CCWQQ.js → settings-QBGGXANI.js} +27 -27
- package/chunks/{shell-FEG3RLPW.js → shell-2DVPSEKT.js} +24 -24
- package/chunks/{skill-XTXAMDAP.js → skill-HIPXUHV7.js} +24 -12
- package/chunks/{spawnChannel-B6JJPBPV.js → spawnChannel-3JYXY52G.js} +27 -27
- package/chunks/{src-HITDGVRM.js → src-NLEX4CUC.js} +91 -36
- package/chunks/{startInteractiveUI-IT423PW4.js → startInteractiveUI-TYGZIU27.js} +6213 -14273
- package/chunks/{syntheticOutput-J3MLCQAR.js → syntheticOutput-F7LSDCAR.js} +3 -3
- package/chunks/{task-create-ELEBZ7H2.js → task-create-RPFZNB4B.js} +7 -7
- package/chunks/{task-list-IEUZLF3Y.js → task-list-TGGJX7EA.js} +5 -5
- package/chunks/{task-stop-QEZW3UDY.js → task-stop-W7NGTMYQ.js} +2 -2
- package/chunks/{task-update-DW36J5P5.js → task-update-5WUG5MVQ.js} +7 -7
- package/chunks/{team-create-DAG6SNHZ.js → team-create-MGX57SZJ.js} +24 -24
- package/chunks/{team-delete-SVITIMBO.js → team-delete-UHQQ2QQX.js} +5 -5
- package/chunks/{team-plan-approval-DRDY5OPC.js → team-plan-approval-BO2N7J3E.js} +24 -24
- package/chunks/{theme-manager-N67EZC6B.js → theme-manager-WU7LOA64.js} +25 -25
- package/chunks/{todoWrite-SFFQHZYY.js → todoWrite-C4ZAMSR3.js} +4 -4
- package/chunks/{tool-search-CMDJ5HGM.js → tool-search-CLBKVEVK.js} +11 -28
- package/chunks/{trustedFolders-JD4QX6UJ.js → trustedFolders-GAQM2GHB.js} +25 -25
- package/chunks/updateCheck-RIK2YIQE.js +65 -0
- package/chunks/{validateNonInterActiveAuth-ADDCGZ6A.js → validateNonInterActiveAuth-NS3GBIJZ.js} +61 -56
- package/chunks/{version-VAONYVE5.js → version-B3RSLP54.js} +2 -1
- package/chunks/{web-fetch-SXAXJHIO.js → web-fetch-UYGMOHW4.js} +5 -5
- package/chunks/{workflow-DG2BR42M.js → workflow-7APPIGND.js} +25 -25
- package/chunks/{workspace-providers-status-XGW5OZTD.js → workspace-providers-status-SC67GLMY.js} +30 -30
- package/chunks/{workspace-service-GX3GGZLU.js → workspace-service-ZJHF22WA.js} +35 -35
- package/chunks/{workspace-skills-status-LFK5WV2V.js → workspace-skills-status-NJEK6P6Y.js} +29 -29
- package/chunks/{write-file-UR7FXWOU.js → write-file-R6Z3VLRY.js} +25 -25
- package/chunks/{zh-FSVJTXA4.js → zh-7XVVRTDH.js} +6 -1
- package/chunks/{zh-TW-66MG4LWL.js → zh-TW-DPH554OG.js} +6 -1
- package/cli.js +19 -11
- package/locales/en.js +9 -2
- package/locales/fr.js +6 -0
- package/locales/ja.js +6 -0
- package/locales/zh-TW.js +9 -2
- package/locales/zh.js +9 -2
- package/node_modules/@qwen-code/audio-capture/package.json +1 -1
- package/node_modules/@qwen-code/audio-capture/prebuilds/darwin-arm64/@qwen-code+audio-capture.node +0 -0
- package/node_modules/@qwen-code/audio-capture/prebuilds/darwin-x64/@qwen-code+audio-capture.node +0 -0
- package/node_modules/@qwen-code/audio-capture/prebuilds/win32-x64/@qwen-code+audio-capture.node +0 -0
- package/package.json +3 -3
- package/web-shell/assets/{arc-CUpe0ibu.js → arc-C7hjaa7g.js} +1 -1
- package/web-shell/assets/{architectureDiagram-3BPJPVTR-JvyrY_mK.js → architectureDiagram-3BPJPVTR-pm9KtMZY.js} +1 -1
- package/web-shell/assets/{blockDiagram-GPEHLZMM-CK3Y_vom.js → blockDiagram-GPEHLZMM-CApElKsU.js} +1 -1
- package/web-shell/assets/{c4Diagram-AAUBKEIU-DprSnyrf.js → c4Diagram-AAUBKEIU-BeDe0-9N.js} +1 -1
- package/web-shell/assets/channel-BNf1dW-j.js +1 -0
- package/web-shell/assets/{chunk-2J33WTMH-DJtRJPmu.js → chunk-2J33WTMH-DJEkmLdl.js} +1 -1
- package/web-shell/assets/{chunk-4BX2VUAB-ON6Q2-BG.js → chunk-4BX2VUAB-DXX8gdpe.js} +1 -1
- package/web-shell/assets/{chunk-55IACEB6-c60ou5HZ.js → chunk-55IACEB6-DORmk1kQ.js} +1 -1
- package/web-shell/assets/{chunk-727SXJPM-BamtPKxx.js → chunk-727SXJPM-DWmO4mCZ.js} +1 -1
- package/web-shell/assets/{chunk-AQP2D5EJ-CsVgpu-T.js → chunk-AQP2D5EJ-C7LRaYw2.js} +1 -1
- package/web-shell/assets/{chunk-FMBD7UC4-vZrpjAx4.js → chunk-FMBD7UC4-CCdXgyC5.js} +1 -1
- package/web-shell/assets/{chunk-ND2GUHAM-qxyNOd5t.js → chunk-ND2GUHAM-SyL_xvWc.js} +1 -1
- package/web-shell/assets/{chunk-QZHKN3VN-BvTJSM_S.js → chunk-QZHKN3VN-BzOuCKxa.js} +1 -1
- package/web-shell/assets/classDiagram-4FO5ZUOK-uOIGlzgi.js +1 -0
- package/web-shell/assets/classDiagram-v2-Q7XG4LA2-uOIGlzgi.js +1 -0
- package/web-shell/assets/{cose-bilkent-S5V4N54A-1tXB4pju.js → cose-bilkent-S5V4N54A-BMPWyDKc.js} +1 -1
- package/web-shell/assets/{dagre-BM42HDAG-DglBzme8.js → dagre-BM42HDAG-B_GnPWik.js} +1 -1
- package/web-shell/assets/{diagram-2AECGRRQ-DEJpqsWz.js → diagram-2AECGRRQ-fJqMc3S_.js} +1 -1
- package/web-shell/assets/{diagram-5GNKFQAL-CsQ1NrMG.js → diagram-5GNKFQAL-ximV3O9D.js} +1 -1
- package/web-shell/assets/{diagram-KO2AKTUF-Z-3codAi.js → diagram-KO2AKTUF-2vVyjzqw.js} +1 -1
- package/web-shell/assets/{diagram-LMA3HP47-mBuPTA2i.js → diagram-LMA3HP47-Dckcx1sT.js} +1 -1
- package/web-shell/assets/{diagram-OG6HWLK6-tcA3K3s9.js → diagram-OG6HWLK6-BAVv47ga.js} +1 -1
- package/web-shell/assets/{erDiagram-TEJ5UH35-D9bXFRyZ.js → erDiagram-TEJ5UH35-Bn3nf5J0.js} +1 -1
- package/web-shell/assets/{flowDiagram-I6XJVG4X-Ck0Py04s.js → flowDiagram-I6XJVG4X-BIbGF9JT.js} +1 -1
- package/web-shell/assets/{ganttDiagram-6RSMTGT7-rOevCekE.js → ganttDiagram-6RSMTGT7-CDbhb3AH.js} +1 -1
- package/web-shell/assets/{gitGraphDiagram-PVQCEYII-CoPQaLj0.js → gitGraphDiagram-PVQCEYII-CbKG1a_w.js} +1 -1
- package/web-shell/assets/index-CnE2ur2L.js +779 -0
- package/web-shell/assets/index-hSKXrlSE.css +5 -0
- package/web-shell/assets/{infoDiagram-5YYISTIA-a77CUKoI.js → infoDiagram-5YYISTIA-DUFxTy8m.js} +1 -1
- package/web-shell/assets/{ishikawaDiagram-YF4QCWOH-DlLgUAO-.js → ishikawaDiagram-YF4QCWOH-D69hrBG8.js} +1 -1
- package/web-shell/assets/{journeyDiagram-JHISSGLW-BXR5kVAZ.js → journeyDiagram-JHISSGLW-DsvEw51Y.js} +1 -1
- package/web-shell/assets/{kanban-definition-UN3LZRKU-h5CoHZg7.js → kanban-definition-UN3LZRKU-lFFjZEYd.js} +1 -1
- package/web-shell/assets/{linear-BkaREbyD.js → linear-Bo5bWzvo.js} +1 -1
- package/web-shell/assets/{mermaid.core-Bkagz0ng.js → mermaid.core-C_HKMJx5.js} +5 -5
- package/web-shell/assets/{mindmap-definition-RKZ34NQL-D7EMKR_5.js → mindmap-definition-RKZ34NQL-DlV-NUTs.js} +1 -1
- package/web-shell/assets/{pieDiagram-4H26LBE5-DAmPOjsL.js → pieDiagram-4H26LBE5-B2e6FH2a.js} +1 -1
- package/web-shell/assets/{quadrantDiagram-W4KKPZXB-e4WFQkQu.js → quadrantDiagram-W4KKPZXB-D75kSk-b.js} +1 -1
- package/web-shell/assets/{requirementDiagram-4Y6WPE33-v1zo7OiV.js → requirementDiagram-4Y6WPE33-CQ6xakhM.js} +1 -1
- package/web-shell/assets/{sankeyDiagram-5OEKKPKP-Bstm4aW_.js → sankeyDiagram-5OEKKPKP-J_XTKrcY.js} +1 -1
- package/web-shell/assets/{sequenceDiagram-3UESZ5HK-DzcZy6xN.js → sequenceDiagram-3UESZ5HK-D7jTcBtf.js} +1 -1
- package/web-shell/assets/{stateDiagram-AJRCARHV-BEJH6YbH.js → stateDiagram-AJRCARHV-CrlM_OT3.js} +1 -1
- package/web-shell/assets/stateDiagram-v2-BHNVJYJU-Bs4TK_84.js +1 -0
- package/web-shell/assets/{timeline-definition-PNZ67QCA-TI1wH-Pm.js → timeline-definition-PNZ67QCA-5oZi76Ef.js} +1 -1
- package/web-shell/assets/{vennDiagram-CIIHVFJN-YaH8VDMS.js → vennDiagram-CIIHVFJN-DO5YptPK.js} +1 -1
- package/web-shell/assets/{wardley-L42UT6IY-B80co7OP.js → wardley-L42UT6IY-4t1eJDvM.js} +1 -1
- package/web-shell/assets/{wardleyDiagram-YWT4CUSO-4fHNDEMl.js → wardleyDiagram-YWT4CUSO-DKZc661S.js} +1 -1
- package/web-shell/assets/{xychartDiagram-2RQKCTM6-DT3q4Z3J.js → xychartDiagram-2RQKCTM6-su9hp08G.js} +1 -1
- package/web-shell/index.html +2 -2
- package/chunks/chunk-L7QPM2YD.js +0 -481
- package/chunks/daemon-status-provider-2EWTHT7Y.js +0 -73
- package/chunks/nonInteractiveCli-HNZRIM5I.js +0 -111
- package/web-shell/assets/channel-BAxQ2-aZ.js +0 -1
- package/web-shell/assets/classDiagram-4FO5ZUOK-Cvd_xhZI.js +0 -1
- package/web-shell/assets/classDiagram-v2-Q7XG4LA2-Cvd_xhZI.js +0 -1
- package/web-shell/assets/index-Cr7MYW6T.css +0 -5
- package/web-shell/assets/index-DCHPAUp3.js +0 -730
- package/web-shell/assets/stateDiagram-v2-BHNVJYJU-BopifCBI.js +0 -1
|
@@ -0,0 +1,3217 @@
|
|
|
1
|
+
// Force strict mode and setup for ESM
|
|
2
|
+
"use strict";
|
|
3
|
+
import {
|
|
4
|
+
require_axios,
|
|
5
|
+
require_ws
|
|
6
|
+
} from "./chunk-A2X3KLC3.js";
|
|
7
|
+
import "./chunk-QXPI7FEC.js";
|
|
8
|
+
import "./chunk-7RYW5LQV.js";
|
|
9
|
+
import {
|
|
10
|
+
ChannelBase,
|
|
11
|
+
sanitizeLogText
|
|
12
|
+
} from "./chunk-2NPS2UDH.js";
|
|
13
|
+
import "./chunk-VRDMOSQS.js";
|
|
14
|
+
import "./chunk-K5PGHDBN.js";
|
|
15
|
+
import "./chunk-UWCTAVOD.js";
|
|
16
|
+
import "./chunk-5IFG2VC4.js";
|
|
17
|
+
import "./chunk-QN5NZ3UQ.js";
|
|
18
|
+
import "./chunk-BR4QREVK.js";
|
|
19
|
+
import {
|
|
20
|
+
init_esbuild_shims
|
|
21
|
+
} from "./chunk-A4BMJM77.js";
|
|
22
|
+
import {
|
|
23
|
+
__commonJS,
|
|
24
|
+
__name,
|
|
25
|
+
__require,
|
|
26
|
+
__toESM
|
|
27
|
+
} from "./chunk-J2S4EL5Y.js";
|
|
28
|
+
|
|
29
|
+
// node_modules/eventemitter3/index.js
|
|
30
|
+
var require_eventemitter3 = __commonJS({
|
|
31
|
+
"node_modules/eventemitter3/index.js"(exports, module) {
|
|
32
|
+
"use strict";
|
|
33
|
+
init_esbuild_shims();
|
|
34
|
+
var has = Object.prototype.hasOwnProperty;
|
|
35
|
+
var prefix = "~";
|
|
36
|
+
function Events() {
|
|
37
|
+
}
|
|
38
|
+
__name(Events, "Events");
|
|
39
|
+
if (Object.create) {
|
|
40
|
+
Events.prototype = /* @__PURE__ */ Object.create(null);
|
|
41
|
+
if (!new Events().__proto__) prefix = false;
|
|
42
|
+
}
|
|
43
|
+
function EE(fn, context, once) {
|
|
44
|
+
this.fn = fn;
|
|
45
|
+
this.context = context;
|
|
46
|
+
this.once = once || false;
|
|
47
|
+
}
|
|
48
|
+
__name(EE, "EE");
|
|
49
|
+
function addListener(emitter, event, fn, context, once) {
|
|
50
|
+
if (typeof fn !== "function") {
|
|
51
|
+
throw new TypeError("The listener must be a function");
|
|
52
|
+
}
|
|
53
|
+
var listener = new EE(fn, context || emitter, once), evt = prefix ? prefix + event : event;
|
|
54
|
+
if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
|
|
55
|
+
else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
|
|
56
|
+
else emitter._events[evt] = [emitter._events[evt], listener];
|
|
57
|
+
return emitter;
|
|
58
|
+
}
|
|
59
|
+
__name(addListener, "addListener");
|
|
60
|
+
function clearEvent(emitter, evt) {
|
|
61
|
+
if (--emitter._eventsCount === 0) emitter._events = new Events();
|
|
62
|
+
else delete emitter._events[evt];
|
|
63
|
+
}
|
|
64
|
+
__name(clearEvent, "clearEvent");
|
|
65
|
+
function EventEmitter() {
|
|
66
|
+
this._events = new Events();
|
|
67
|
+
this._eventsCount = 0;
|
|
68
|
+
}
|
|
69
|
+
__name(EventEmitter, "EventEmitter");
|
|
70
|
+
EventEmitter.prototype.eventNames = /* @__PURE__ */ __name(function eventNames() {
|
|
71
|
+
var names = [], events, name;
|
|
72
|
+
if (this._eventsCount === 0) return names;
|
|
73
|
+
for (name in events = this._events) {
|
|
74
|
+
if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
|
|
75
|
+
}
|
|
76
|
+
if (Object.getOwnPropertySymbols) {
|
|
77
|
+
return names.concat(Object.getOwnPropertySymbols(events));
|
|
78
|
+
}
|
|
79
|
+
return names;
|
|
80
|
+
}, "eventNames");
|
|
81
|
+
EventEmitter.prototype.listeners = /* @__PURE__ */ __name(function listeners(event) {
|
|
82
|
+
var evt = prefix ? prefix + event : event, handlers = this._events[evt];
|
|
83
|
+
if (!handlers) return [];
|
|
84
|
+
if (handlers.fn) return [handlers.fn];
|
|
85
|
+
for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {
|
|
86
|
+
ee[i] = handlers[i].fn;
|
|
87
|
+
}
|
|
88
|
+
return ee;
|
|
89
|
+
}, "listeners");
|
|
90
|
+
EventEmitter.prototype.listenerCount = /* @__PURE__ */ __name(function listenerCount(event) {
|
|
91
|
+
var evt = prefix ? prefix + event : event, listeners = this._events[evt];
|
|
92
|
+
if (!listeners) return 0;
|
|
93
|
+
if (listeners.fn) return 1;
|
|
94
|
+
return listeners.length;
|
|
95
|
+
}, "listenerCount");
|
|
96
|
+
EventEmitter.prototype.emit = /* @__PURE__ */ __name(function emit(event, a1, a2, a3, a4, a5) {
|
|
97
|
+
var evt = prefix ? prefix + event : event;
|
|
98
|
+
if (!this._events[evt]) return false;
|
|
99
|
+
var listeners = this._events[evt], len = arguments.length, args, i;
|
|
100
|
+
if (listeners.fn) {
|
|
101
|
+
if (listeners.once) this.removeListener(event, listeners.fn, void 0, true);
|
|
102
|
+
switch (len) {
|
|
103
|
+
case 1:
|
|
104
|
+
return listeners.fn.call(listeners.context), true;
|
|
105
|
+
case 2:
|
|
106
|
+
return listeners.fn.call(listeners.context, a1), true;
|
|
107
|
+
case 3:
|
|
108
|
+
return listeners.fn.call(listeners.context, a1, a2), true;
|
|
109
|
+
case 4:
|
|
110
|
+
return listeners.fn.call(listeners.context, a1, a2, a3), true;
|
|
111
|
+
case 5:
|
|
112
|
+
return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
|
|
113
|
+
case 6:
|
|
114
|
+
return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
|
|
115
|
+
}
|
|
116
|
+
for (i = 1, args = new Array(len - 1); i < len; i++) {
|
|
117
|
+
args[i - 1] = arguments[i];
|
|
118
|
+
}
|
|
119
|
+
listeners.fn.apply(listeners.context, args);
|
|
120
|
+
} else {
|
|
121
|
+
var length = listeners.length, j;
|
|
122
|
+
for (i = 0; i < length; i++) {
|
|
123
|
+
if (listeners[i].once) this.removeListener(event, listeners[i].fn, void 0, true);
|
|
124
|
+
switch (len) {
|
|
125
|
+
case 1:
|
|
126
|
+
listeners[i].fn.call(listeners[i].context);
|
|
127
|
+
break;
|
|
128
|
+
case 2:
|
|
129
|
+
listeners[i].fn.call(listeners[i].context, a1);
|
|
130
|
+
break;
|
|
131
|
+
case 3:
|
|
132
|
+
listeners[i].fn.call(listeners[i].context, a1, a2);
|
|
133
|
+
break;
|
|
134
|
+
case 4:
|
|
135
|
+
listeners[i].fn.call(listeners[i].context, a1, a2, a3);
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
if (!args) for (j = 1, args = new Array(len - 1); j < len; j++) {
|
|
139
|
+
args[j - 1] = arguments[j];
|
|
140
|
+
}
|
|
141
|
+
listeners[i].fn.apply(listeners[i].context, args);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}, "emit");
|
|
147
|
+
EventEmitter.prototype.on = /* @__PURE__ */ __name(function on(event, fn, context) {
|
|
148
|
+
return addListener(this, event, fn, context, false);
|
|
149
|
+
}, "on");
|
|
150
|
+
EventEmitter.prototype.once = /* @__PURE__ */ __name(function once(event, fn, context) {
|
|
151
|
+
return addListener(this, event, fn, context, true);
|
|
152
|
+
}, "once");
|
|
153
|
+
EventEmitter.prototype.removeListener = /* @__PURE__ */ __name(function removeListener(event, fn, context, once) {
|
|
154
|
+
var evt = prefix ? prefix + event : event;
|
|
155
|
+
if (!this._events[evt]) return this;
|
|
156
|
+
if (!fn) {
|
|
157
|
+
clearEvent(this, evt);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
var listeners = this._events[evt];
|
|
161
|
+
if (listeners.fn) {
|
|
162
|
+
if (listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context)) {
|
|
163
|
+
clearEvent(this, evt);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
for (var i = 0, events = [], length = listeners.length; i < length; i++) {
|
|
167
|
+
if (listeners[i].fn !== fn || once && !listeners[i].once || context && listeners[i].context !== context) {
|
|
168
|
+
events.push(listeners[i]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
|
|
172
|
+
else clearEvent(this, evt);
|
|
173
|
+
}
|
|
174
|
+
return this;
|
|
175
|
+
}, "removeListener");
|
|
176
|
+
EventEmitter.prototype.removeAllListeners = /* @__PURE__ */ __name(function removeAllListeners(event) {
|
|
177
|
+
var evt;
|
|
178
|
+
if (event) {
|
|
179
|
+
evt = prefix ? prefix + event : event;
|
|
180
|
+
if (this._events[evt]) clearEvent(this, evt);
|
|
181
|
+
} else {
|
|
182
|
+
this._events = new Events();
|
|
183
|
+
this._eventsCount = 0;
|
|
184
|
+
}
|
|
185
|
+
return this;
|
|
186
|
+
}, "removeAllListeners");
|
|
187
|
+
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
|
188
|
+
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
|
|
189
|
+
EventEmitter.prefixed = prefix;
|
|
190
|
+
EventEmitter.EventEmitter = EventEmitter;
|
|
191
|
+
if ("undefined" !== typeof module) {
|
|
192
|
+
module.exports = EventEmitter;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// node_modules/@wecom/aibot-node-sdk/dist/index.cjs.js
|
|
198
|
+
var require_index_cjs = __commonJS({
|
|
199
|
+
"node_modules/@wecom/aibot-node-sdk/dist/index.cjs.js"(exports) {
|
|
200
|
+
"use strict";
|
|
201
|
+
init_esbuild_shims();
|
|
202
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
203
|
+
var eventemitter3 = require_eventemitter3();
|
|
204
|
+
var crypto = __require("crypto");
|
|
205
|
+
var axios = require_axios();
|
|
206
|
+
var WebSocket = require_ws();
|
|
207
|
+
var crypto$1 = __require("node:crypto");
|
|
208
|
+
var WSAuthFailureError = class extends Error {
|
|
209
|
+
static {
|
|
210
|
+
__name(this, "WSAuthFailureError");
|
|
211
|
+
}
|
|
212
|
+
constructor(maxAttempts) {
|
|
213
|
+
super(`Max auth failure attempts exceeded (${maxAttempts})`);
|
|
214
|
+
this.code = "WS_AUTH_FAILURE_EXHAUSTED";
|
|
215
|
+
this.name = "WSAuthFailureError";
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var WSReconnectExhaustedError = class extends Error {
|
|
219
|
+
static {
|
|
220
|
+
__name(this, "WSReconnectExhaustedError");
|
|
221
|
+
}
|
|
222
|
+
constructor(maxAttempts) {
|
|
223
|
+
super(`Max reconnect attempts exceeded (${maxAttempts})`);
|
|
224
|
+
this.code = "WS_RECONNECT_EXHAUSTED";
|
|
225
|
+
this.name = "WSReconnectExhaustedError";
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
exports.MessageType = void 0;
|
|
229
|
+
(function(MessageType) {
|
|
230
|
+
MessageType["Text"] = "text";
|
|
231
|
+
MessageType["Image"] = "image";
|
|
232
|
+
MessageType["Mixed"] = "mixed";
|
|
233
|
+
MessageType["Voice"] = "voice";
|
|
234
|
+
MessageType["File"] = "file";
|
|
235
|
+
MessageType["Video"] = "video";
|
|
236
|
+
})(exports.MessageType || (exports.MessageType = {}));
|
|
237
|
+
var WsCmd = {
|
|
238
|
+
// ========== 开发者 → 企业微信 ==========
|
|
239
|
+
/** 认证订阅 */
|
|
240
|
+
SUBSCRIBE: "aibot_subscribe",
|
|
241
|
+
/** 心跳 */
|
|
242
|
+
HEARTBEAT: "ping",
|
|
243
|
+
/** 回复消息 */
|
|
244
|
+
RESPONSE: "aibot_respond_msg",
|
|
245
|
+
/** 回复欢迎语 */
|
|
246
|
+
RESPONSE_WELCOME: "aibot_respond_welcome_msg",
|
|
247
|
+
/** 更新模板卡片 */
|
|
248
|
+
RESPONSE_UPDATE: "aibot_respond_update_msg",
|
|
249
|
+
/** 主动发送消息 */
|
|
250
|
+
SEND_MSG: "aibot_send_msg",
|
|
251
|
+
/** 上传临时素材 - 初始化 */
|
|
252
|
+
UPLOAD_MEDIA_INIT: "aibot_upload_media_init",
|
|
253
|
+
/** 上传临时素材 - 分片上传 */
|
|
254
|
+
UPLOAD_MEDIA_CHUNK: "aibot_upload_media_chunk",
|
|
255
|
+
/** 上传临时素材 - 完成上传 */
|
|
256
|
+
UPLOAD_MEDIA_FINISH: "aibot_upload_media_finish",
|
|
257
|
+
// ========== 企业微信 → 开发者 ==========
|
|
258
|
+
/** 消息推送回调 */
|
|
259
|
+
CALLBACK: "aibot_msg_callback",
|
|
260
|
+
/** 事件推送回调 */
|
|
261
|
+
EVENT_CALLBACK: "aibot_event_callback"
|
|
262
|
+
};
|
|
263
|
+
exports.TemplateCardType = void 0;
|
|
264
|
+
(function(TemplateCardType) {
|
|
265
|
+
TemplateCardType["TextNotice"] = "text_notice";
|
|
266
|
+
TemplateCardType["NewsNotice"] = "news_notice";
|
|
267
|
+
TemplateCardType["ButtonInteraction"] = "button_interaction";
|
|
268
|
+
TemplateCardType["VoteInteraction"] = "vote_interaction";
|
|
269
|
+
TemplateCardType["MultipleInteraction"] = "multiple_interaction";
|
|
270
|
+
})(exports.TemplateCardType || (exports.TemplateCardType = {}));
|
|
271
|
+
exports.EventType = void 0;
|
|
272
|
+
(function(EventType) {
|
|
273
|
+
EventType["EnterChat"] = "enter_chat";
|
|
274
|
+
EventType["TemplateCardEvent"] = "template_card_event";
|
|
275
|
+
EventType["FeedbackEvent"] = "feedback_event";
|
|
276
|
+
EventType["Disconnected"] = "disconnected_event";
|
|
277
|
+
})(exports.EventType || (exports.EventType = {}));
|
|
278
|
+
var WeComApiClient = class {
|
|
279
|
+
static {
|
|
280
|
+
__name(this, "WeComApiClient");
|
|
281
|
+
}
|
|
282
|
+
constructor(logger, timeout = 1e4) {
|
|
283
|
+
this.logger = logger;
|
|
284
|
+
this.httpClient = axios.create({
|
|
285
|
+
timeout,
|
|
286
|
+
headers: {
|
|
287
|
+
"Content-Type": "application/json"
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 下载文件(返回原始 Buffer 及文件名)
|
|
293
|
+
*/
|
|
294
|
+
async downloadFileRaw(url) {
|
|
295
|
+
this.logger.info("Downloading file...");
|
|
296
|
+
try {
|
|
297
|
+
const response = await this.httpClient.get(url, {
|
|
298
|
+
responseType: "arraybuffer"
|
|
299
|
+
});
|
|
300
|
+
const contentDisposition = response.headers["content-disposition"];
|
|
301
|
+
let filename;
|
|
302
|
+
if (contentDisposition) {
|
|
303
|
+
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;\s]+)/i);
|
|
304
|
+
if (utf8Match) {
|
|
305
|
+
filename = decodeURIComponent(utf8Match[1]);
|
|
306
|
+
} else {
|
|
307
|
+
const match = contentDisposition.match(/filename="?([^";\s]+)"?/i);
|
|
308
|
+
if (match) {
|
|
309
|
+
filename = decodeURIComponent(match[1]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
this.logger.info("File downloaded successfully");
|
|
314
|
+
return { buffer: Buffer.from(response.data), filename };
|
|
315
|
+
} catch (error) {
|
|
316
|
+
this.logger.error("File download failed:", error.message);
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
function generateRandomString(length = 8) {
|
|
322
|
+
return crypto.randomBytes(Math.ceil(length / 2)).toString("hex").substring(0, length);
|
|
323
|
+
}
|
|
324
|
+
__name(generateRandomString, "generateRandomString");
|
|
325
|
+
function generateReqId(prefix) {
|
|
326
|
+
const timestamp = Date.now();
|
|
327
|
+
const random = generateRandomString();
|
|
328
|
+
return `${prefix}_${timestamp}_${random}`;
|
|
329
|
+
}
|
|
330
|
+
__name(generateReqId, "generateReqId");
|
|
331
|
+
var DEFAULT_WS_URL = "wss://openws.work.weixin.qq.com";
|
|
332
|
+
var WsConnectionManager = class {
|
|
333
|
+
static {
|
|
334
|
+
__name(this, "WsConnectionManager");
|
|
335
|
+
}
|
|
336
|
+
constructor(logger, heartbeatInterval = 3e4, reconnectBaseDelay = 1e3, maxReconnectAttempts = 10, wsUrl, wsOptions, maxReplyQueueSize, maxAuthFailureAttempts) {
|
|
337
|
+
this.ws = null;
|
|
338
|
+
this.heartbeatTimer = null;
|
|
339
|
+
this.reconnectAttempts = 0;
|
|
340
|
+
this.authFailureAttempts = 0;
|
|
341
|
+
this.isManualClose = false;
|
|
342
|
+
this.lastCloseWasAuthFailure = false;
|
|
343
|
+
this.botId = "";
|
|
344
|
+
this.botSecret = "";
|
|
345
|
+
this.extraAuthParams = {};
|
|
346
|
+
this.missedPongCount = 0;
|
|
347
|
+
this.maxMissedPong = 2;
|
|
348
|
+
this.reconnectBaseDelay = 1e3;
|
|
349
|
+
this.reconnectMaxDelay = 3e4;
|
|
350
|
+
this.reconnectTimer = null;
|
|
351
|
+
this.replyQueues = /* @__PURE__ */ new Map();
|
|
352
|
+
this.pendingAcks = /* @__PURE__ */ new Map();
|
|
353
|
+
this.pendingAckSeq = 0;
|
|
354
|
+
this.replyAckTimeout = 5e3;
|
|
355
|
+
this.maxReplyQueueSize = 500;
|
|
356
|
+
this.onConnected = null;
|
|
357
|
+
this.onAuthenticated = null;
|
|
358
|
+
this.onDisconnected = null;
|
|
359
|
+
this.onMessage = null;
|
|
360
|
+
this.onReconnecting = null;
|
|
361
|
+
this.onError = null;
|
|
362
|
+
this.onServerDisconnect = null;
|
|
363
|
+
this.logger = logger;
|
|
364
|
+
this.heartbeatInterval = heartbeatInterval;
|
|
365
|
+
this.reconnectBaseDelay = reconnectBaseDelay;
|
|
366
|
+
this.maxReconnectAttempts = maxReconnectAttempts;
|
|
367
|
+
this.maxAuthFailureAttempts = maxAuthFailureAttempts ?? 5;
|
|
368
|
+
this.wsUrl = wsUrl || DEFAULT_WS_URL;
|
|
369
|
+
this.wsOptions = wsOptions || {};
|
|
370
|
+
if (maxReplyQueueSize !== void 0) {
|
|
371
|
+
this.maxReplyQueueSize = maxReplyQueueSize;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 设置认证凭证
|
|
376
|
+
*/
|
|
377
|
+
setCredentials(botId, botSecret, extraAuthParams) {
|
|
378
|
+
this.botId = botId;
|
|
379
|
+
this.botSecret = botSecret;
|
|
380
|
+
this.extraAuthParams = extraAuthParams || {};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 建立 WebSocket 连接(使用 SDK 内置默认地址)
|
|
384
|
+
*/
|
|
385
|
+
connect() {
|
|
386
|
+
this.isManualClose = false;
|
|
387
|
+
if (this.reconnectTimer) {
|
|
388
|
+
clearTimeout(this.reconnectTimer);
|
|
389
|
+
this.reconnectTimer = null;
|
|
390
|
+
}
|
|
391
|
+
if (this.ws) {
|
|
392
|
+
this.ws.removeAllListeners();
|
|
393
|
+
this.ws.terminate();
|
|
394
|
+
this.ws = null;
|
|
395
|
+
}
|
|
396
|
+
this.logger.info(`Connecting to WebSocket: ${this.wsUrl}...`);
|
|
397
|
+
try {
|
|
398
|
+
this.ws = new WebSocket(this.wsUrl, this.wsOptions);
|
|
399
|
+
this.setupEventHandlers();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
this.logger.error("Failed to create WebSocket connection:", error.message);
|
|
402
|
+
this.onError?.(error);
|
|
403
|
+
this.scheduleReconnect();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* 设置 WebSocket 事件处理器
|
|
408
|
+
*/
|
|
409
|
+
setupEventHandlers() {
|
|
410
|
+
if (!this.ws)
|
|
411
|
+
return;
|
|
412
|
+
this.ws.on("open", () => {
|
|
413
|
+
this.logger.info("WebSocket connection established, sending auth...");
|
|
414
|
+
this.missedPongCount = 0;
|
|
415
|
+
this.lastCloseWasAuthFailure = false;
|
|
416
|
+
this.sendAuth();
|
|
417
|
+
this.onConnected?.();
|
|
418
|
+
});
|
|
419
|
+
this.ws.on("message", (data) => {
|
|
420
|
+
try {
|
|
421
|
+
const raw = data.toString().replace(/[\x00-\x08\x0B-\x0D\x0E-\x1F]/g, "");
|
|
422
|
+
const frame = JSON.parse(raw);
|
|
423
|
+
this.handleFrame(frame);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
this.logger.error("Failed to parse WebSocket message:", error.message);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
this.ws.on("close", (code, reason) => {
|
|
429
|
+
const reasonStr = reason.toString() || `code: ${code}`;
|
|
430
|
+
this.logger.warn(`WebSocket connection closed: ${reasonStr}`);
|
|
431
|
+
this.stopHeartbeat();
|
|
432
|
+
this.clearPendingMessages(`WebSocket connection closed (${reasonStr})`);
|
|
433
|
+
this.onDisconnected?.(reasonStr);
|
|
434
|
+
this.ws = null;
|
|
435
|
+
if (!this.isManualClose) {
|
|
436
|
+
this.scheduleReconnect();
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
this.ws.on("error", (error) => {
|
|
440
|
+
this.logger.error("WebSocket error:", error.message);
|
|
441
|
+
this.onError?.(error);
|
|
442
|
+
});
|
|
443
|
+
this.ws.on("ping", () => {
|
|
444
|
+
this.ws?.pong();
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 发送认证帧
|
|
449
|
+
*
|
|
450
|
+
* 格式:{ cmd: "aibot_subscribe", headers: { req_id }, body: { secret, bot_id } }
|
|
451
|
+
*/
|
|
452
|
+
sendAuth() {
|
|
453
|
+
try {
|
|
454
|
+
this.send({
|
|
455
|
+
cmd: WsCmd.SUBSCRIBE,
|
|
456
|
+
headers: { req_id: generateReqId(WsCmd.SUBSCRIBE) },
|
|
457
|
+
body: {
|
|
458
|
+
bot_id: this.botId,
|
|
459
|
+
secret: this.botSecret,
|
|
460
|
+
...this.extraAuthParams
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
this.logger.info("Auth frame sent");
|
|
464
|
+
} catch (error) {
|
|
465
|
+
this.logger.error("Failed to send auth frame:", error.message);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* 处理收到的帧数据
|
|
470
|
+
*
|
|
471
|
+
* 接收帧结构:
|
|
472
|
+
* - 消息推送:{ cmd: "aibot_msg_callback", headers: { req_id }, body: { ... } }
|
|
473
|
+
* - 认证/心跳响应:{ headers: { req_id }, errcode: 0, errmsg: "ok" }
|
|
474
|
+
*/
|
|
475
|
+
handleFrame(frame) {
|
|
476
|
+
const cmd = frame.cmd || "";
|
|
477
|
+
const reqId = frame.headers?.req_id || "";
|
|
478
|
+
if (frame.cmd === WsCmd.CALLBACK) {
|
|
479
|
+
this.logger.debug(`[server -> plugin] cmd=${cmd}, reqId=${reqId}, body=${JSON.stringify(frame.body)}`);
|
|
480
|
+
this.onMessage?.(frame);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (frame.cmd === WsCmd.EVENT_CALLBACK) {
|
|
484
|
+
this.logger.debug(`[server -> plugin] cmd=${cmd}, reqId=${reqId}, body=${JSON.stringify(frame.body)}`);
|
|
485
|
+
if (frame.body?.event?.eventtype === "disconnected_event") {
|
|
486
|
+
this.logger.warn("Received disconnected_event: a new connection has been established, this connection will be closed by server");
|
|
487
|
+
this.onMessage?.(frame);
|
|
488
|
+
this.stopHeartbeat();
|
|
489
|
+
this.clearPendingMessages("Server disconnected due to new connection");
|
|
490
|
+
this.isManualClose = true;
|
|
491
|
+
this.onServerDisconnect?.("New connection established, server disconnected this connection");
|
|
492
|
+
if (this.ws) {
|
|
493
|
+
this.ws.removeAllListeners();
|
|
494
|
+
this.ws.terminate();
|
|
495
|
+
this.ws = null;
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
this.onMessage?.(frame);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const actualReqId = frame.headers?.req_id || "";
|
|
503
|
+
if (actualReqId.startsWith(WsCmd.SUBSCRIBE)) {
|
|
504
|
+
if (frame.errcode !== 0) {
|
|
505
|
+
this.logger.error(`Authentication failed: errcode=${frame.errcode}, errmsg=${frame.errmsg}`);
|
|
506
|
+
this.onError?.(new Error(`Authentication failed: ${frame.errmsg} (code: ${frame.errcode})`));
|
|
507
|
+
this.lastCloseWasAuthFailure = true;
|
|
508
|
+
if (this.ws) {
|
|
509
|
+
this.ws.terminate();
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.logger.info("Authentication successful");
|
|
514
|
+
this.reconnectAttempts = 0;
|
|
515
|
+
this.authFailureAttempts = 0;
|
|
516
|
+
this.startHeartbeat();
|
|
517
|
+
this.onAuthenticated?.();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (actualReqId.startsWith(WsCmd.HEARTBEAT)) {
|
|
521
|
+
if (frame.errcode !== 0) {
|
|
522
|
+
this.logger.warn(`Heartbeat ack error: errcode=${frame.errcode}, errmsg=${frame.errmsg}`);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
this.missedPongCount = 0;
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (this.pendingAcks.has(actualReqId)) {
|
|
529
|
+
this.handleReplyAck(actualReqId, frame);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this.logger.warn("Received unknown frame (ignored):", JSON.stringify(frame));
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* 启动心跳定时器
|
|
536
|
+
*/
|
|
537
|
+
startHeartbeat() {
|
|
538
|
+
this.stopHeartbeat();
|
|
539
|
+
this.heartbeatTimer = setInterval(() => {
|
|
540
|
+
this.sendHeartbeat();
|
|
541
|
+
}, this.heartbeatInterval);
|
|
542
|
+
this.logger.debug(`Heartbeat timer started, interval: ${this.heartbeatInterval}ms`);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* 停止心跳定时器
|
|
546
|
+
*/
|
|
547
|
+
stopHeartbeat() {
|
|
548
|
+
if (this.heartbeatTimer) {
|
|
549
|
+
clearInterval(this.heartbeatTimer);
|
|
550
|
+
this.heartbeatTimer = null;
|
|
551
|
+
this.logger.debug("Heartbeat timer stopped");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* 发送心跳
|
|
556
|
+
* If consecutive missed pong count reaches the threshold, treat the
|
|
557
|
+
* connection as dead and trigger reconnection.
|
|
558
|
+
*
|
|
559
|
+
* 格式:{ cmd: "ping", headers: { req_id } }
|
|
560
|
+
*/
|
|
561
|
+
sendHeartbeat() {
|
|
562
|
+
if (this.missedPongCount >= this.maxMissedPong) {
|
|
563
|
+
this.logger.warn(`No heartbeat ack received for ${this.missedPongCount} consecutive pings, connection considered dead`);
|
|
564
|
+
this.stopHeartbeat();
|
|
565
|
+
if (this.ws) {
|
|
566
|
+
this.ws.terminate();
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
this.missedPongCount++;
|
|
571
|
+
try {
|
|
572
|
+
this.send({
|
|
573
|
+
cmd: WsCmd.HEARTBEAT,
|
|
574
|
+
headers: { req_id: generateReqId(WsCmd.HEARTBEAT) }
|
|
575
|
+
});
|
|
576
|
+
} catch (error) {
|
|
577
|
+
this.logger.error("Failed to send heartbeat:", error.message);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* 安排重连
|
|
582
|
+
*
|
|
583
|
+
* 区分两种重连场景,使用独立的计数器和最大重试次数:
|
|
584
|
+
* - 认证失败(lastCloseWasAuthFailure=true):使用 authFailureAttempts / maxAuthFailureAttempts
|
|
585
|
+
* - 连接断开(lastCloseWasAuthFailure=false):使用 reconnectAttempts / maxReconnectAttempts
|
|
586
|
+
*
|
|
587
|
+
* disconnected_event(被踢下线)不会触发重连,因为 isManualClose 已被设为 true。
|
|
588
|
+
*/
|
|
589
|
+
scheduleReconnect() {
|
|
590
|
+
if (this.lastCloseWasAuthFailure) {
|
|
591
|
+
if (this.maxAuthFailureAttempts !== -1 && this.authFailureAttempts >= this.maxAuthFailureAttempts) {
|
|
592
|
+
this.logger.error(`Max auth failure attempts reached (${this.maxAuthFailureAttempts}), giving up`);
|
|
593
|
+
this.onError?.(new WSAuthFailureError(this.maxAuthFailureAttempts));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.authFailureAttempts++;
|
|
597
|
+
const delay2 = Math.min(this.reconnectBaseDelay * Math.pow(2, this.authFailureAttempts - 1), this.reconnectMaxDelay);
|
|
598
|
+
this.logger.info(`Auth failed, reconnecting in ${delay2}ms (auth attempt ${this.authFailureAttempts}/${this.maxAuthFailureAttempts})...`);
|
|
599
|
+
this.onReconnecting?.(this.authFailureAttempts);
|
|
600
|
+
this.reconnectTimer = setTimeout(() => {
|
|
601
|
+
this.reconnectTimer = null;
|
|
602
|
+
if (this.isManualClose)
|
|
603
|
+
return;
|
|
604
|
+
this.connect();
|
|
605
|
+
}, delay2);
|
|
606
|
+
} else {
|
|
607
|
+
if (this.maxReconnectAttempts !== -1 && this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
608
|
+
this.logger.error(`Max reconnect attempts reached (${this.maxReconnectAttempts}), giving up`);
|
|
609
|
+
this.onError?.(new WSReconnectExhaustedError(this.maxReconnectAttempts));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
this.reconnectAttempts++;
|
|
613
|
+
const delay2 = Math.min(this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1), this.reconnectMaxDelay);
|
|
614
|
+
this.logger.info(`Connection lost, reconnecting in ${delay2}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
615
|
+
this.onReconnecting?.(this.reconnectAttempts);
|
|
616
|
+
this.reconnectTimer = setTimeout(() => {
|
|
617
|
+
this.reconnectTimer = null;
|
|
618
|
+
if (this.isManualClose)
|
|
619
|
+
return;
|
|
620
|
+
this.connect();
|
|
621
|
+
}, delay2);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* 发送数据帧
|
|
626
|
+
*
|
|
627
|
+
* 统一格式:{ cmd, headers: { req_id }, body }
|
|
628
|
+
*/
|
|
629
|
+
send(frame) {
|
|
630
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
631
|
+
const data = JSON.stringify(frame);
|
|
632
|
+
this.ws.send(data);
|
|
633
|
+
} else {
|
|
634
|
+
throw new Error("WebSocket not connected, unable to send data");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 通过 WebSocket 通道发送回复消息(串行队列版本)
|
|
639
|
+
*
|
|
640
|
+
* 同一个 req_id 的消息会被放入队列中串行发送:
|
|
641
|
+
* 发送一条后等待服务端回执,收到回执或超时后才发送下一条。
|
|
642
|
+
*
|
|
643
|
+
* 格式:{ cmd: "aibot_respond_msg", headers: { req_id }, body: { ... } }
|
|
644
|
+
*
|
|
645
|
+
* @param reqId - 透传回调中的 req_id
|
|
646
|
+
* @param body - 回复消息体(如 StreamReplyBody)
|
|
647
|
+
* @param cmd - 发送的命令类型,默认 WsCmd.RESPONSE
|
|
648
|
+
* @returns Promise,收到回执时 resolve(回执帧),超时或errcode非0时 reject(Error)
|
|
649
|
+
*/
|
|
650
|
+
sendReply(reqId, body, cmd = WsCmd.RESPONSE) {
|
|
651
|
+
return new Promise((resolve2, reject) => {
|
|
652
|
+
const frame = {
|
|
653
|
+
cmd,
|
|
654
|
+
headers: { req_id: reqId },
|
|
655
|
+
body
|
|
656
|
+
};
|
|
657
|
+
const item = { frame, resolve: resolve2, reject };
|
|
658
|
+
if (!this.replyQueues.has(reqId)) {
|
|
659
|
+
this.replyQueues.set(reqId, []);
|
|
660
|
+
}
|
|
661
|
+
const queue = this.replyQueues.get(reqId);
|
|
662
|
+
if (queue.length >= this.maxReplyQueueSize) {
|
|
663
|
+
this.logger.warn(`Reply queue for reqId ${reqId} exceeds max size (${this.maxReplyQueueSize}), rejecting new message`);
|
|
664
|
+
reject(new Error(`Reply queue for reqId ${reqId} exceeds max size (${this.maxReplyQueueSize})`));
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
queue.push(item);
|
|
668
|
+
if (queue.length === 1) {
|
|
669
|
+
this.processReplyQueue(reqId);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* 处理指定 req_id 的回复队列
|
|
675
|
+
* 取出队列头部的消息发送,并设置回执超时
|
|
676
|
+
*/
|
|
677
|
+
processReplyQueue(reqId) {
|
|
678
|
+
const queue = this.replyQueues.get(reqId);
|
|
679
|
+
if (!queue || queue.length === 0) {
|
|
680
|
+
this.replyQueues.delete(reqId);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const item = queue[0];
|
|
684
|
+
try {
|
|
685
|
+
this.send(item.frame);
|
|
686
|
+
this.logger.debug(`Reply message sent via WebSocket, reqId: ${reqId}, queue length: ${queue.length}`);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
this.logger.error(`Failed to send reply for reqId ${reqId}:`, error.message);
|
|
689
|
+
queue.shift();
|
|
690
|
+
item.reject(error);
|
|
691
|
+
queueMicrotask(() => this.processReplyQueue(reqId));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const seq = ++this.pendingAckSeq;
|
|
695
|
+
const timer = setTimeout(() => {
|
|
696
|
+
const currentPending = this.pendingAcks.get(reqId);
|
|
697
|
+
if (!currentPending || currentPending.seq !== seq) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
this.logger.warn(`Reply ack timeout (${this.replyAckTimeout}ms) for reqId: ${reqId}`);
|
|
701
|
+
this.pendingAcks.delete(reqId);
|
|
702
|
+
queue.shift();
|
|
703
|
+
item.reject(new Error(`Reply ack timeout (${this.replyAckTimeout}ms) for reqId: ${reqId}`));
|
|
704
|
+
this.processReplyQueue(reqId);
|
|
705
|
+
}, this.replyAckTimeout);
|
|
706
|
+
this.pendingAcks.set(reqId, {
|
|
707
|
+
resolve: item.resolve,
|
|
708
|
+
reject: item.reject,
|
|
709
|
+
timer,
|
|
710
|
+
seq
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* 处理回复消息的回执
|
|
715
|
+
* 收到回执后释放队列锁,继续处理下一条
|
|
716
|
+
*/
|
|
717
|
+
handleReplyAck(reqId, frame) {
|
|
718
|
+
const pending = this.pendingAcks.get(reqId);
|
|
719
|
+
if (!pending)
|
|
720
|
+
return;
|
|
721
|
+
clearTimeout(pending.timer);
|
|
722
|
+
this.pendingAcks.delete(reqId);
|
|
723
|
+
const queue = this.replyQueues.get(reqId);
|
|
724
|
+
if (frame.errcode !== 0) {
|
|
725
|
+
this.logger.warn(`Reply ack error: reqId=${reqId}, errcode=${frame.errcode}, errmsg=${frame.errmsg}`);
|
|
726
|
+
if (queue) {
|
|
727
|
+
queue.shift();
|
|
728
|
+
}
|
|
729
|
+
pending.reject(frame);
|
|
730
|
+
} else {
|
|
731
|
+
this.logger.debug(`Reply ack received for reqId: ${reqId}`);
|
|
732
|
+
if (queue) {
|
|
733
|
+
queue.shift();
|
|
734
|
+
}
|
|
735
|
+
pending.resolve(frame);
|
|
736
|
+
}
|
|
737
|
+
this.processReplyQueue(reqId);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* 主动断开连接
|
|
741
|
+
*/
|
|
742
|
+
/**
|
|
743
|
+
* 清理所有待处理的消息和回执
|
|
744
|
+
* @param reason - 清理原因,用于 reject 的错误信息
|
|
745
|
+
*/
|
|
746
|
+
clearPendingMessages(reason) {
|
|
747
|
+
const pendingRejects = /* @__PURE__ */ new Set();
|
|
748
|
+
for (const [reqId, pending] of this.pendingAcks) {
|
|
749
|
+
clearTimeout(pending.timer);
|
|
750
|
+
pendingRejects.add(pending.reject);
|
|
751
|
+
pending.reject(new Error(`${reason}, reply for reqId: ${reqId} cancelled`));
|
|
752
|
+
}
|
|
753
|
+
this.pendingAcks.clear();
|
|
754
|
+
for (const [reqId, queue] of this.replyQueues) {
|
|
755
|
+
for (const item of queue) {
|
|
756
|
+
if (pendingRejects.has(item.reject)) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
item.reject(new Error(`${reason}, reply for reqId: ${reqId} cancelled`));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
this.replyQueues.clear();
|
|
763
|
+
}
|
|
764
|
+
disconnect() {
|
|
765
|
+
this.isManualClose = true;
|
|
766
|
+
this.stopHeartbeat();
|
|
767
|
+
this.clearPendingMessages("Connection manually closed");
|
|
768
|
+
if (this.reconnectTimer) {
|
|
769
|
+
clearTimeout(this.reconnectTimer);
|
|
770
|
+
this.reconnectTimer = null;
|
|
771
|
+
}
|
|
772
|
+
if (this.ws) {
|
|
773
|
+
this.ws.terminate();
|
|
774
|
+
this.ws = null;
|
|
775
|
+
}
|
|
776
|
+
this.logger.info("WebSocket connection manually closed");
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* 检查指定 reqId 是否有待回执的消息(即上一条消息还未收到 ack)
|
|
780
|
+
*
|
|
781
|
+
* 用于流式场景:调用方可据此决定是否跳过当前帧,避免排队积压。
|
|
782
|
+
*
|
|
783
|
+
* @param reqId - 要检查的 req_id
|
|
784
|
+
* @returns true 表示有消息正在等待 ack
|
|
785
|
+
*/
|
|
786
|
+
hasPendingAck(reqId) {
|
|
787
|
+
return this.pendingAcks.has(reqId);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* 获取当前连接状态
|
|
791
|
+
*/
|
|
792
|
+
get isConnected() {
|
|
793
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
var MessageHandler = class {
|
|
797
|
+
static {
|
|
798
|
+
__name(this, "MessageHandler");
|
|
799
|
+
}
|
|
800
|
+
constructor(logger) {
|
|
801
|
+
this.logger = logger;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* 处理收到的 WebSocket 帧,解析并触发对应的消息/事件
|
|
805
|
+
*
|
|
806
|
+
* WebSocket 推送帧结构:
|
|
807
|
+
* - 消息推送:{ cmd: "aibot_msg_callback", headers: { req_id: "xxx" }, body: { msgid, msgtype, ... } }
|
|
808
|
+
* - 事件推送:{ cmd: "aibot_event_callback", headers: { req_id: "xxx" }, body: { msgid, msgtype: "event", event: { ... } } }
|
|
809
|
+
*
|
|
810
|
+
* @param frame - WebSocket 接收帧
|
|
811
|
+
* @param emitter - WSClient 实例,用于触发事件
|
|
812
|
+
*/
|
|
813
|
+
handleFrame(frame, emitter) {
|
|
814
|
+
try {
|
|
815
|
+
const body = frame.body;
|
|
816
|
+
if (!body || !body.msgtype) {
|
|
817
|
+
this.logger.warn("Received invalid message format:", JSON.stringify(frame).substring(0, 200));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (frame.cmd === WsCmd.EVENT_CALLBACK) {
|
|
821
|
+
this.handleEventCallback(frame, emitter);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
this.handleMessageCallback(frame, emitter);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
this.logger.error("Failed to handle message:", error.message);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* 处理消息推送回调 (aibot_msg_callback)
|
|
831
|
+
*/
|
|
832
|
+
handleMessageCallback(frame, emitter) {
|
|
833
|
+
const body = frame.body;
|
|
834
|
+
emitter.emit("message", frame);
|
|
835
|
+
switch (body.msgtype) {
|
|
836
|
+
case exports.MessageType.Text:
|
|
837
|
+
emitter.emit("message.text", frame);
|
|
838
|
+
break;
|
|
839
|
+
case exports.MessageType.Image:
|
|
840
|
+
emitter.emit("message.image", frame);
|
|
841
|
+
break;
|
|
842
|
+
case exports.MessageType.Mixed:
|
|
843
|
+
emitter.emit("message.mixed", frame);
|
|
844
|
+
break;
|
|
845
|
+
case exports.MessageType.Voice:
|
|
846
|
+
emitter.emit("message.voice", frame);
|
|
847
|
+
break;
|
|
848
|
+
case exports.MessageType.File:
|
|
849
|
+
emitter.emit("message.file", frame);
|
|
850
|
+
break;
|
|
851
|
+
case exports.MessageType.Video:
|
|
852
|
+
emitter.emit("message.video", frame);
|
|
853
|
+
break;
|
|
854
|
+
default:
|
|
855
|
+
this.logger.debug(`Received unhandled message type: ${body.msgtype}`);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* 处理事件推送回调 (aibot_event_callback)
|
|
861
|
+
*/
|
|
862
|
+
handleEventCallback(frame, emitter) {
|
|
863
|
+
const body = frame.body;
|
|
864
|
+
emitter.emit("event", frame);
|
|
865
|
+
const eventType = body.event?.eventtype;
|
|
866
|
+
if (eventType) {
|
|
867
|
+
const eventKey = `event.${eventType}`;
|
|
868
|
+
emitter.emit(eventKey, frame);
|
|
869
|
+
} else {
|
|
870
|
+
this.logger.debug("Received event callback without eventtype:", JSON.stringify(body).substring(0, 200));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
function decryptFile2(encryptedBuffer, aesKey) {
|
|
875
|
+
if (!encryptedBuffer || encryptedBuffer.length === 0) {
|
|
876
|
+
throw new Error("decryptFile: encryptedBuffer is empty or not provided");
|
|
877
|
+
}
|
|
878
|
+
if (!aesKey || typeof aesKey !== "string") {
|
|
879
|
+
throw new Error("decryptFile: aesKey must be a non-empty string");
|
|
880
|
+
}
|
|
881
|
+
const key = Buffer.from(aesKey, "base64");
|
|
882
|
+
const iv = key.subarray(0, 16);
|
|
883
|
+
try {
|
|
884
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
|
885
|
+
decipher.setAutoPadding(false);
|
|
886
|
+
const decrypted = Buffer.concat([
|
|
887
|
+
decipher.update(encryptedBuffer),
|
|
888
|
+
decipher.final()
|
|
889
|
+
]);
|
|
890
|
+
const padLen = decrypted[decrypted.length - 1];
|
|
891
|
+
if (padLen < 1 || padLen > 32 || padLen > decrypted.length) {
|
|
892
|
+
throw new Error(`Invalid PKCS#7 padding value: ${padLen}`);
|
|
893
|
+
}
|
|
894
|
+
for (let i = decrypted.length - padLen; i < decrypted.length; i++) {
|
|
895
|
+
if (decrypted[i] !== padLen) {
|
|
896
|
+
throw new Error("Invalid PKCS#7 padding: padding bytes mismatch");
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return decrypted.subarray(0, decrypted.length - padLen);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
throw new Error(`decryptFile: Decryption failed - ${error.message}. This may indicate corrupted data or an incorrect aesKey.`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
__name(decryptFile2, "decryptFile");
|
|
905
|
+
var DefaultLogger = class {
|
|
906
|
+
static {
|
|
907
|
+
__name(this, "DefaultLogger");
|
|
908
|
+
}
|
|
909
|
+
constructor(prefix = "AiBotSDK") {
|
|
910
|
+
this.prefix = prefix;
|
|
911
|
+
}
|
|
912
|
+
formatTime() {
|
|
913
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
914
|
+
}
|
|
915
|
+
debug(message, ...args) {
|
|
916
|
+
console.debug(`[${this.formatTime()}] [${this.prefix}] [DEBUG] ${message}`, ...args);
|
|
917
|
+
}
|
|
918
|
+
info(message, ...args) {
|
|
919
|
+
console.info(`[${this.formatTime()}] [${this.prefix}] [INFO] ${message}`, ...args);
|
|
920
|
+
}
|
|
921
|
+
warn(message, ...args) {
|
|
922
|
+
console.warn(`[${this.formatTime()}] [${this.prefix}] [WARN] ${message}`, ...args);
|
|
923
|
+
}
|
|
924
|
+
error(message, ...args) {
|
|
925
|
+
console.error(`[${this.formatTime()}] [${this.prefix}] [ERROR] ${message}`, ...args);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
var WSClient2 = class extends eventemitter3.EventEmitter {
|
|
929
|
+
static {
|
|
930
|
+
__name(this, "WSClient");
|
|
931
|
+
}
|
|
932
|
+
constructor(options) {
|
|
933
|
+
super();
|
|
934
|
+
this.started = false;
|
|
935
|
+
this.options = {
|
|
936
|
+
reconnectInterval: 1e3,
|
|
937
|
+
maxReconnectAttempts: 10,
|
|
938
|
+
maxAuthFailureAttempts: 5,
|
|
939
|
+
heartbeatInterval: 3e4,
|
|
940
|
+
requestTimeout: 1e4,
|
|
941
|
+
wsUrl: "",
|
|
942
|
+
wsOptions: {},
|
|
943
|
+
maxReplyQueueSize: 500,
|
|
944
|
+
logger: new DefaultLogger(),
|
|
945
|
+
...options
|
|
946
|
+
};
|
|
947
|
+
this.logger = this.options.logger;
|
|
948
|
+
this.apiClient = new WeComApiClient(this.logger, this.options.requestTimeout);
|
|
949
|
+
this.wsManager = new WsConnectionManager(this.logger, this.options.heartbeatInterval, this.options.reconnectInterval, this.options.maxReconnectAttempts, this.options.wsUrl || void 0, this.options.wsOptions, this.options.maxReplyQueueSize, this.options.maxAuthFailureAttempts);
|
|
950
|
+
this.wsManager.setCredentials(this.options.botId, this.options.secret, {
|
|
951
|
+
...this.options.scene !== void 0 && { scene: this.options.scene },
|
|
952
|
+
...this.options.plug_version !== void 0 && { plug_version: this.options.plug_version }
|
|
953
|
+
});
|
|
954
|
+
this.messageHandler = new MessageHandler(this.logger);
|
|
955
|
+
this.setupWsEvents();
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* 设置 WebSocket 事件处理
|
|
959
|
+
*/
|
|
960
|
+
setupWsEvents() {
|
|
961
|
+
this.wsManager.onConnected = () => {
|
|
962
|
+
this.emit("connected");
|
|
963
|
+
};
|
|
964
|
+
this.wsManager.onAuthenticated = () => {
|
|
965
|
+
this.logger.info("Authenticated");
|
|
966
|
+
this.emit("authenticated");
|
|
967
|
+
};
|
|
968
|
+
this.wsManager.onDisconnected = (reason) => {
|
|
969
|
+
this.emit("disconnected", reason);
|
|
970
|
+
};
|
|
971
|
+
this.wsManager.onServerDisconnect = (reason) => {
|
|
972
|
+
this.logger.warn(`Server disconnected this connection: ${reason}`);
|
|
973
|
+
this.started = false;
|
|
974
|
+
this.emit("disconnected", reason);
|
|
975
|
+
};
|
|
976
|
+
this.wsManager.onReconnecting = (attempt) => {
|
|
977
|
+
this.emit("reconnecting", attempt);
|
|
978
|
+
};
|
|
979
|
+
this.wsManager.onError = (error) => {
|
|
980
|
+
this.emit("error", error);
|
|
981
|
+
};
|
|
982
|
+
this.wsManager.onMessage = (frame) => {
|
|
983
|
+
this.messageHandler.handleFrame(frame, this);
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* 建立 WebSocket 长连接
|
|
988
|
+
* SDK 使用内置默认地址建立连接,连接成功后自动发送认证帧(botId + secret)。
|
|
989
|
+
* 支持链式调用:wsClient.connect().on('message', handler)
|
|
990
|
+
*
|
|
991
|
+
* @returns 返回 this,支持链式调用
|
|
992
|
+
*/
|
|
993
|
+
connect() {
|
|
994
|
+
if (this.started) {
|
|
995
|
+
this.logger.warn("Client already connected");
|
|
996
|
+
return this;
|
|
997
|
+
}
|
|
998
|
+
this.logger.info("Establishing WebSocket connection...");
|
|
999
|
+
this.started = true;
|
|
1000
|
+
this.wsManager.connect();
|
|
1001
|
+
return this;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* 断开 WebSocket 连接
|
|
1005
|
+
*/
|
|
1006
|
+
disconnect() {
|
|
1007
|
+
if (!this.started) {
|
|
1008
|
+
this.logger.warn("Client not connected");
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
this.logger.info("Disconnecting...");
|
|
1012
|
+
this.started = false;
|
|
1013
|
+
this.wsManager.disconnect();
|
|
1014
|
+
this.logger.info("Disconnected");
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* 通过 WebSocket 通道发送回复消息(通用方法)
|
|
1018
|
+
*
|
|
1019
|
+
* @param frame - 收到的原始 WebSocket 帧,透传 headers.req_id
|
|
1020
|
+
* @param body - 回复消息体
|
|
1021
|
+
* @param cmd
|
|
1022
|
+
*/
|
|
1023
|
+
reply(frame, body, cmd) {
|
|
1024
|
+
const reqId = frame.headers?.req_id || "";
|
|
1025
|
+
return this.wsManager.sendReply(reqId, body, cmd);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* 发送流式文本回复(便捷方法)
|
|
1029
|
+
*
|
|
1030
|
+
* @param frame - 收到的原始 WebSocket 帧,透传 headers.req_id
|
|
1031
|
+
* @param streamId - 流式消息 ID
|
|
1032
|
+
* @param content - 回复内容(支持 Markdown)
|
|
1033
|
+
* @param finish - 是否结束流式消息,默认 false
|
|
1034
|
+
* @param msgItem - 图文混排项(仅在 finish=true 时有效),用于在结束时附带图片内容
|
|
1035
|
+
* @param feedback - 反馈信息(仅在首次回复时设置)
|
|
1036
|
+
*/
|
|
1037
|
+
replyStream(frame, streamId, content, finish = false, msgItem, feedback) {
|
|
1038
|
+
const stream = {
|
|
1039
|
+
id: streamId,
|
|
1040
|
+
finish,
|
|
1041
|
+
content
|
|
1042
|
+
};
|
|
1043
|
+
if (finish && msgItem && msgItem.length > 0) {
|
|
1044
|
+
stream.msg_item = msgItem;
|
|
1045
|
+
}
|
|
1046
|
+
if (feedback) {
|
|
1047
|
+
stream.feedback = feedback;
|
|
1048
|
+
}
|
|
1049
|
+
return this.reply(frame, {
|
|
1050
|
+
msgtype: "stream",
|
|
1051
|
+
stream
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* 发送欢迎语回复
|
|
1056
|
+
*
|
|
1057
|
+
* 注意:此方法需要使用对应事件(如 enter_chat)的 req_id 才能调用,
|
|
1058
|
+
* 即 frame 参数应来自触发欢迎语的事件帧。
|
|
1059
|
+
* 收到事件回调后需在 5 秒内发送回复,超时将无法发送欢迎语。
|
|
1060
|
+
*
|
|
1061
|
+
* @param frame - 对应事件的 WebSocket 帧(需包含该事件的 req_id)
|
|
1062
|
+
* @param body - 欢迎语消息体(支持文本或模板卡片格式)
|
|
1063
|
+
*/
|
|
1064
|
+
replyWelcome(frame, body) {
|
|
1065
|
+
return this.reply(frame, body, WsCmd.RESPONSE_WELCOME);
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* 回复模板卡片消息
|
|
1069
|
+
*
|
|
1070
|
+
* 收到消息回调或进入会话事件后,可使用此方法回复模板卡片消息。
|
|
1071
|
+
*
|
|
1072
|
+
* @param frame - 收到的原始 WebSocket 帧,透传 headers.req_id
|
|
1073
|
+
* @param templateCard - 模板卡片内容
|
|
1074
|
+
* @param feedback - 反馈信息
|
|
1075
|
+
*/
|
|
1076
|
+
replyTemplateCard(frame, templateCard, feedback) {
|
|
1077
|
+
const card = feedback ? { ...templateCard, feedback } : templateCard;
|
|
1078
|
+
const body = {
|
|
1079
|
+
msgtype: "template_card",
|
|
1080
|
+
template_card: card
|
|
1081
|
+
};
|
|
1082
|
+
return this.reply(frame, body);
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* 发送流式消息 + 模板卡片组合回复
|
|
1086
|
+
*
|
|
1087
|
+
* 首次回复时必须返回 stream 的 id。
|
|
1088
|
+
* template_card 可首次回复,也可在后续回复中发送,但同一个消息只能回复一次。
|
|
1089
|
+
*
|
|
1090
|
+
* @param frame - 收到的原始 WebSocket 帧,透传 headers.req_id
|
|
1091
|
+
* @param streamId - 流式消息 ID
|
|
1092
|
+
* @param content - 回复内容(支持 Markdown)
|
|
1093
|
+
* @param finish - 是否结束流式消息,默认 false
|
|
1094
|
+
* @param options - 可选项
|
|
1095
|
+
* @param options.msgItem - 图文混排项(仅在 finish=true 时有效)
|
|
1096
|
+
* @param options.streamFeedback - 流式消息反馈信息(首次回复时设置)
|
|
1097
|
+
* @param options.templateCard - 模板卡片内容(同一消息只能回复一次)
|
|
1098
|
+
* @param options.cardFeedback - 模板卡片反馈信息
|
|
1099
|
+
*/
|
|
1100
|
+
replyStreamWithCard(frame, streamId, content, finish = false, options) {
|
|
1101
|
+
const stream = {
|
|
1102
|
+
id: streamId,
|
|
1103
|
+
finish,
|
|
1104
|
+
content
|
|
1105
|
+
};
|
|
1106
|
+
if (finish && options?.msgItem && options.msgItem.length > 0) {
|
|
1107
|
+
stream.msg_item = options.msgItem;
|
|
1108
|
+
}
|
|
1109
|
+
if (options?.streamFeedback) {
|
|
1110
|
+
stream.feedback = options.streamFeedback;
|
|
1111
|
+
}
|
|
1112
|
+
const body = {
|
|
1113
|
+
msgtype: "stream_with_template_card",
|
|
1114
|
+
stream
|
|
1115
|
+
};
|
|
1116
|
+
if (options?.templateCard) {
|
|
1117
|
+
body.template_card = options.cardFeedback ? { ...options.templateCard, feedback: options.cardFeedback } : options.templateCard;
|
|
1118
|
+
}
|
|
1119
|
+
return this.reply(frame, body);
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* 更新模板卡片
|
|
1123
|
+
*
|
|
1124
|
+
* 注意:此方法需要使用对应事件(template_card_event)的 req_id 才能调用,
|
|
1125
|
+
* 即 frame 参数应来自触发更新的事件帧。
|
|
1126
|
+
* 收到事件回调后需在 5 秒内发送回复,超时将无法更新卡片。
|
|
1127
|
+
*
|
|
1128
|
+
* @param frame - 对应事件的 WebSocket 帧(需包含该事件的 req_id)
|
|
1129
|
+
* @param templateCard - 模板卡片内容(task_id 需跟回调收到的 task_id 一致)
|
|
1130
|
+
* @param userids - 要替换模版卡片消息的 userid 列表,若不填则替换所有用户
|
|
1131
|
+
*/
|
|
1132
|
+
updateTemplateCard(frame, templateCard, userids) {
|
|
1133
|
+
const body = {
|
|
1134
|
+
response_type: "update_template_card",
|
|
1135
|
+
template_card: templateCard
|
|
1136
|
+
};
|
|
1137
|
+
if (userids && userids.length > 0) {
|
|
1138
|
+
body.userids = userids;
|
|
1139
|
+
}
|
|
1140
|
+
return this.reply(frame, body, WsCmd.RESPONSE_UPDATE);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* 主动发送消息
|
|
1144
|
+
*
|
|
1145
|
+
* 向指定会话(单聊或群聊)主动推送消息,无需依赖收到的回调帧。
|
|
1146
|
+
*
|
|
1147
|
+
* @param chatid - 会话 ID,单聊填用户的 userid,群聊填对应群聊的 chatid
|
|
1148
|
+
* @param body - 消息体(支持 markdown 或 template_card 格式)
|
|
1149
|
+
* @returns Promise,收到回执时 resolve(回执帧)
|
|
1150
|
+
*
|
|
1151
|
+
* @example
|
|
1152
|
+
* ```ts
|
|
1153
|
+
* // 发送 markdown 消息
|
|
1154
|
+
* await wsClient.sendMessage('CHATID', {
|
|
1155
|
+
* msgtype: 'markdown',
|
|
1156
|
+
* markdown: { content: '这是一条**主动推送**的消息' },
|
|
1157
|
+
* });
|
|
1158
|
+
*
|
|
1159
|
+
* // 发送模板卡片消息
|
|
1160
|
+
* await wsClient.sendMessage('CHATID', {
|
|
1161
|
+
* msgtype: 'template_card',
|
|
1162
|
+
* template_card: { card_type: 'text_notice', ... },
|
|
1163
|
+
* });
|
|
1164
|
+
* ```
|
|
1165
|
+
*/
|
|
1166
|
+
sendMessage(chatid, body) {
|
|
1167
|
+
const reqId = generateReqId(WsCmd.SEND_MSG);
|
|
1168
|
+
const fullBody = {
|
|
1169
|
+
chatid,
|
|
1170
|
+
...body
|
|
1171
|
+
};
|
|
1172
|
+
return this.wsManager.sendReply(reqId, fullBody, WsCmd.SEND_MSG);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* 上传临时素材(三步分片上传)
|
|
1176
|
+
*
|
|
1177
|
+
* 通过 WebSocket 长连接执行分片上传:init → chunk × N → finish
|
|
1178
|
+
* 单个分片不超过 512KB(Base64 编码前),最多 100 个分片。
|
|
1179
|
+
*
|
|
1180
|
+
* @param fileBuffer - 文件 Buffer
|
|
1181
|
+
* @param options - 上传选项(类型、文件名)
|
|
1182
|
+
* @returns 上传结果,包含 media_id
|
|
1183
|
+
*/
|
|
1184
|
+
async uploadMedia(fileBuffer, options) {
|
|
1185
|
+
const { type, filename } = options;
|
|
1186
|
+
const totalSize = fileBuffer.length;
|
|
1187
|
+
const CHUNK_SIZE = 512 * 1024;
|
|
1188
|
+
const totalChunks = Math.ceil(totalSize / CHUNK_SIZE);
|
|
1189
|
+
if (totalChunks > 100) {
|
|
1190
|
+
throw new Error(`File too large: ${totalChunks} chunks exceeds maximum of 100 chunks (max ~50MB)`);
|
|
1191
|
+
}
|
|
1192
|
+
const md5 = crypto.createHash("md5").update(fileBuffer).digest("hex");
|
|
1193
|
+
this.logger.info(`Uploading media: type=${type}, filename=${filename}, size=${totalSize}, chunks=${totalChunks}`);
|
|
1194
|
+
const initReqId = generateReqId(WsCmd.UPLOAD_MEDIA_INIT);
|
|
1195
|
+
const initResult = await this.wsManager.sendReply(initReqId, { type, filename, total_size: totalSize, total_chunks: totalChunks, md5 }, WsCmd.UPLOAD_MEDIA_INIT);
|
|
1196
|
+
const uploadId = initResult.body?.upload_id;
|
|
1197
|
+
if (!uploadId) {
|
|
1198
|
+
throw new Error(`Upload init failed: no upload_id returned. Response: ${JSON.stringify(initResult)}`);
|
|
1199
|
+
}
|
|
1200
|
+
this.logger.info(`Upload init success: upload_id=${uploadId}`);
|
|
1201
|
+
const MAX_CHUNK_RETRIES = 2;
|
|
1202
|
+
const MAX_CONCURRENCY = totalChunks <= 4 ? totalChunks : totalChunks <= 10 ? 3 : 2;
|
|
1203
|
+
const uploadChunk = /* @__PURE__ */ __name(async (chunkIndex) => {
|
|
1204
|
+
const start = chunkIndex * CHUNK_SIZE;
|
|
1205
|
+
const end = Math.min(start + CHUNK_SIZE, totalSize);
|
|
1206
|
+
const chunk = fileBuffer.subarray(start, end);
|
|
1207
|
+
const base64Data = chunk.toString("base64");
|
|
1208
|
+
let lastError;
|
|
1209
|
+
for (let attempt = 0; attempt <= MAX_CHUNK_RETRIES; attempt++) {
|
|
1210
|
+
try {
|
|
1211
|
+
const chunkReqId = generateReqId(WsCmd.UPLOAD_MEDIA_CHUNK);
|
|
1212
|
+
await this.wsManager.sendReply(chunkReqId, { upload_id: uploadId, chunk_index: chunkIndex, base64_data: base64Data }, WsCmd.UPLOAD_MEDIA_CHUNK);
|
|
1213
|
+
this.logger.debug(`Uploaded chunk ${chunkIndex + 1}/${totalChunks} (${chunk.length} bytes)`);
|
|
1214
|
+
return;
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
lastError = err;
|
|
1217
|
+
if (attempt < MAX_CHUNK_RETRIES) {
|
|
1218
|
+
const delay2 = 500 * (attempt + 1);
|
|
1219
|
+
this.logger.warn(`Chunk ${chunkIndex} upload failed (attempt ${attempt + 1}/${MAX_CHUNK_RETRIES + 1}), retrying in ${delay2}ms... error: ${err instanceof Error ? err.message : JSON.stringify(err)}`);
|
|
1220
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const errMsg = lastError instanceof Error ? lastError.message : JSON.stringify(lastError);
|
|
1225
|
+
throw new Error(`Chunk ${chunkIndex} upload failed after ${MAX_CHUNK_RETRIES + 1} attempts: ${errMsg}`);
|
|
1226
|
+
}, "uploadChunk");
|
|
1227
|
+
this.logger.debug(`Upload concurrency: ${MAX_CONCURRENCY} workers for ${totalChunks} chunks`);
|
|
1228
|
+
if (totalChunks <= 1) {
|
|
1229
|
+
await uploadChunk(0);
|
|
1230
|
+
} else {
|
|
1231
|
+
let nextIndex = 0;
|
|
1232
|
+
const errors = [];
|
|
1233
|
+
const runWorker = /* @__PURE__ */ __name(async () => {
|
|
1234
|
+
while (nextIndex < totalChunks) {
|
|
1235
|
+
const idx = nextIndex++;
|
|
1236
|
+
try {
|
|
1237
|
+
await uploadChunk(idx);
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}, "runWorker");
|
|
1243
|
+
const workerCount = Math.min(MAX_CONCURRENCY, totalChunks);
|
|
1244
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
1245
|
+
if (errors.length > 0) {
|
|
1246
|
+
throw new Error(`Upload failed: ${errors.length} chunk(s) failed. First error: ${errors[0].message}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
this.logger.info(`All ${totalChunks} chunks uploaded, finishing...`);
|
|
1250
|
+
const finishReqId = generateReqId(WsCmd.UPLOAD_MEDIA_FINISH);
|
|
1251
|
+
const finishResult = await this.wsManager.sendReply(finishReqId, { upload_id: uploadId }, WsCmd.UPLOAD_MEDIA_FINISH);
|
|
1252
|
+
const mediaId = finishResult.body?.media_id;
|
|
1253
|
+
if (!mediaId) {
|
|
1254
|
+
throw new Error(`Upload finish failed: no media_id returned. Response: ${JSON.stringify(finishResult)}`);
|
|
1255
|
+
}
|
|
1256
|
+
this.logger.info(`Upload complete: media_id=${mediaId}, type=${finishResult.body?.type}`);
|
|
1257
|
+
return {
|
|
1258
|
+
type: finishResult.body?.type ?? type,
|
|
1259
|
+
media_id: mediaId,
|
|
1260
|
+
created_at: finishResult.body?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* 被动回复媒体消息(便捷方法)
|
|
1265
|
+
*
|
|
1266
|
+
* 通过 aibot_respond_msg 被动回复通道发送媒体消息(file/image/voice/video)
|
|
1267
|
+
*
|
|
1268
|
+
* @param frame - 收到的原始 WebSocket 帧,透传 headers.req_id
|
|
1269
|
+
* @param mediaType - 媒体类型
|
|
1270
|
+
* @param mediaId - 临时素材 media_id
|
|
1271
|
+
* @param videoOptions - 视频消息可选参数(仅 mediaType='video' 时生效)
|
|
1272
|
+
*/
|
|
1273
|
+
replyMedia(frame, mediaType, mediaId, videoOptions) {
|
|
1274
|
+
const mediaContent = { media_id: mediaId };
|
|
1275
|
+
if (mediaType === "video" && videoOptions) {
|
|
1276
|
+
if (videoOptions.title)
|
|
1277
|
+
mediaContent.title = videoOptions.title;
|
|
1278
|
+
if (videoOptions.description)
|
|
1279
|
+
mediaContent.description = videoOptions.description;
|
|
1280
|
+
}
|
|
1281
|
+
const body = {
|
|
1282
|
+
msgtype: mediaType,
|
|
1283
|
+
[mediaType]: mediaContent
|
|
1284
|
+
};
|
|
1285
|
+
return this.reply(frame, body);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* 主动发送媒体消息(便捷方法)
|
|
1289
|
+
*
|
|
1290
|
+
* 通过 aibot_send_msg 主动推送通道发送媒体消息
|
|
1291
|
+
*
|
|
1292
|
+
* @param chatid - 会话 ID
|
|
1293
|
+
* @param mediaType - 媒体类型
|
|
1294
|
+
* @param mediaId - 临时素材 media_id
|
|
1295
|
+
* @param videoOptions - 视频消息可选参数(仅 mediaType='video' 时生效)
|
|
1296
|
+
*/
|
|
1297
|
+
sendMediaMessage(chatid, mediaType, mediaId, videoOptions) {
|
|
1298
|
+
const mediaContent = { media_id: mediaId };
|
|
1299
|
+
if (mediaType === "video" && videoOptions) {
|
|
1300
|
+
if (videoOptions.title)
|
|
1301
|
+
mediaContent.title = videoOptions.title;
|
|
1302
|
+
if (videoOptions.description)
|
|
1303
|
+
mediaContent.description = videoOptions.description;
|
|
1304
|
+
}
|
|
1305
|
+
const body = {
|
|
1306
|
+
msgtype: mediaType,
|
|
1307
|
+
[mediaType]: mediaContent
|
|
1308
|
+
};
|
|
1309
|
+
return this.sendMessage(chatid, body);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* 下载文件并使用 AES 密钥解密
|
|
1313
|
+
*
|
|
1314
|
+
* @param url - 文件下载地址
|
|
1315
|
+
* @param aesKey - AES 解密密钥(Base64 编码),取自消息中 image.aeskey 或 file.aeskey 字段
|
|
1316
|
+
* @returns 解密后的文件 Buffer 及文件名
|
|
1317
|
+
*
|
|
1318
|
+
* @example
|
|
1319
|
+
* ```ts
|
|
1320
|
+
* // aesKey 来自消息体中的 image.aeskey 或 file.aeskey
|
|
1321
|
+
* const { buffer, filename } = await wsClient.downloadFile(imageUrl, body.image?.aeskey);
|
|
1322
|
+
* ```
|
|
1323
|
+
*/
|
|
1324
|
+
async downloadFile(url, aesKey) {
|
|
1325
|
+
this.logger.debug(`[plugin] downloadFile: url=${url}, hasAesKey=${!!aesKey}`);
|
|
1326
|
+
this.logger.info("Downloading and decrypting file...");
|
|
1327
|
+
try {
|
|
1328
|
+
const { buffer: encryptedBuffer, filename } = await this.apiClient.downloadFileRaw(url);
|
|
1329
|
+
if (!aesKey) {
|
|
1330
|
+
this.logger.warn("No aesKey provided, returning raw file data");
|
|
1331
|
+
return { buffer: encryptedBuffer, filename };
|
|
1332
|
+
}
|
|
1333
|
+
const decryptedBuffer = decryptFile2(encryptedBuffer, aesKey);
|
|
1334
|
+
this.logger.info("File downloaded and decrypted successfully");
|
|
1335
|
+
return { buffer: decryptedBuffer, filename };
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
this.logger.error("File download/decrypt failed:", error.message);
|
|
1338
|
+
throw error;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* 检查指定消息帧是否有未完成的 ack(即上一条消息还未收到回执)
|
|
1343
|
+
*
|
|
1344
|
+
* 用于流式场景:调用方可据此决定是否跳过当前中间帧,避免排队积压。
|
|
1345
|
+
*
|
|
1346
|
+
* @param frame - 收到的原始 WebSocket 帧
|
|
1347
|
+
* @returns true 表示有消息正在等待 ack
|
|
1348
|
+
*/
|
|
1349
|
+
hasPendingReplyAck(frame) {
|
|
1350
|
+
const reqId = frame.headers?.req_id || "";
|
|
1351
|
+
return this.wsManager.hasPendingAck(reqId);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* 非阻塞流式文本回复
|
|
1355
|
+
*
|
|
1356
|
+
* 如果上一条同 reqId 的消息尚未收到 ack,则跳过本次发送(返回 'skipped'),
|
|
1357
|
+
* 避免流式中间帧排队积压导致延迟。
|
|
1358
|
+
*
|
|
1359
|
+
* 注意:finish=true 的最终帧不受此限制,始终保证发送(走正常队列)。
|
|
1360
|
+
*
|
|
1361
|
+
* @param frame - 收到的原始 WebSocket 帧
|
|
1362
|
+
* @param streamId - 流式消息 ID
|
|
1363
|
+
* @param content - 回复内容
|
|
1364
|
+
* @param finish - 是否结束流式消息
|
|
1365
|
+
* @param msgItem - 图文混排项(仅在 finish=true 时有效),用于在结束时附带图片内容
|
|
1366
|
+
* @param feedback - 反馈信息(仅在首次回复时设置)
|
|
1367
|
+
* @returns Promise<WsFrame> 正常发送时返回回执帧,跳过时返回 'skipped'
|
|
1368
|
+
*/
|
|
1369
|
+
replyStreamNonBlocking(frame, streamId, content, finish = false, msgItem, feedback) {
|
|
1370
|
+
if (!finish && this.hasPendingReplyAck(frame)) {
|
|
1371
|
+
return Promise.resolve("skipped");
|
|
1372
|
+
}
|
|
1373
|
+
return this.replyStream(frame, streamId, content, finish, msgItem, feedback);
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* 获取当前连接状态
|
|
1377
|
+
*/
|
|
1378
|
+
get isConnected() {
|
|
1379
|
+
return this.wsManager.isConnected;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* 获取 API 客户端实例(供高级用途使用,如文件下载)
|
|
1383
|
+
*/
|
|
1384
|
+
get api() {
|
|
1385
|
+
return this.apiClient;
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
var CRYPTO_CONSTANTS = {
|
|
1389
|
+
/** PKCS#7 块大小 */
|
|
1390
|
+
PKCS7_BLOCK_SIZE: 32,
|
|
1391
|
+
/** AES Key 长度 */
|
|
1392
|
+
AES_KEY_LENGTH: 32
|
|
1393
|
+
};
|
|
1394
|
+
function decodeEncodingAESKey(encodingAESKey) {
|
|
1395
|
+
const trimmed = encodingAESKey.trim();
|
|
1396
|
+
if (!trimmed)
|
|
1397
|
+
throw new Error("encodingAESKey missing");
|
|
1398
|
+
const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`;
|
|
1399
|
+
const key = Buffer.from(withPadding, "base64");
|
|
1400
|
+
if (key.length !== CRYPTO_CONSTANTS.AES_KEY_LENGTH) {
|
|
1401
|
+
throw new Error(`invalid encodingAESKey (expected ${CRYPTO_CONSTANTS.AES_KEY_LENGTH} bytes, got ${key.length})`);
|
|
1402
|
+
}
|
|
1403
|
+
return key;
|
|
1404
|
+
}
|
|
1405
|
+
__name(decodeEncodingAESKey, "decodeEncodingAESKey");
|
|
1406
|
+
function pkcs7Pad(buf, blockSize) {
|
|
1407
|
+
const mod = buf.length % blockSize;
|
|
1408
|
+
const pad = mod === 0 ? blockSize : blockSize - mod;
|
|
1409
|
+
const padByte = Buffer.alloc(1, pad);
|
|
1410
|
+
return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]);
|
|
1411
|
+
}
|
|
1412
|
+
__name(pkcs7Pad, "pkcs7Pad");
|
|
1413
|
+
function pkcs7Unpad(buf, blockSize) {
|
|
1414
|
+
if (buf.length === 0)
|
|
1415
|
+
throw new Error("invalid pkcs7 payload");
|
|
1416
|
+
const pad = buf[buf.length - 1];
|
|
1417
|
+
if (pad < 1 || pad > blockSize) {
|
|
1418
|
+
throw new Error("invalid pkcs7 padding value");
|
|
1419
|
+
}
|
|
1420
|
+
if (pad > buf.length) {
|
|
1421
|
+
throw new Error("invalid pkcs7 payload length");
|
|
1422
|
+
}
|
|
1423
|
+
for (let i = 0; i < pad; i += 1) {
|
|
1424
|
+
if (buf[buf.length - 1 - i] !== pad) {
|
|
1425
|
+
throw new Error("invalid pkcs7 padding byte");
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return buf.subarray(0, buf.length - pad);
|
|
1429
|
+
}
|
|
1430
|
+
__name(pkcs7Unpad, "pkcs7Unpad");
|
|
1431
|
+
function sha1Hex(input) {
|
|
1432
|
+
return crypto$1.createHash("sha1").update(input).digest("hex");
|
|
1433
|
+
}
|
|
1434
|
+
__name(sha1Hex, "sha1Hex");
|
|
1435
|
+
var WecomCrypto = class {
|
|
1436
|
+
static {
|
|
1437
|
+
__name(this, "WecomCrypto");
|
|
1438
|
+
}
|
|
1439
|
+
constructor(token, encodingAESKey, receiveId) {
|
|
1440
|
+
this.token = token;
|
|
1441
|
+
this.encodingAESKey = encodingAESKey;
|
|
1442
|
+
this.receiveId = receiveId;
|
|
1443
|
+
if (!token)
|
|
1444
|
+
throw new Error("token is required");
|
|
1445
|
+
this.aesKey = decodeEncodingAESKey(encodingAESKey);
|
|
1446
|
+
this.iv = this.aesKey.subarray(0, 16);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* 计算 WeCom 消息签名
|
|
1450
|
+
*/
|
|
1451
|
+
computeSignature(timestamp, nonce, encrypt) {
|
|
1452
|
+
const parts = [this.token, timestamp, nonce, encrypt].map((v) => String(v ?? "")).sort();
|
|
1453
|
+
return sha1Hex(parts.join(""));
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* 验证 WeCom 消息签名
|
|
1457
|
+
*/
|
|
1458
|
+
verifySignature(signature, timestamp, nonce, encrypt) {
|
|
1459
|
+
const expected = this.computeSignature(timestamp, nonce, encrypt);
|
|
1460
|
+
return expected === signature;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* 消息解密
|
|
1464
|
+
* 返回纯文本字符串(XML 或 JSON 根据上层业务而定)
|
|
1465
|
+
*/
|
|
1466
|
+
decrypt(encryptText) {
|
|
1467
|
+
const decipher = crypto$1.createDecipheriv("aes-256-cbc", this.aesKey, this.iv);
|
|
1468
|
+
decipher.setAutoPadding(false);
|
|
1469
|
+
const decryptedPadded = Buffer.concat([
|
|
1470
|
+
decipher.update(Buffer.from(encryptText, "base64")),
|
|
1471
|
+
decipher.final()
|
|
1472
|
+
]);
|
|
1473
|
+
const decrypted = pkcs7Unpad(decryptedPadded, CRYPTO_CONSTANTS.PKCS7_BLOCK_SIZE);
|
|
1474
|
+
if (decrypted.length < 20) {
|
|
1475
|
+
throw new Error(`invalid payload (expected >=20 bytes, got ${decrypted.length})`);
|
|
1476
|
+
}
|
|
1477
|
+
const msgLen = decrypted.readUInt32BE(16);
|
|
1478
|
+
const msgStart = 20;
|
|
1479
|
+
const msgEnd = msgStart + msgLen;
|
|
1480
|
+
if (msgEnd > decrypted.length) {
|
|
1481
|
+
throw new Error(`invalid msg length (msgEnd=${msgEnd}, total=${decrypted.length})`);
|
|
1482
|
+
}
|
|
1483
|
+
const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
|
|
1484
|
+
const receiveId = this.receiveId ?? "";
|
|
1485
|
+
if (receiveId) {
|
|
1486
|
+
const trailing = decrypted.subarray(msgEnd).toString("utf8");
|
|
1487
|
+
if (trailing !== receiveId) {
|
|
1488
|
+
throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return msg;
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* 消息加密
|
|
1495
|
+
* 加密明文并返回 base64 格式密文与对应的新签名
|
|
1496
|
+
*/
|
|
1497
|
+
encrypt(plainText, timestamp, nonce) {
|
|
1498
|
+
const random16 = crypto$1.randomBytes(16);
|
|
1499
|
+
const msgBuf = Buffer.from(plainText ?? "", "utf8");
|
|
1500
|
+
const msgLen = Buffer.alloc(4);
|
|
1501
|
+
msgLen.writeUInt32BE(msgBuf.length, 0);
|
|
1502
|
+
const receiveIdBuf = Buffer.from(this.receiveId ?? "", "utf8");
|
|
1503
|
+
const raw = Buffer.concat([random16, msgLen, msgBuf, receiveIdBuf]);
|
|
1504
|
+
const padded = pkcs7Pad(raw, CRYPTO_CONSTANTS.PKCS7_BLOCK_SIZE);
|
|
1505
|
+
const cipher = crypto$1.createCipheriv("aes-256-cbc", this.aesKey, this.iv);
|
|
1506
|
+
cipher.setAutoPadding(false);
|
|
1507
|
+
const encryptedBuf = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
1508
|
+
const encryptBase64 = encryptedBuf.toString("base64");
|
|
1509
|
+
const signature = this.computeSignature(timestamp, nonce, encryptBase64);
|
|
1510
|
+
return { encrypt: encryptBase64, signature };
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
var AiBot = {
|
|
1514
|
+
WSClient: WSClient2
|
|
1515
|
+
};
|
|
1516
|
+
exports.DefaultLogger = DefaultLogger;
|
|
1517
|
+
exports.MessageHandler = MessageHandler;
|
|
1518
|
+
exports.WSAuthFailureError = WSAuthFailureError;
|
|
1519
|
+
exports.WSClient = WSClient2;
|
|
1520
|
+
exports.WSReconnectExhaustedError = WSReconnectExhaustedError;
|
|
1521
|
+
exports.WeComApiClient = WeComApiClient;
|
|
1522
|
+
exports.WecomCrypto = WecomCrypto;
|
|
1523
|
+
exports.WsCmd = WsCmd;
|
|
1524
|
+
exports.WsConnectionManager = WsConnectionManager;
|
|
1525
|
+
exports.decodeEncodingAESKey = decodeEncodingAESKey;
|
|
1526
|
+
exports.decryptFile = decryptFile2;
|
|
1527
|
+
exports.default = AiBot;
|
|
1528
|
+
exports.generateRandomString = generateRandomString;
|
|
1529
|
+
exports.generateReqId = generateReqId;
|
|
1530
|
+
exports.pkcs7Pad = pkcs7Pad;
|
|
1531
|
+
exports.pkcs7Unpad = pkcs7Unpad;
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
// packages/channels/wecom/dist/index.js
|
|
1536
|
+
init_esbuild_shims();
|
|
1537
|
+
|
|
1538
|
+
// packages/channels/wecom/dist/WeComAdapter.js
|
|
1539
|
+
init_esbuild_shims();
|
|
1540
|
+
var import_aibot_node_sdk = __toESM(require_index_cjs(), 1);
|
|
1541
|
+
import { constants, lstatSync, mkdirSync, realpathSync, rmSync } from "node:fs";
|
|
1542
|
+
import { open, writeFile } from "node:fs/promises";
|
|
1543
|
+
import { request as httpsRequest } from "node:https";
|
|
1544
|
+
import { randomUUID } from "node:crypto";
|
|
1545
|
+
import { basename, join, resolve, win32, posix } from "node:path";
|
|
1546
|
+
import { tmpdir } from "node:os";
|
|
1547
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
1548
|
+
import { isIP } from "node:net";
|
|
1549
|
+
import { lookup } from "node:dns/promises";
|
|
1550
|
+
var ClientCtor = import_aibot_node_sdk.WSClient;
|
|
1551
|
+
var MESSAGE_EVENTS = [
|
|
1552
|
+
"message.text",
|
|
1553
|
+
"message.image",
|
|
1554
|
+
"message.mixed",
|
|
1555
|
+
"message.voice",
|
|
1556
|
+
"message.file",
|
|
1557
|
+
"message.video"
|
|
1558
|
+
];
|
|
1559
|
+
var SENSITIVE_ERROR_FIELDS = /* @__PURE__ */ new Set([
|
|
1560
|
+
"secret",
|
|
1561
|
+
"aeskey",
|
|
1562
|
+
"token",
|
|
1563
|
+
"password",
|
|
1564
|
+
"authorization"
|
|
1565
|
+
]);
|
|
1566
|
+
var DEDUP_TTL_MS = 5 * 60 * 1e3;
|
|
1567
|
+
var MAX_MEDIA_BYTES = 20 * 1024 * 1024;
|
|
1568
|
+
var MARKDOWN_CHUNK_BYTES = 3800;
|
|
1569
|
+
var AUTHENTICATION_TIMEOUT_MS = 3e4;
|
|
1570
|
+
var KICK_RECONNECT_MAX_ATTEMPTS = 3;
|
|
1571
|
+
var KICK_RECONNECT_MAX_RETRY_CYCLES = 3;
|
|
1572
|
+
var KICK_RECONNECT_BASE_DELAY_MS = 1e3;
|
|
1573
|
+
var KICK_RECONNECT_RESET_MS = 6e4;
|
|
1574
|
+
var KICK_RECONNECT_RETRY_MS = 5 * 60 * 1e3;
|
|
1575
|
+
var KICK_RECONNECT_LONG_RETRY_MS = 15 * 60 * 1e3;
|
|
1576
|
+
var DISCONNECT_RECONNECT_FALLBACK_MS = 3e4;
|
|
1577
|
+
var ACTIVITY_WATCHDOG_INTERVAL_MS = 6e4;
|
|
1578
|
+
var ACTIVITY_STALE_MS = 5 * 6e4;
|
|
1579
|
+
var WeComChannel = class extends ChannelBase {
|
|
1580
|
+
static {
|
|
1581
|
+
__name(this, "WeComChannel");
|
|
1582
|
+
}
|
|
1583
|
+
wecom;
|
|
1584
|
+
client;
|
|
1585
|
+
seenMessages = /* @__PURE__ */ new Map();
|
|
1586
|
+
inFlightMessages = /* @__PURE__ */ new Set();
|
|
1587
|
+
attachmentDirsByMessage = /* @__PURE__ */ new Map();
|
|
1588
|
+
attachmentMessageByDir = /* @__PURE__ */ new Map();
|
|
1589
|
+
attachmentDirsBySession = /* @__PURE__ */ new Map();
|
|
1590
|
+
attachmentDirsWithoutMessageByRoute = /* @__PURE__ */ new Map();
|
|
1591
|
+
bufferedAttachmentMessages = /* @__PURE__ */ new Set();
|
|
1592
|
+
coalescedAttachmentMessages = /* @__PURE__ */ new Map();
|
|
1593
|
+
dedupTimer;
|
|
1594
|
+
kickReconnectReset;
|
|
1595
|
+
kickReconnectRetry;
|
|
1596
|
+
disconnectReconnectFallback;
|
|
1597
|
+
activityWatchdog;
|
|
1598
|
+
lastActivityAt = 0;
|
|
1599
|
+
connecting;
|
|
1600
|
+
connectingClient;
|
|
1601
|
+
authentication;
|
|
1602
|
+
disconnectGeneration = 0;
|
|
1603
|
+
clientHandlers;
|
|
1604
|
+
reconnectingAfterKick = false;
|
|
1605
|
+
pendingKickReconnect = false;
|
|
1606
|
+
kickReconnectAttempts = 0;
|
|
1607
|
+
kickReconnectRetryCycles = 0;
|
|
1608
|
+
constructor(name, config, bridge, options) {
|
|
1609
|
+
super(name, config, bridge, options);
|
|
1610
|
+
this.wecom = parseWeComConfig(name, config);
|
|
1611
|
+
}
|
|
1612
|
+
async connect() {
|
|
1613
|
+
if (this.client)
|
|
1614
|
+
return;
|
|
1615
|
+
if (this.connecting)
|
|
1616
|
+
return this.connecting;
|
|
1617
|
+
const connecting = this.openClient();
|
|
1618
|
+
this.connecting = connecting;
|
|
1619
|
+
try {
|
|
1620
|
+
await connecting;
|
|
1621
|
+
} finally {
|
|
1622
|
+
if (this.connecting === connecting)
|
|
1623
|
+
this.connecting = void 0;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
async openClient() {
|
|
1627
|
+
const options = {
|
|
1628
|
+
botId: this.wecom.botId,
|
|
1629
|
+
secret: this.wecom.secret,
|
|
1630
|
+
logger: createWeComLogger(this.name)
|
|
1631
|
+
};
|
|
1632
|
+
if (this.wecom.wsUrl) {
|
|
1633
|
+
options.wsUrl = this.wecom.wsUrl;
|
|
1634
|
+
}
|
|
1635
|
+
const client = new ClientCtor(options);
|
|
1636
|
+
let authenticated = false;
|
|
1637
|
+
const connectionGeneration = this.disconnectGeneration;
|
|
1638
|
+
const messageHandler = /* @__PURE__ */ __name((payload) => {
|
|
1639
|
+
this.recordActivity();
|
|
1640
|
+
if (!authenticated) {
|
|
1641
|
+
process.stderr.write(`[WeCom:${this.name}] dropping message before authentication.
|
|
1642
|
+
`);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
this.clearDisconnectReconnectFallback();
|
|
1646
|
+
this.onMessage(payload, connectionGeneration).catch((err) => {
|
|
1647
|
+
const logMessageId = getLogMessageId(payload);
|
|
1648
|
+
process.stderr.write(`[WeCom:${this.name}] message handling failed for ${logMessageId}: ${sanitizeLogText(formatSdkError(err), 200)}
|
|
1649
|
+
`);
|
|
1650
|
+
});
|
|
1651
|
+
}, "messageHandler");
|
|
1652
|
+
const errorHandler = /* @__PURE__ */ __name((err) => {
|
|
1653
|
+
this.recordActivity();
|
|
1654
|
+
process.stderr.write(`[WeCom:${this.name}] SDK error: ${sanitizeLogText(formatSdkError(err), 200)}
|
|
1655
|
+
`);
|
|
1656
|
+
}, "errorHandler");
|
|
1657
|
+
const disconnectedHandler = /* @__PURE__ */ __name((reason) => {
|
|
1658
|
+
this.recordActivity();
|
|
1659
|
+
if (this.disconnectGeneration !== connectionGeneration)
|
|
1660
|
+
return;
|
|
1661
|
+
process.stderr.write(`[WeCom:${this.name}] WebSocket ${formatDisconnectReason(reason)}; waiting for SDK reconnect.
|
|
1662
|
+
`);
|
|
1663
|
+
if (authenticated) {
|
|
1664
|
+
this.scheduleDisconnectReconnectFallback(reason, client, this.disconnectGeneration);
|
|
1665
|
+
}
|
|
1666
|
+
}, "disconnectedHandler");
|
|
1667
|
+
const kickedHandler = /* @__PURE__ */ __name((reason) => {
|
|
1668
|
+
this.recordActivity();
|
|
1669
|
+
if (this.disconnectGeneration !== connectionGeneration)
|
|
1670
|
+
return;
|
|
1671
|
+
this.clearDisconnectReconnectFallback();
|
|
1672
|
+
this.startKickReconnect(reason);
|
|
1673
|
+
}, "kickedHandler");
|
|
1674
|
+
const handlers = {
|
|
1675
|
+
message: messageHandler,
|
|
1676
|
+
error: errorHandler,
|
|
1677
|
+
disconnected: disconnectedHandler,
|
|
1678
|
+
kicked: kickedHandler
|
|
1679
|
+
};
|
|
1680
|
+
for (const event of MESSAGE_EVENTS) {
|
|
1681
|
+
client.on(event, messageHandler);
|
|
1682
|
+
}
|
|
1683
|
+
client.on("error", errorHandler);
|
|
1684
|
+
client.on("disconnected", disconnectedHandler);
|
|
1685
|
+
client.on("event.disconnected_event", kickedHandler);
|
|
1686
|
+
this.clientHandlers = handlers;
|
|
1687
|
+
this.connectingClient = client;
|
|
1688
|
+
const authentication = waitForAuthentication(client);
|
|
1689
|
+
this.authentication = authentication;
|
|
1690
|
+
try {
|
|
1691
|
+
authentication.promise.catch(() => {
|
|
1692
|
+
});
|
|
1693
|
+
const connected = client.connect();
|
|
1694
|
+
const connectedPromise = isPromiseLike(connected) ? withTimeout(Promise.resolve(connected).then(() => {
|
|
1695
|
+
}), AUTHENTICATION_TIMEOUT_MS, "WeCom SDK connect timed out.") : Promise.resolve();
|
|
1696
|
+
await Promise.all([connectedPromise, authentication.promise]);
|
|
1697
|
+
authenticated = true;
|
|
1698
|
+
if (this.connectingClient !== client) {
|
|
1699
|
+
throw new Error("WeCom connection was replaced before authentication.");
|
|
1700
|
+
}
|
|
1701
|
+
this.client = client;
|
|
1702
|
+
this.connectingClient = void 0;
|
|
1703
|
+
this.authentication = void 0;
|
|
1704
|
+
this.recordActivity();
|
|
1705
|
+
this.startActivityWatchdog(connectionGeneration);
|
|
1706
|
+
} catch (err) {
|
|
1707
|
+
authentication.cancel();
|
|
1708
|
+
try {
|
|
1709
|
+
this.detachClientHandlers(client, handlers);
|
|
1710
|
+
} catch {
|
|
1711
|
+
}
|
|
1712
|
+
if (this.connectingClient === client)
|
|
1713
|
+
this.connectingClient = void 0;
|
|
1714
|
+
if (this.authentication === authentication)
|
|
1715
|
+
this.authentication = void 0;
|
|
1716
|
+
try {
|
|
1717
|
+
client.disconnect();
|
|
1718
|
+
} catch {
|
|
1719
|
+
}
|
|
1720
|
+
throw err;
|
|
1721
|
+
}
|
|
1722
|
+
if (!this.dedupTimer) {
|
|
1723
|
+
this.dedupTimer = setInterval(() => this.cleanupSeenMessages(), 6e4);
|
|
1724
|
+
this.dedupTimer.unref?.();
|
|
1725
|
+
}
|
|
1726
|
+
process.stderr.write(`[WeCom:${this.name}] Connected via smart bot.
|
|
1727
|
+
`);
|
|
1728
|
+
}
|
|
1729
|
+
disconnect() {
|
|
1730
|
+
this.disconnectGeneration += 1;
|
|
1731
|
+
this.kickReconnectAttempts = 0;
|
|
1732
|
+
this.kickReconnectRetryCycles = 0;
|
|
1733
|
+
this.pendingKickReconnect = false;
|
|
1734
|
+
if (this.kickReconnectReset) {
|
|
1735
|
+
clearTimeout(this.kickReconnectReset);
|
|
1736
|
+
this.kickReconnectReset = void 0;
|
|
1737
|
+
}
|
|
1738
|
+
if (this.kickReconnectRetry) {
|
|
1739
|
+
clearTimeout(this.kickReconnectRetry);
|
|
1740
|
+
this.kickReconnectRetry = void 0;
|
|
1741
|
+
}
|
|
1742
|
+
this.clearDisconnectReconnectFallback();
|
|
1743
|
+
this.clearActivityWatchdog();
|
|
1744
|
+
if (this.dedupTimer) {
|
|
1745
|
+
clearInterval(this.dedupTimer);
|
|
1746
|
+
this.dedupTimer = void 0;
|
|
1747
|
+
}
|
|
1748
|
+
this.seenMessages.clear();
|
|
1749
|
+
this.inFlightMessages.clear();
|
|
1750
|
+
this.cleanupAllAttachmentDirs();
|
|
1751
|
+
this.disconnectClientOnly(new Error("WeCom channel disconnected."));
|
|
1752
|
+
process.stderr.write(`[WeCom:${this.name}] Disconnected.
|
|
1753
|
+
`);
|
|
1754
|
+
}
|
|
1755
|
+
supportsProactiveSend() {
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
async sendMessage(chatId, text) {
|
|
1759
|
+
const client = this.client;
|
|
1760
|
+
if (!client) {
|
|
1761
|
+
throw new Error(`[WeCom:${this.name}] No active SDK client, cannot send.`);
|
|
1762
|
+
}
|
|
1763
|
+
const { cleanedText, media } = parseOutboundMediaMarkers(text);
|
|
1764
|
+
const chunks = splitMarkdownChunks(cleanedText);
|
|
1765
|
+
if (chunks.length === 0 && media.length === 0) {
|
|
1766
|
+
process.stderr.write(`[WeCom:${this.name}] sendMessage produced empty payload for chatId=${sanitizeLogText(chatId, 100)}.
|
|
1767
|
+
`);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
for (const chunk of chunks) {
|
|
1771
|
+
await client.sendMessage(chatId, {
|
|
1772
|
+
msgtype: "markdown",
|
|
1773
|
+
markdown: { content: chunk }
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
const mediaErrors = [];
|
|
1777
|
+
for (const item of media) {
|
|
1778
|
+
if (item.type !== "image") {
|
|
1779
|
+
process.stderr.write(`[WeCom:${this.name}] skipping unsupported outbound media marker: ${item.type}
|
|
1780
|
+
`);
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
const file = await readOutboundMedia(item.path, this.config.cwd);
|
|
1785
|
+
const upload = await client.uploadMedia(file.data, {
|
|
1786
|
+
type: item.type,
|
|
1787
|
+
filename: file.fileName
|
|
1788
|
+
});
|
|
1789
|
+
const mediaId = extractMediaId(upload);
|
|
1790
|
+
if (!mediaId) {
|
|
1791
|
+
mediaErrors.push(`upload returned no media_id for ${item.type}`);
|
|
1792
|
+
process.stderr.write(`[WeCom:${this.name}] upload returned no media_id, skipping.
|
|
1793
|
+
`);
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
await client.sendMediaMessage(chatId, item.type, mediaId);
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
const message = sanitizeLogText(formatSdkError(err), 200);
|
|
1799
|
+
mediaErrors.push(`${item.type}: ${message}`);
|
|
1800
|
+
process.stderr.write(`[WeCom:${this.name}] media send failed for ${item.type}: ${message}
|
|
1801
|
+
`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (mediaErrors.length > 0) {
|
|
1805
|
+
const message = `[WeCom:${this.name}] ${mediaErrors.length} media send(s) failed (markdown text may already be delivered): ${mediaErrors.join("; ")}`;
|
|
1806
|
+
process.stderr.write(`${message}
|
|
1807
|
+
`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
async onMessage(payload, connectionGeneration) {
|
|
1811
|
+
const body = extractBody(payload);
|
|
1812
|
+
if (!body) {
|
|
1813
|
+
process.stderr.write(`[WeCom:${this.name}] dropping message with unrecognized payload structure.
|
|
1814
|
+
`);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const rawMessageId = getString(body, "msgid") || void 0;
|
|
1818
|
+
const messageId = rawMessageId ?? `synthetic-${randomUUID()}`;
|
|
1819
|
+
const logMessageId = sanitizeLogText(rawMessageId || "(no id)", 100);
|
|
1820
|
+
const from = getRecord(body, "from");
|
|
1821
|
+
const senderId = getString(from, "userid") || "";
|
|
1822
|
+
const senderName = getString(from, "name") || senderId || "Unknown";
|
|
1823
|
+
const isGroup = getString(body, "chattype") === "group";
|
|
1824
|
+
const rawChatId = getString(body, "chatid");
|
|
1825
|
+
const chatId = isGroup ? rawChatId : rawChatId || senderId;
|
|
1826
|
+
if (!chatId || !senderId) {
|
|
1827
|
+
process.stderr.write(`[WeCom:${this.name}] dropping message ${logMessageId}: missing ${!senderId ? "senderId" : "chatId"}.
|
|
1828
|
+
`);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (rawMessageId) {
|
|
1832
|
+
if (this.inFlightMessages.has(rawMessageId)) {
|
|
1833
|
+
process.stderr.write(`[WeCom:${this.name}] dropping duplicate message ${logMessageId} (already in flight).
|
|
1834
|
+
`);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (this.seenMessages.has(rawMessageId)) {
|
|
1838
|
+
process.stderr.write(`[WeCom:${this.name}] dropping duplicate message ${logMessageId} (already seen).
|
|
1839
|
+
`);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
this.inFlightMessages.add(rawMessageId);
|
|
1843
|
+
}
|
|
1844
|
+
const text = extractText(body);
|
|
1845
|
+
const explicitMention = getExplicitMention(body, this.wecom.botId);
|
|
1846
|
+
const quote = getRecord(body, "quote");
|
|
1847
|
+
const envelope = {
|
|
1848
|
+
channelName: this.name,
|
|
1849
|
+
senderId,
|
|
1850
|
+
senderName,
|
|
1851
|
+
chatId,
|
|
1852
|
+
text,
|
|
1853
|
+
messageId: rawMessageId ?? messageId,
|
|
1854
|
+
isGroup,
|
|
1855
|
+
isMentioned: !isGroup || (explicitMention ?? false),
|
|
1856
|
+
isReplyToBot: getString(getRecord(quote, "from"), "userid") === this.wecom.botId,
|
|
1857
|
+
referencedText: extractQuoteText(quote)
|
|
1858
|
+
};
|
|
1859
|
+
let attachments = [];
|
|
1860
|
+
const attachmentRouteKey = this.attachmentRouteKey(senderId, chatId, envelope.threadId);
|
|
1861
|
+
let processStarted = false;
|
|
1862
|
+
try {
|
|
1863
|
+
if (!await this.preflightInbound(envelope)) {
|
|
1864
|
+
process.stderr.write(`[WeCom:${this.name}] dropping message ${logMessageId}: preflight rejected.
|
|
1865
|
+
`);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
attachments = await this.downloadAttachments(body, attachments, messageId, attachmentRouteKey, connectionGeneration);
|
|
1869
|
+
if (this.disconnectGeneration !== connectionGeneration) {
|
|
1870
|
+
process.stderr.write(`[WeCom:${this.name}] dropping message ${logMessageId}: connection changed during attachment download.
|
|
1871
|
+
`);
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (attachments.length) {
|
|
1875
|
+
envelope.attachments = attachments;
|
|
1876
|
+
}
|
|
1877
|
+
if (!envelope.text && attachments.length) {
|
|
1878
|
+
envelope.text = attachments.some((a) => a.type === "image") ? "(image)" : `(file: ${attachments[0]?.fileName ?? "file"})`;
|
|
1879
|
+
}
|
|
1880
|
+
if (rawMessageId)
|
|
1881
|
+
this.seenMessages.set(rawMessageId, Date.now());
|
|
1882
|
+
processStarted = true;
|
|
1883
|
+
await this.processInbound(envelope);
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
if (rawMessageId && !processStarted) {
|
|
1886
|
+
this.seenMessages.delete(rawMessageId);
|
|
1887
|
+
} else if (rawMessageId) {
|
|
1888
|
+
process.stderr.write(`[WeCom:${this.name}] message ${logMessageId} failed after processing started; dedup entry retained.
|
|
1889
|
+
`);
|
|
1890
|
+
}
|
|
1891
|
+
throw err;
|
|
1892
|
+
} finally {
|
|
1893
|
+
if (rawMessageId)
|
|
1894
|
+
this.inFlightMessages.delete(rawMessageId);
|
|
1895
|
+
if (messageId && !this.bufferedAttachmentMessages.has(messageId) && this.attachmentDirsByMessage.has(messageId)) {
|
|
1896
|
+
this.cleanupAttachmentDirsForMessage(messageId);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
async downloadAttachments(body, attachments = [], messageId, routeKey, connectionGeneration = this.disconnectGeneration) {
|
|
1901
|
+
const refs = collectInboundMediaRefs(body);
|
|
1902
|
+
for (const ref of refs) {
|
|
1903
|
+
if (this.disconnectGeneration !== connectionGeneration)
|
|
1904
|
+
return attachments;
|
|
1905
|
+
let downloaded;
|
|
1906
|
+
try {
|
|
1907
|
+
downloaded = await downloadInboundMedia(ref);
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
process.stderr.write(`[WeCom:${this.name}] skipping ${ref.type} attachment: ${sanitizeLogText(err instanceof Error ? err.message : String(err), 160)}.
|
|
1910
|
+
`);
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (this.disconnectGeneration !== connectionGeneration)
|
|
1914
|
+
return attachments;
|
|
1915
|
+
const data = downloaded.buffer;
|
|
1916
|
+
const fileName = sanitizeFileName(ref.fileName || downloaded.filename);
|
|
1917
|
+
if (ref.type === "image") {
|
|
1918
|
+
attachments.push({
|
|
1919
|
+
type: "image",
|
|
1920
|
+
data: data.toString("base64"),
|
|
1921
|
+
mimeType: detectImageMime(data),
|
|
1922
|
+
fileName
|
|
1923
|
+
});
|
|
1924
|
+
} else {
|
|
1925
|
+
const dir = join(tmpdir(), "channel-files", randomUUID());
|
|
1926
|
+
const safeName = fileName || `wecom_${ref.type}`;
|
|
1927
|
+
const filePath = join(dir, safeName);
|
|
1928
|
+
try {
|
|
1929
|
+
if (this.disconnectGeneration !== connectionGeneration) {
|
|
1930
|
+
return attachments;
|
|
1931
|
+
}
|
|
1932
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1933
|
+
await writeFile(filePath, data, { mode: 384 });
|
|
1934
|
+
if (this.disconnectGeneration !== connectionGeneration) {
|
|
1935
|
+
cleanupAttachmentDirs([dir]);
|
|
1936
|
+
return attachments;
|
|
1937
|
+
}
|
|
1938
|
+
this.rememberAttachmentDir(dir, messageId, routeKey);
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
cleanupAttachmentDirs([dir]);
|
|
1941
|
+
process.stderr.write(`[WeCom:${this.name}] skipping ${ref.type} attachment: ${sanitizeLogText(err instanceof Error ? err.message : String(err), 160)}.
|
|
1942
|
+
`);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
attachments.push({
|
|
1946
|
+
type: ref.type === "voice" ? "audio" : ref.type,
|
|
1947
|
+
filePath,
|
|
1948
|
+
mimeType: mediaTypeToMime(ref.type),
|
|
1949
|
+
fileName: safeName
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return attachments;
|
|
1954
|
+
}
|
|
1955
|
+
onPromptBuffered(_chatId, sessionId, messageId) {
|
|
1956
|
+
if (!messageId) {
|
|
1957
|
+
this.rememberUntrackedDirsForSession(sessionId);
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
this.bufferedAttachmentMessages.add(messageId);
|
|
1961
|
+
this.rememberMessageDirsForSession(messageId, sessionId);
|
|
1962
|
+
}
|
|
1963
|
+
onPromptStart(_chatId, sessionId, messageId) {
|
|
1964
|
+
if (messageId) {
|
|
1965
|
+
this.rememberMessageDirsForSession(messageId, sessionId);
|
|
1966
|
+
} else {
|
|
1967
|
+
this.rememberUntrackedDirsForSession(sessionId);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
onPromptBufferDrained(_chatId, _sessionId, messageIds) {
|
|
1971
|
+
const lastMessageId = messageIds.at(-1);
|
|
1972
|
+
if (lastMessageId) {
|
|
1973
|
+
this.coalescedAttachmentMessages.set(lastMessageId, messageIds);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
onPromptBufferDropped(_chatId, sessionId, messageIds) {
|
|
1977
|
+
for (const messageId of messageIds) {
|
|
1978
|
+
this.cleanupAttachmentDirsForMessage(messageId);
|
|
1979
|
+
}
|
|
1980
|
+
this.cleanupUntrackedAttachmentDirsForSession(sessionId);
|
|
1981
|
+
}
|
|
1982
|
+
onPromptEnd(_chatId, sessionId, messageId) {
|
|
1983
|
+
if (!messageId) {
|
|
1984
|
+
this.cleanupAttachmentDirsForSession(sessionId);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const coalescedMessageIds = this.coalescedAttachmentMessages.get(messageId);
|
|
1988
|
+
if (coalescedMessageIds) {
|
|
1989
|
+
this.coalescedAttachmentMessages.delete(messageId);
|
|
1990
|
+
for (const coalescedMessageId of coalescedMessageIds) {
|
|
1991
|
+
this.cleanupAttachmentDirsForMessage(coalescedMessageId);
|
|
1992
|
+
}
|
|
1993
|
+
this.cleanupUntrackedAttachmentDirsForSession(sessionId);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
this.cleanupAttachmentDirsForMessage(messageId);
|
|
1997
|
+
}
|
|
1998
|
+
rememberAttachmentDir(dir, messageId, routeKey) {
|
|
1999
|
+
if (messageId) {
|
|
2000
|
+
const messageDirs = this.attachmentDirsByMessage.get(messageId) ?? [];
|
|
2001
|
+
messageDirs.push(dir);
|
|
2002
|
+
this.attachmentDirsByMessage.set(messageId, messageDirs);
|
|
2003
|
+
this.attachmentMessageByDir.set(dir, messageId);
|
|
2004
|
+
} else if (routeKey) {
|
|
2005
|
+
const dirs = this.attachmentDirsWithoutMessageByRoute.get(routeKey) ?? [];
|
|
2006
|
+
dirs.push(dir);
|
|
2007
|
+
this.attachmentDirsWithoutMessageByRoute.set(routeKey, dirs);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
rememberMessageDirsForSession(messageId, sessionId) {
|
|
2011
|
+
const dirs = this.attachmentDirsByMessage.get(messageId);
|
|
2012
|
+
if (!dirs)
|
|
2013
|
+
return;
|
|
2014
|
+
const sessionDirs = this.attachmentDirsBySession.get(sessionId) ?? [];
|
|
2015
|
+
for (const dir of dirs) {
|
|
2016
|
+
if (!sessionDirs.includes(dir))
|
|
2017
|
+
sessionDirs.push(dir);
|
|
2018
|
+
}
|
|
2019
|
+
this.attachmentDirsBySession.set(sessionId, sessionDirs);
|
|
2020
|
+
}
|
|
2021
|
+
cleanupAttachmentDirsForMessage(messageId) {
|
|
2022
|
+
this.bufferedAttachmentMessages.delete(messageId);
|
|
2023
|
+
const dirs = this.attachmentDirsByMessage.get(messageId);
|
|
2024
|
+
if (!dirs)
|
|
2025
|
+
return;
|
|
2026
|
+
this.attachmentDirsByMessage.delete(messageId);
|
|
2027
|
+
for (const dir of dirs) {
|
|
2028
|
+
this.attachmentMessageByDir.delete(dir);
|
|
2029
|
+
}
|
|
2030
|
+
this.removeAttachmentDirsFromSessions(dirs);
|
|
2031
|
+
cleanupAttachmentDirs(dirs);
|
|
2032
|
+
}
|
|
2033
|
+
cleanupAttachmentDirsForSession(sessionId) {
|
|
2034
|
+
const dirs = this.attachmentDirsBySession.get(sessionId);
|
|
2035
|
+
if (!dirs)
|
|
2036
|
+
return;
|
|
2037
|
+
this.attachmentDirsBySession.delete(sessionId);
|
|
2038
|
+
this.removeAttachmentDirsFromMessages(dirs);
|
|
2039
|
+
for (const dir of dirs) {
|
|
2040
|
+
this.attachmentMessageByDir.delete(dir);
|
|
2041
|
+
}
|
|
2042
|
+
cleanupAttachmentDirs(dirs);
|
|
2043
|
+
}
|
|
2044
|
+
cleanupUntrackedAttachmentDirsForSession(sessionId) {
|
|
2045
|
+
const dirs = this.attachmentDirsBySession.get(sessionId);
|
|
2046
|
+
if (!dirs)
|
|
2047
|
+
return;
|
|
2048
|
+
const untrackedDirs = dirs.filter((dir) => !this.attachmentMessageByDir.has(dir));
|
|
2049
|
+
if (untrackedDirs.length === 0)
|
|
2050
|
+
return;
|
|
2051
|
+
const remainingDirs = dirs.filter((dir) => this.attachmentMessageByDir.has(dir));
|
|
2052
|
+
if (remainingDirs.length > 0) {
|
|
2053
|
+
this.attachmentDirsBySession.set(sessionId, remainingDirs);
|
|
2054
|
+
} else {
|
|
2055
|
+
this.attachmentDirsBySession.delete(sessionId);
|
|
2056
|
+
}
|
|
2057
|
+
cleanupAttachmentDirs(untrackedDirs);
|
|
2058
|
+
}
|
|
2059
|
+
rememberUntrackedDirsForSession(sessionId) {
|
|
2060
|
+
const routeKey = this.attachmentRouteKeyForSession(sessionId);
|
|
2061
|
+
if (!routeKey)
|
|
2062
|
+
return;
|
|
2063
|
+
const dirs = this.attachmentDirsWithoutMessageByRoute.get(routeKey);
|
|
2064
|
+
if (!dirs || dirs.length === 0)
|
|
2065
|
+
return;
|
|
2066
|
+
const sessionDirs = this.attachmentDirsBySession.get(sessionId) ?? [];
|
|
2067
|
+
for (const dir of dirs) {
|
|
2068
|
+
if (!sessionDirs.includes(dir))
|
|
2069
|
+
sessionDirs.push(dir);
|
|
2070
|
+
}
|
|
2071
|
+
this.attachmentDirsBySession.set(sessionId, sessionDirs);
|
|
2072
|
+
this.attachmentDirsWithoutMessageByRoute.delete(routeKey);
|
|
2073
|
+
}
|
|
2074
|
+
removeAttachmentDirsFromSessions(dirs) {
|
|
2075
|
+
const removed = new Set(dirs);
|
|
2076
|
+
for (const [sessionId, sessionDirs] of this.attachmentDirsBySession) {
|
|
2077
|
+
const remaining = sessionDirs.filter((dir) => !removed.has(dir));
|
|
2078
|
+
if (remaining.length) {
|
|
2079
|
+
this.attachmentDirsBySession.set(sessionId, remaining);
|
|
2080
|
+
} else {
|
|
2081
|
+
this.attachmentDirsBySession.delete(sessionId);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
removeAttachmentDirsFromMessages(dirs) {
|
|
2086
|
+
const removed = new Set(dirs);
|
|
2087
|
+
for (const [messageId, messageDirs] of this.attachmentDirsByMessage) {
|
|
2088
|
+
const remaining = messageDirs.filter((dir) => !removed.has(dir));
|
|
2089
|
+
if (remaining.length) {
|
|
2090
|
+
this.attachmentDirsByMessage.set(messageId, remaining);
|
|
2091
|
+
} else {
|
|
2092
|
+
this.attachmentDirsByMessage.delete(messageId);
|
|
2093
|
+
this.bufferedAttachmentMessages.delete(messageId);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
for (const dir of dirs) {
|
|
2097
|
+
this.attachmentMessageByDir.delete(dir);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
cleanupAllAttachmentDirs() {
|
|
2101
|
+
const dirs = Array.from(/* @__PURE__ */ new Set([
|
|
2102
|
+
...Array.from(this.attachmentDirsBySession.values()).flat(),
|
|
2103
|
+
...Array.from(this.attachmentDirsByMessage.values()).flat(),
|
|
2104
|
+
...Array.from(this.attachmentDirsWithoutMessageByRoute.values()).flat()
|
|
2105
|
+
]));
|
|
2106
|
+
this.attachmentDirsBySession.clear();
|
|
2107
|
+
this.attachmentDirsByMessage.clear();
|
|
2108
|
+
this.attachmentMessageByDir.clear();
|
|
2109
|
+
this.attachmentDirsWithoutMessageByRoute.clear();
|
|
2110
|
+
this.bufferedAttachmentMessages.clear();
|
|
2111
|
+
this.coalescedAttachmentMessages.clear();
|
|
2112
|
+
cleanupAttachmentDirs(dirs);
|
|
2113
|
+
}
|
|
2114
|
+
attachmentRouteKeyForSession(sessionId) {
|
|
2115
|
+
const target = this.router.getTarget(sessionId);
|
|
2116
|
+
if (!target || target.channelName !== this.name)
|
|
2117
|
+
return void 0;
|
|
2118
|
+
return this.attachmentRouteKey(target.senderId, target.chatId, target.threadId);
|
|
2119
|
+
}
|
|
2120
|
+
attachmentRouteKey(senderId, chatId, threadId) {
|
|
2121
|
+
switch (this.config.sessionScope) {
|
|
2122
|
+
case "thread":
|
|
2123
|
+
return `${this.name}:${threadId || chatId}`;
|
|
2124
|
+
case "single":
|
|
2125
|
+
return `${this.name}:__single__`;
|
|
2126
|
+
case "user":
|
|
2127
|
+
default:
|
|
2128
|
+
return `${this.name}:${senderId}:${chatId}`;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
detachClientHandlers(client, handlers = this.clientHandlers) {
|
|
2132
|
+
if (!handlers)
|
|
2133
|
+
return;
|
|
2134
|
+
for (const event of MESSAGE_EVENTS) {
|
|
2135
|
+
client.off?.(event, handlers.message);
|
|
2136
|
+
}
|
|
2137
|
+
client.off?.("error", handlers.error);
|
|
2138
|
+
client.off?.("disconnected", handlers.disconnected);
|
|
2139
|
+
client.off?.("event.disconnected_event", handlers.kicked);
|
|
2140
|
+
if (this.clientHandlers === handlers)
|
|
2141
|
+
this.clientHandlers = void 0;
|
|
2142
|
+
}
|
|
2143
|
+
disconnectClientOnly(err) {
|
|
2144
|
+
const client = this.client ?? this.connectingClient;
|
|
2145
|
+
this.authentication?.cancel(err);
|
|
2146
|
+
this.authentication = void 0;
|
|
2147
|
+
this.client = void 0;
|
|
2148
|
+
this.connectingClient = void 0;
|
|
2149
|
+
this.clearActivityWatchdog();
|
|
2150
|
+
if (client)
|
|
2151
|
+
this.detachClientHandlers(client);
|
|
2152
|
+
try {
|
|
2153
|
+
client?.disconnect();
|
|
2154
|
+
} catch (e) {
|
|
2155
|
+
process.stderr.write(`[WeCom:${this.name}] client.disconnect() threw: ${sanitizeLogText(formatSdkError(e), 200)}
|
|
2156
|
+
`);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
clearDisconnectReconnectFallback() {
|
|
2160
|
+
if (!this.disconnectReconnectFallback)
|
|
2161
|
+
return;
|
|
2162
|
+
clearTimeout(this.disconnectReconnectFallback);
|
|
2163
|
+
this.disconnectReconnectFallback = void 0;
|
|
2164
|
+
}
|
|
2165
|
+
recordActivity() {
|
|
2166
|
+
this.lastActivityAt = Date.now();
|
|
2167
|
+
}
|
|
2168
|
+
clearActivityWatchdog() {
|
|
2169
|
+
if (!this.activityWatchdog)
|
|
2170
|
+
return;
|
|
2171
|
+
clearInterval(this.activityWatchdog);
|
|
2172
|
+
this.activityWatchdog = void 0;
|
|
2173
|
+
}
|
|
2174
|
+
startActivityWatchdog(disconnectGeneration) {
|
|
2175
|
+
this.clearActivityWatchdog();
|
|
2176
|
+
this.activityWatchdog = setInterval(() => {
|
|
2177
|
+
if (this.disconnectGeneration !== disconnectGeneration)
|
|
2178
|
+
return;
|
|
2179
|
+
if (!this.client || this.reconnectingAfterKick)
|
|
2180
|
+
return;
|
|
2181
|
+
if (Date.now() - this.lastActivityAt < ACTIVITY_STALE_MS)
|
|
2182
|
+
return;
|
|
2183
|
+
process.stderr.write(`[WeCom:${this.name}] no SDK activity for ${ACTIVITY_STALE_MS / 6e4} minutes; reconnecting adapter.
|
|
2184
|
+
`);
|
|
2185
|
+
this.kickReconnectAttempts = 0;
|
|
2186
|
+
this.kickReconnectRetryCycles = 0;
|
|
2187
|
+
this.startKickReconnect(new Error("WeCom SDK activity watchdog timed out."), "activity watchdog");
|
|
2188
|
+
}, ACTIVITY_WATCHDOG_INTERVAL_MS);
|
|
2189
|
+
this.activityWatchdog.unref?.();
|
|
2190
|
+
}
|
|
2191
|
+
scheduleDisconnectReconnectFallback(reason, client, disconnectGeneration) {
|
|
2192
|
+
this.clearDisconnectReconnectFallback();
|
|
2193
|
+
const formattedReason = formatDisconnectReason(reason);
|
|
2194
|
+
this.disconnectReconnectFallback = setTimeout(() => {
|
|
2195
|
+
this.disconnectReconnectFallback = void 0;
|
|
2196
|
+
if (this.disconnectGeneration !== disconnectGeneration)
|
|
2197
|
+
return;
|
|
2198
|
+
if (this.client !== client)
|
|
2199
|
+
return;
|
|
2200
|
+
process.stderr.write(`[WeCom:${this.name}] SDK reconnect did not recover after WebSocket ${formattedReason}; reconnecting adapter.
|
|
2201
|
+
`);
|
|
2202
|
+
this.kickReconnectAttempts = 0;
|
|
2203
|
+
this.kickReconnectRetryCycles = 0;
|
|
2204
|
+
this.startKickReconnect(reason, "SDK disconnect");
|
|
2205
|
+
}, DISCONNECT_RECONNECT_FALLBACK_MS);
|
|
2206
|
+
this.disconnectReconnectFallback.unref?.();
|
|
2207
|
+
}
|
|
2208
|
+
async reconnectAfterKick(reason, reconnectReason = "server kick") {
|
|
2209
|
+
if (this.reconnectingAfterKick) {
|
|
2210
|
+
this.pendingKickReconnect = true;
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (this.kickReconnectRetry) {
|
|
2214
|
+
clearTimeout(this.kickReconnectRetry);
|
|
2215
|
+
this.kickReconnectRetry = void 0;
|
|
2216
|
+
this.kickReconnectAttempts = 0;
|
|
2217
|
+
}
|
|
2218
|
+
if (this.kickReconnectReset) {
|
|
2219
|
+
clearTimeout(this.kickReconnectReset);
|
|
2220
|
+
this.kickReconnectReset = void 0;
|
|
2221
|
+
}
|
|
2222
|
+
this.reconnectingAfterKick = true;
|
|
2223
|
+
const previousConnecting = this.connecting;
|
|
2224
|
+
const disconnectGeneration = this.disconnectGeneration;
|
|
2225
|
+
process.stderr.write(`[WeCom:${this.name}] WebSocket ${formatDisconnectReason(reason)}; reconnecting after ${reconnectReason}.
|
|
2226
|
+
`);
|
|
2227
|
+
try {
|
|
2228
|
+
this.disconnectClientOnly(new Error(`WeCom connection was kicked: ${formatDisconnectReason(reason)}`));
|
|
2229
|
+
if (previousConnecting) {
|
|
2230
|
+
await previousConnecting.catch(() => {
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
while (this.kickReconnectAttempts < KICK_RECONNECT_MAX_ATTEMPTS) {
|
|
2234
|
+
const attempt = ++this.kickReconnectAttempts;
|
|
2235
|
+
await delay(KICK_RECONNECT_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1));
|
|
2236
|
+
if (this.disconnectGeneration !== disconnectGeneration) {
|
|
2237
|
+
process.stderr.write(`[WeCom:${this.name}] reconnect after ${reconnectReason} abandoned: connection generation changed.
|
|
2238
|
+
`);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
try {
|
|
2242
|
+
await this.connect();
|
|
2243
|
+
if (this.disconnectGeneration !== disconnectGeneration) {
|
|
2244
|
+
process.stderr.write(`[WeCom:${this.name}] reconnect after ${reconnectReason} abandoned: connection generation changed.
|
|
2245
|
+
`);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
this.kickReconnectAttempts = 0;
|
|
2249
|
+
this.kickReconnectRetryCycles = 0;
|
|
2250
|
+
this.scheduleKickReconnectReset();
|
|
2251
|
+
process.stderr.write(`[WeCom:${this.name}] reconnected after ${reconnectReason}.
|
|
2252
|
+
`);
|
|
2253
|
+
return;
|
|
2254
|
+
} catch (err) {
|
|
2255
|
+
process.stderr.write(`[WeCom:${this.name}] reconnect after ${reconnectReason} attempt ${attempt} failed: ${sanitizeLogText(formatSdkError(err), 200)}
|
|
2256
|
+
`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
this.kickReconnectRetryCycles += 1;
|
|
2260
|
+
if (this.kickReconnectRetryCycles >= KICK_RECONNECT_MAX_RETRY_CYCLES) {
|
|
2261
|
+
process.stderr.write(`[WeCom:${this.name}] reconnect after ${reconnectReason} exhausted ${this.kickReconnectRetryCycles} retry cycles; next attempt in ${KICK_RECONNECT_LONG_RETRY_MS / 6e4} minutes.
|
|
2262
|
+
`);
|
|
2263
|
+
this.scheduleKickReconnectRetry(reason, disconnectGeneration, KICK_RECONNECT_LONG_RETRY_MS, reconnectReason, true);
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
process.stderr.write(`[WeCom:${this.name}] reconnect after ${reconnectReason} gave up after ${KICK_RECONNECT_MAX_ATTEMPTS} attempts; retrying later.
|
|
2267
|
+
`);
|
|
2268
|
+
this.scheduleKickReconnectRetry(reason, disconnectGeneration, KICK_RECONNECT_RETRY_MS, reconnectReason);
|
|
2269
|
+
} finally {
|
|
2270
|
+
this.reconnectingAfterKick = false;
|
|
2271
|
+
const shouldRetryPendingKick = this.pendingKickReconnect && this.disconnectGeneration === disconnectGeneration;
|
|
2272
|
+
this.pendingKickReconnect = false;
|
|
2273
|
+
if (shouldRetryPendingKick && !this.client) {
|
|
2274
|
+
this.kickReconnectAttempts = 0;
|
|
2275
|
+
this.startKickReconnect(reason, reconnectReason);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
scheduleKickReconnectReset() {
|
|
2280
|
+
if (this.kickReconnectRetry) {
|
|
2281
|
+
clearTimeout(this.kickReconnectRetry);
|
|
2282
|
+
this.kickReconnectRetry = void 0;
|
|
2283
|
+
}
|
|
2284
|
+
if (this.kickReconnectReset)
|
|
2285
|
+
clearTimeout(this.kickReconnectReset);
|
|
2286
|
+
this.kickReconnectReset = setTimeout(() => {
|
|
2287
|
+
this.kickReconnectAttempts = 0;
|
|
2288
|
+
this.kickReconnectRetryCycles = 0;
|
|
2289
|
+
this.kickReconnectReset = void 0;
|
|
2290
|
+
}, KICK_RECONNECT_RESET_MS);
|
|
2291
|
+
this.kickReconnectReset.unref?.();
|
|
2292
|
+
}
|
|
2293
|
+
scheduleKickReconnectRetry(reason, disconnectGeneration, delayMs = KICK_RECONNECT_RETRY_MS, reconnectReason = "server kick", resetRetryCycles = false) {
|
|
2294
|
+
this.kickReconnectRetry = setTimeout(() => {
|
|
2295
|
+
this.kickReconnectRetry = void 0;
|
|
2296
|
+
if (this.disconnectGeneration !== disconnectGeneration) {
|
|
2297
|
+
process.stderr.write(`[WeCom:${this.name}] scheduled kick-reconnect cancelled; connection generation changed.
|
|
2298
|
+
`);
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
this.kickReconnectAttempts = 0;
|
|
2302
|
+
if (resetRetryCycles)
|
|
2303
|
+
this.kickReconnectRetryCycles = 0;
|
|
2304
|
+
this.startKickReconnect(reason, reconnectReason);
|
|
2305
|
+
}, delayMs);
|
|
2306
|
+
this.kickReconnectRetry.unref?.();
|
|
2307
|
+
}
|
|
2308
|
+
startKickReconnect(reason, reconnectReason = "server kick") {
|
|
2309
|
+
void this.reconnectAfterKick(reason, reconnectReason).catch((err) => {
|
|
2310
|
+
process.stderr.write(`[WeCom:${this.name}] kick-reconnect failed: ${sanitizeLogText(formatSdkError(err), 200)}
|
|
2311
|
+
`);
|
|
2312
|
+
if (this.kickReconnectRetry === void 0) {
|
|
2313
|
+
this.scheduleKickReconnectRetry(reason, this.disconnectGeneration, KICK_RECONNECT_LONG_RETRY_MS, reconnectReason, true);
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
cleanupSeenMessages() {
|
|
2318
|
+
const now = Date.now();
|
|
2319
|
+
for (const [id, ts] of this.seenMessages) {
|
|
2320
|
+
if (now - ts > DEDUP_TTL_MS) {
|
|
2321
|
+
this.seenMessages.delete(id);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
function waitForAuthentication(client) {
|
|
2327
|
+
let timeout;
|
|
2328
|
+
let settled = false;
|
|
2329
|
+
let finish = /* @__PURE__ */ __name(() => {
|
|
2330
|
+
}, "finish");
|
|
2331
|
+
const onAuth = /* @__PURE__ */ __name(() => finish(), "onAuth");
|
|
2332
|
+
const onError = /* @__PURE__ */ __name((err) => finish(new Error(`WeCom authentication failed: ${sanitizeLogText(String(err), 200)}`)), "onError");
|
|
2333
|
+
const onKicked = /* @__PURE__ */ __name((reason) => finish(new Error(`WeCom authentication interrupted by server kick: ${formatDisconnectReason(reason)}`)), "onKicked");
|
|
2334
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
2335
|
+
finish = /* @__PURE__ */ __name((err) => {
|
|
2336
|
+
if (settled)
|
|
2337
|
+
return;
|
|
2338
|
+
settled = true;
|
|
2339
|
+
if (timeout)
|
|
2340
|
+
clearTimeout(timeout);
|
|
2341
|
+
client.off?.("authenticated", onAuth);
|
|
2342
|
+
client.off?.("error", onError);
|
|
2343
|
+
client.off?.("event.disconnected_event", onKicked);
|
|
2344
|
+
if (err) {
|
|
2345
|
+
rejectPromise(err);
|
|
2346
|
+
} else {
|
|
2347
|
+
resolvePromise();
|
|
2348
|
+
}
|
|
2349
|
+
}, "finish");
|
|
2350
|
+
timeout = setTimeout(() => {
|
|
2351
|
+
finish(new Error("WeCom authentication timed out."));
|
|
2352
|
+
}, AUTHENTICATION_TIMEOUT_MS);
|
|
2353
|
+
timeout.unref?.();
|
|
2354
|
+
client.on("authenticated", onAuth);
|
|
2355
|
+
client.on("error", onError);
|
|
2356
|
+
client.on("event.disconnected_event", onKicked);
|
|
2357
|
+
});
|
|
2358
|
+
return {
|
|
2359
|
+
promise,
|
|
2360
|
+
cancel: /* @__PURE__ */ __name((err) => finish(err), "cancel")
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
__name(waitForAuthentication, "waitForAuthentication");
|
|
2364
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
2365
|
+
let timeout;
|
|
2366
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
2367
|
+
timeout = setTimeout(() => {
|
|
2368
|
+
rejectPromise(new Error(message));
|
|
2369
|
+
}, timeoutMs);
|
|
2370
|
+
timeout.unref?.();
|
|
2371
|
+
promise.then((value) => {
|
|
2372
|
+
if (timeout)
|
|
2373
|
+
clearTimeout(timeout);
|
|
2374
|
+
resolvePromise(value);
|
|
2375
|
+
}, (err) => {
|
|
2376
|
+
if (timeout)
|
|
2377
|
+
clearTimeout(timeout);
|
|
2378
|
+
rejectPromise(err);
|
|
2379
|
+
});
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
__name(withTimeout, "withTimeout");
|
|
2383
|
+
function delay(ms) {
|
|
2384
|
+
return new Promise((resolveDelay) => {
|
|
2385
|
+
const timer = setTimeout(resolveDelay, ms);
|
|
2386
|
+
timer.unref?.();
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
__name(delay, "delay");
|
|
2390
|
+
function cleanupAttachmentDirs(dirs) {
|
|
2391
|
+
for (const dir of dirs) {
|
|
2392
|
+
try {
|
|
2393
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
process.stderr.write(`[WeCom] failed to remove attachment dir ${sanitizeLogText(dir, 200)}: ${sanitizeLogText(formatSdkError(err), 200)}.
|
|
2396
|
+
`);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
__name(cleanupAttachmentDirs, "cleanupAttachmentDirs");
|
|
2401
|
+
function formatDisconnectReason(reason) {
|
|
2402
|
+
const text = typeof reason === "string" && reason ? reason : formatSdkError(reason);
|
|
2403
|
+
return sanitizeLogText(text, 120);
|
|
2404
|
+
}
|
|
2405
|
+
__name(formatDisconnectReason, "formatDisconnectReason");
|
|
2406
|
+
function formatSdkError(err) {
|
|
2407
|
+
if (err instanceof Error)
|
|
2408
|
+
return err.message;
|
|
2409
|
+
const record = asRecord(err);
|
|
2410
|
+
if (record) {
|
|
2411
|
+
const errcode = record["errcode"];
|
|
2412
|
+
const errmsg = record["errmsg"];
|
|
2413
|
+
if (typeof errcode === "number" || typeof errmsg === "string") {
|
|
2414
|
+
return redactSensitiveErrorText(`errcode=${String(errcode)} errmsg=${String(errmsg)}`);
|
|
2415
|
+
}
|
|
2416
|
+
const code = record["code"];
|
|
2417
|
+
const reason = record["reason"];
|
|
2418
|
+
const wasClean = record["wasClean"];
|
|
2419
|
+
if (typeof code === "number" || typeof reason === "string" || typeof wasClean === "boolean") {
|
|
2420
|
+
return [
|
|
2421
|
+
typeof code === "number" ? `code=${code}` : void 0,
|
|
2422
|
+
typeof reason === "string" ? `reason=${redactSensitiveErrorText(reason)}` : void 0,
|
|
2423
|
+
typeof wasClean === "boolean" ? `wasClean=${wasClean}` : void 0
|
|
2424
|
+
].filter((part) => Boolean(part)).join(" ");
|
|
2425
|
+
}
|
|
2426
|
+
try {
|
|
2427
|
+
return JSON.stringify(record, (key, value) => SENSITIVE_ERROR_FIELDS.has(key.toLowerCase()) ? "[REDACTED]" : value);
|
|
2428
|
+
} catch {
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return String(err);
|
|
2432
|
+
}
|
|
2433
|
+
__name(formatSdkError, "formatSdkError");
|
|
2434
|
+
function redactSensitiveErrorText(text) {
|
|
2435
|
+
return text.replace(/(["']?(?:secret|aeskey|token|password|authorization)["']?\s*[:=]\s*)(["']?)[^"',\s}]+(\2)/giu, "$1$2[REDACTED]$3");
|
|
2436
|
+
}
|
|
2437
|
+
__name(redactSensitiveErrorText, "redactSensitiveErrorText");
|
|
2438
|
+
function isPromiseLike(value) {
|
|
2439
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function";
|
|
2440
|
+
}
|
|
2441
|
+
__name(isPromiseLike, "isPromiseLike");
|
|
2442
|
+
function parseWeComConfig(name, config) {
|
|
2443
|
+
const botId = readRequiredString(config, "botId");
|
|
2444
|
+
const secret = readRequiredString(config, "secret");
|
|
2445
|
+
if (!botId || !secret) {
|
|
2446
|
+
throw new Error(`Channel "${name}" requires botId and secret for WeCom.`);
|
|
2447
|
+
}
|
|
2448
|
+
const wsUrl = readOptionalString(config, "wsUrl");
|
|
2449
|
+
if (wsUrl && !isSecureWebSocketUrl(wsUrl)) {
|
|
2450
|
+
throw new Error(`Channel "${name}" requires wsUrl to use wss://.`);
|
|
2451
|
+
}
|
|
2452
|
+
return wsUrl ? { botId, secret, wsUrl } : { botId, secret };
|
|
2453
|
+
}
|
|
2454
|
+
__name(parseWeComConfig, "parseWeComConfig");
|
|
2455
|
+
function readRequiredString(config, key) {
|
|
2456
|
+
const value = config[key];
|
|
2457
|
+
return typeof value === "string" ? value.trim() : "";
|
|
2458
|
+
}
|
|
2459
|
+
__name(readRequiredString, "readRequiredString");
|
|
2460
|
+
function readOptionalString(config, key) {
|
|
2461
|
+
const value = config[key];
|
|
2462
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2463
|
+
}
|
|
2464
|
+
__name(readOptionalString, "readOptionalString");
|
|
2465
|
+
function isSecureWebSocketUrl(value) {
|
|
2466
|
+
try {
|
|
2467
|
+
return new URL(value).protocol === "wss:";
|
|
2468
|
+
} catch {
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
__name(isSecureWebSocketUrl, "isSecureWebSocketUrl");
|
|
2473
|
+
function createWeComLogger(name) {
|
|
2474
|
+
const write = /* @__PURE__ */ __name((level, message) => {
|
|
2475
|
+
process.stderr.write(`[WeCom:${name}] SDK ${level}: ${sanitizeLogText(message, 200)}
|
|
2476
|
+
`);
|
|
2477
|
+
}, "write");
|
|
2478
|
+
return {
|
|
2479
|
+
debug: /* @__PURE__ */ __name(() => {
|
|
2480
|
+
}, "debug"),
|
|
2481
|
+
info: /* @__PURE__ */ __name(() => {
|
|
2482
|
+
}, "info"),
|
|
2483
|
+
warn: /* @__PURE__ */ __name((message) => write("warn", message), "warn"),
|
|
2484
|
+
error: /* @__PURE__ */ __name((message) => write("error", message), "error")
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
__name(createWeComLogger, "createWeComLogger");
|
|
2488
|
+
function extractBody(payload) {
|
|
2489
|
+
const raw = asRecord(payload);
|
|
2490
|
+
if (!raw)
|
|
2491
|
+
return void 0;
|
|
2492
|
+
return getRecord(raw, "body") ?? raw;
|
|
2493
|
+
}
|
|
2494
|
+
__name(extractBody, "extractBody");
|
|
2495
|
+
function getLogMessageId(payload) {
|
|
2496
|
+
const body = extractBody(payload);
|
|
2497
|
+
if (!body)
|
|
2498
|
+
return "(unknown id)";
|
|
2499
|
+
return sanitizeLogText(getString(body, "msgid") || "(no id)", 100);
|
|
2500
|
+
}
|
|
2501
|
+
__name(getLogMessageId, "getLogMessageId");
|
|
2502
|
+
function extractText(body) {
|
|
2503
|
+
const msgType = getString(body, "msgtype");
|
|
2504
|
+
if (msgType === "mixed") {
|
|
2505
|
+
const mixed = getRecord(body, "mixed");
|
|
2506
|
+
const items = getArray(mixed, "msg_item");
|
|
2507
|
+
return items.map((item) => {
|
|
2508
|
+
const record = asRecord(item);
|
|
2509
|
+
if (!record)
|
|
2510
|
+
return "";
|
|
2511
|
+
const itemType = getString(record, "msgtype");
|
|
2512
|
+
if (itemType === "text") {
|
|
2513
|
+
return getString(getRecord(record, "text"), "content");
|
|
2514
|
+
}
|
|
2515
|
+
if (itemType === "voice") {
|
|
2516
|
+
return getString(getRecord(record, "voice"), "content");
|
|
2517
|
+
}
|
|
2518
|
+
return "";
|
|
2519
|
+
}).filter(Boolean).join("\n").trim();
|
|
2520
|
+
}
|
|
2521
|
+
const text = getString(getRecord(body, "text"), "content");
|
|
2522
|
+
const voiceText = getString(getRecord(body, "voice"), "content");
|
|
2523
|
+
if (text)
|
|
2524
|
+
return text;
|
|
2525
|
+
if (voiceText)
|
|
2526
|
+
return voiceText;
|
|
2527
|
+
if (msgType === "image")
|
|
2528
|
+
return "(image)";
|
|
2529
|
+
if (msgType === "voice")
|
|
2530
|
+
return "(voice)";
|
|
2531
|
+
if (msgType === "video")
|
|
2532
|
+
return "(video)";
|
|
2533
|
+
if (msgType === "file") {
|
|
2534
|
+
const name = sanitizeFileName(getString(getRecord(body, "file"), "filename"));
|
|
2535
|
+
return `(file: ${name || "file"})`;
|
|
2536
|
+
}
|
|
2537
|
+
return "";
|
|
2538
|
+
}
|
|
2539
|
+
__name(extractText, "extractText");
|
|
2540
|
+
function extractQuoteText(quote) {
|
|
2541
|
+
if (!quote)
|
|
2542
|
+
return void 0;
|
|
2543
|
+
return extractText(quote) || void 0;
|
|
2544
|
+
}
|
|
2545
|
+
__name(extractQuoteText, "extractQuoteText");
|
|
2546
|
+
function collectInboundMediaRefs(body, depth = 0, seenUrls = /* @__PURE__ */ new Set()) {
|
|
2547
|
+
if (depth > 3)
|
|
2548
|
+
return [];
|
|
2549
|
+
const refs = [];
|
|
2550
|
+
const add = /* @__PURE__ */ __name((type, source) => {
|
|
2551
|
+
const url = getString(source, "url");
|
|
2552
|
+
if (!url || seenUrls.has(url))
|
|
2553
|
+
return;
|
|
2554
|
+
seenUrls.add(url);
|
|
2555
|
+
refs.push({
|
|
2556
|
+
type,
|
|
2557
|
+
url,
|
|
2558
|
+
aesKey: getString(source, "aeskey") || void 0,
|
|
2559
|
+
fileName: getString(source, "filename") || getString(source, "file_name") || void 0
|
|
2560
|
+
});
|
|
2561
|
+
}, "add");
|
|
2562
|
+
const mixed = getRecord(body, "mixed");
|
|
2563
|
+
for (const item of getArray(mixed, "msg_item")) {
|
|
2564
|
+
const record = asRecord(item);
|
|
2565
|
+
if (!record)
|
|
2566
|
+
continue;
|
|
2567
|
+
const itemType = getString(record, "msgtype");
|
|
2568
|
+
if (isWeComMediaType(itemType)) {
|
|
2569
|
+
add(itemType, getRecord(record, itemType) ?? {});
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
add("image", getRecord(body, "image") ?? {});
|
|
2573
|
+
add("file", getRecord(body, "file") ?? {});
|
|
2574
|
+
add("video", getRecord(body, "video") ?? {});
|
|
2575
|
+
add("voice", getRecord(body, "voice") ?? {});
|
|
2576
|
+
const quote = getRecord(body, "quote");
|
|
2577
|
+
if (quote)
|
|
2578
|
+
refs.push(...collectInboundMediaRefs(quote, depth + 1, seenUrls));
|
|
2579
|
+
return refs;
|
|
2580
|
+
}
|
|
2581
|
+
__name(collectInboundMediaRefs, "collectInboundMediaRefs");
|
|
2582
|
+
function parseOutboundMediaMarkers(text) {
|
|
2583
|
+
const codeRanges = findCodeRanges(text);
|
|
2584
|
+
const markerRe = /\[(IMAGE):\s*([^\]]+)\]/gi;
|
|
2585
|
+
const media = [];
|
|
2586
|
+
const rangesToRemove = [];
|
|
2587
|
+
for (const match of text.matchAll(markerRe)) {
|
|
2588
|
+
const start = match.index ?? 0;
|
|
2589
|
+
const end = start + match[0].length;
|
|
2590
|
+
if (codeRanges.some(([from, to]) => start >= from && start < to))
|
|
2591
|
+
continue;
|
|
2592
|
+
const path = match[2]?.trim();
|
|
2593
|
+
const rawType = match[1]?.toLowerCase();
|
|
2594
|
+
if (!path || !isWeComMediaType(rawType))
|
|
2595
|
+
continue;
|
|
2596
|
+
media.push({ type: rawType, path });
|
|
2597
|
+
rangesToRemove.push([start, end]);
|
|
2598
|
+
}
|
|
2599
|
+
let cleanedText = text;
|
|
2600
|
+
for (const [start, end] of rangesToRemove.toReversed()) {
|
|
2601
|
+
cleanedText = `${cleanedText.slice(0, start)}${cleanedText.slice(end)}`;
|
|
2602
|
+
}
|
|
2603
|
+
return {
|
|
2604
|
+
cleanedText: cleanedText.replace(/\n{3,}/g, "\n\n").trim(),
|
|
2605
|
+
media
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
__name(parseOutboundMediaMarkers, "parseOutboundMediaMarkers");
|
|
2609
|
+
function findCodeRanges(text) {
|
|
2610
|
+
const ranges = [];
|
|
2611
|
+
let fenceStart;
|
|
2612
|
+
let fenceToken;
|
|
2613
|
+
for (const match of text.matchAll(/```|~~~/g)) {
|
|
2614
|
+
const start = match.index ?? 0;
|
|
2615
|
+
const token = match[0];
|
|
2616
|
+
if (fenceStart === void 0) {
|
|
2617
|
+
fenceStart = start;
|
|
2618
|
+
fenceToken = token;
|
|
2619
|
+
} else if (token === fenceToken) {
|
|
2620
|
+
ranges.push([fenceStart, start + 3]);
|
|
2621
|
+
fenceStart = void 0;
|
|
2622
|
+
fenceToken = void 0;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
if (fenceStart !== void 0) {
|
|
2626
|
+
ranges.push([fenceStart, text.length]);
|
|
2627
|
+
}
|
|
2628
|
+
for (const match of text.matchAll(/(`+)[^`\n]*\1/g)) {
|
|
2629
|
+
const start = match.index ?? 0;
|
|
2630
|
+
if (ranges.some(([from, to]) => start >= from && start < to))
|
|
2631
|
+
continue;
|
|
2632
|
+
ranges.push([start, start + match[0].length]);
|
|
2633
|
+
}
|
|
2634
|
+
const lineRe = /^(?: {4,}|\t).*$/gm;
|
|
2635
|
+
for (const match of text.matchAll(lineRe)) {
|
|
2636
|
+
const start = match.index ?? 0;
|
|
2637
|
+
if (ranges.some(([from, to]) => start >= from && start < to))
|
|
2638
|
+
continue;
|
|
2639
|
+
ranges.push([start, start + match[0].length]);
|
|
2640
|
+
}
|
|
2641
|
+
return ranges;
|
|
2642
|
+
}
|
|
2643
|
+
__name(findCodeRanges, "findCodeRanges");
|
|
2644
|
+
function isWeComMediaType(value) {
|
|
2645
|
+
return value === "image" || value === "file" || value === "voice" || value === "video";
|
|
2646
|
+
}
|
|
2647
|
+
__name(isWeComMediaType, "isWeComMediaType");
|
|
2648
|
+
function splitMarkdownChunks(text) {
|
|
2649
|
+
if (!text)
|
|
2650
|
+
return [];
|
|
2651
|
+
const chunks = [];
|
|
2652
|
+
let current = "";
|
|
2653
|
+
let codeFence;
|
|
2654
|
+
const fits = /* @__PURE__ */ __name((value, nextCodeFence = codeFence) => Buffer2.byteLength(nextCodeFence ? `${value}
|
|
2655
|
+
${nextCodeFence}` : value, "utf8") <= MARKDOWN_CHUNK_BYTES, "fits");
|
|
2656
|
+
const flush = /* @__PURE__ */ __name((closeCode = true) => {
|
|
2657
|
+
if (!current)
|
|
2658
|
+
return;
|
|
2659
|
+
chunks.push(closeCode && codeFence ? `${current}
|
|
2660
|
+
${codeFence}` : current);
|
|
2661
|
+
current = closeCode && codeFence ? codeFence : "";
|
|
2662
|
+
}, "flush");
|
|
2663
|
+
for (const line of text.split("\n")) {
|
|
2664
|
+
const candidate = current ? `${current}
|
|
2665
|
+
${line}` : line;
|
|
2666
|
+
const candidateCodeFence = toggleCodeFenceState(line, codeFence);
|
|
2667
|
+
if (fits(candidate, candidateCodeFence)) {
|
|
2668
|
+
current = candidate;
|
|
2669
|
+
codeFence = candidateCodeFence;
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
flush();
|
|
2673
|
+
const retried = current ? `${current}
|
|
2674
|
+
${line}` : line;
|
|
2675
|
+
const retriedCodeFence = toggleCodeFenceState(line, codeFence);
|
|
2676
|
+
if (fits(retried, retriedCodeFence)) {
|
|
2677
|
+
current = retried;
|
|
2678
|
+
codeFence = retriedCodeFence;
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
let needsLineBreak = Boolean(current);
|
|
2682
|
+
for (let index = 0; index < line.length; ) {
|
|
2683
|
+
const codePoint = line.codePointAt(index);
|
|
2684
|
+
const token = line.startsWith("```", index) ? "```" : line.startsWith("~~~", index) ? "~~~" : codePoint === void 0 ? "" : String.fromCodePoint(codePoint);
|
|
2685
|
+
if (!token)
|
|
2686
|
+
break;
|
|
2687
|
+
const nextCodeFence = token === codeFence ? void 0 : !codeFence && isFenceToken(token) ? token : codeFence;
|
|
2688
|
+
const addition = needsLineBreak && current ? `
|
|
2689
|
+
${token}` : token;
|
|
2690
|
+
const candidate2 = `${current}${addition}`;
|
|
2691
|
+
if (!fits(candidate2, nextCodeFence)) {
|
|
2692
|
+
flush();
|
|
2693
|
+
current = current ? `${current}
|
|
2694
|
+
${token}` : token;
|
|
2695
|
+
} else {
|
|
2696
|
+
current = candidate2;
|
|
2697
|
+
}
|
|
2698
|
+
codeFence = nextCodeFence;
|
|
2699
|
+
needsLineBreak = false;
|
|
2700
|
+
index += token.length;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
flush();
|
|
2704
|
+
return chunks;
|
|
2705
|
+
}
|
|
2706
|
+
__name(splitMarkdownChunks, "splitMarkdownChunks");
|
|
2707
|
+
function toggleCodeFenceState(line, codeFence) {
|
|
2708
|
+
let nextCodeFence = codeFence;
|
|
2709
|
+
for (const match of line.matchAll(/```|~~~/g)) {
|
|
2710
|
+
const token = match[0];
|
|
2711
|
+
if (nextCodeFence === token) {
|
|
2712
|
+
nextCodeFence = void 0;
|
|
2713
|
+
} else if (!nextCodeFence) {
|
|
2714
|
+
nextCodeFence = token;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return nextCodeFence;
|
|
2718
|
+
}
|
|
2719
|
+
__name(toggleCodeFenceState, "toggleCodeFenceState");
|
|
2720
|
+
function isFenceToken(token) {
|
|
2721
|
+
return token === "```" || token === "~~~";
|
|
2722
|
+
}
|
|
2723
|
+
__name(isFenceToken, "isFenceToken");
|
|
2724
|
+
async function readOutboundMedia(rawPath, cwd) {
|
|
2725
|
+
const resolved = resolve(cwd, rawPath);
|
|
2726
|
+
const real = realpathSync(resolved);
|
|
2727
|
+
const allowedDirs = [
|
|
2728
|
+
ensureDirectoryRealpath(join(tmpdir(), "channel-files"))
|
|
2729
|
+
];
|
|
2730
|
+
if (!allowedDirs.some((dir) => isInsideDir(real, dir))) {
|
|
2731
|
+
throw new Error("Media path outside allowed outbound directory");
|
|
2732
|
+
}
|
|
2733
|
+
const file = await open(real, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
2734
|
+
try {
|
|
2735
|
+
const stat = await file.stat();
|
|
2736
|
+
if (!stat.isFile())
|
|
2737
|
+
throw new Error(`Not a regular file: ${basename(rawPath)}`);
|
|
2738
|
+
if (stat.size > MAX_MEDIA_BYTES) {
|
|
2739
|
+
throw new Error(`Media file too large: ${stat.size} bytes`);
|
|
2740
|
+
}
|
|
2741
|
+
return { data: await file.readFile(), fileName: basename(real) };
|
|
2742
|
+
} finally {
|
|
2743
|
+
await file.close();
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
__name(readOutboundMedia, "readOutboundMedia");
|
|
2747
|
+
function ensureDirectoryRealpath(path) {
|
|
2748
|
+
try {
|
|
2749
|
+
mkdirSync(path, { recursive: true });
|
|
2750
|
+
const stat = lstatSync(path);
|
|
2751
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
2752
|
+
throw new Error("not a safe directory");
|
|
2753
|
+
}
|
|
2754
|
+
return realpathSync(path);
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2757
|
+
throw new Error(`Cannot prepare outbound media directory ${path}: ${reason}`);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
__name(ensureDirectoryRealpath, "ensureDirectoryRealpath");
|
|
2761
|
+
function isInsideDir(filePath, dir) {
|
|
2762
|
+
const windowsStyle = /^[a-zA-Z]:[\\/]/.test(filePath);
|
|
2763
|
+
const pathImpl = windowsStyle ? win32 : posix;
|
|
2764
|
+
const relative = pathImpl.relative(dir, filePath);
|
|
2765
|
+
return relative === "" || !relative.startsWith("..") && !pathImpl.isAbsolute(relative);
|
|
2766
|
+
}
|
|
2767
|
+
__name(isInsideDir, "isInsideDir");
|
|
2768
|
+
async function isSafeInboundMediaUrl(rawUrl) {
|
|
2769
|
+
let url;
|
|
2770
|
+
try {
|
|
2771
|
+
url = new URL(rawUrl);
|
|
2772
|
+
} catch {
|
|
2773
|
+
return { safe: false, reason: "invalid URL" };
|
|
2774
|
+
}
|
|
2775
|
+
if (url.protocol !== "https:") {
|
|
2776
|
+
return { safe: false, reason: "non-HTTPS protocol" };
|
|
2777
|
+
}
|
|
2778
|
+
if (url.username || url.password) {
|
|
2779
|
+
return { safe: false, reason: "URL contains embedded credentials" };
|
|
2780
|
+
}
|
|
2781
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
2782
|
+
if (!host || host === "localhost" || host.endsWith(".localhost")) {
|
|
2783
|
+
return { safe: false, reason: "local hostname" };
|
|
2784
|
+
}
|
|
2785
|
+
if (host.endsWith(".local")) {
|
|
2786
|
+
return { safe: false, reason: "local hostname" };
|
|
2787
|
+
}
|
|
2788
|
+
if (isIP(host)) {
|
|
2789
|
+
return isPublicIpAddress(host) ? { safe: true } : { safe: false, reason: `private address ${host}` };
|
|
2790
|
+
}
|
|
2791
|
+
if (!host.includes("."))
|
|
2792
|
+
return { safe: false, reason: "bare hostname" };
|
|
2793
|
+
try {
|
|
2794
|
+
const records = await lookup(host, { all: true });
|
|
2795
|
+
if (records.length === 0) {
|
|
2796
|
+
return { safe: false, reason: "no DNS records" };
|
|
2797
|
+
}
|
|
2798
|
+
const privateRecord = records.find((record) => !isPublicIpAddress(record.address));
|
|
2799
|
+
return privateRecord ? {
|
|
2800
|
+
safe: false,
|
|
2801
|
+
reason: `${host} resolved to private address ${privateRecord.address}`
|
|
2802
|
+
} : { safe: true };
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2805
|
+
return { safe: false, reason: `DNS lookup failed for ${host}: ${message}` };
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
__name(isSafeInboundMediaUrl, "isSafeInboundMediaUrl");
|
|
2809
|
+
async function downloadInboundMedia(ref) {
|
|
2810
|
+
const urlSafety = await isSafeInboundMediaUrl(ref.url);
|
|
2811
|
+
if (!urlSafety.safe) {
|
|
2812
|
+
throw new Error(`unsafe media URL (${urlSafety.reason})`);
|
|
2813
|
+
}
|
|
2814
|
+
const downloaded = await guardedHttpsDownload(ref.url);
|
|
2815
|
+
return {
|
|
2816
|
+
buffer: ref.aesKey ? (0, import_aibot_node_sdk.decryptFile)(downloaded.buffer, ref.aesKey) : downloaded.buffer,
|
|
2817
|
+
...ref.fileName || downloaded.filename ? { filename: ref.fileName || downloaded.filename } : {}
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
__name(downloadInboundMedia, "downloadInboundMedia");
|
|
2821
|
+
function guardedHttpsDownload(rawUrl) {
|
|
2822
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
2823
|
+
let settled = false;
|
|
2824
|
+
const cleanup = {};
|
|
2825
|
+
const finish = /* @__PURE__ */ __name((err, value) => {
|
|
2826
|
+
if (settled)
|
|
2827
|
+
return;
|
|
2828
|
+
settled = true;
|
|
2829
|
+
if (cleanup.absoluteTimeout)
|
|
2830
|
+
clearTimeout(cleanup.absoluteTimeout);
|
|
2831
|
+
if (err) {
|
|
2832
|
+
rejectPromise(err);
|
|
2833
|
+
} else {
|
|
2834
|
+
resolvePromise(value ?? { buffer: Buffer2.alloc(0) });
|
|
2835
|
+
}
|
|
2836
|
+
}, "finish");
|
|
2837
|
+
const req = httpsRequest(rawUrl, {
|
|
2838
|
+
method: "GET",
|
|
2839
|
+
lookup: safePublicLookup
|
|
2840
|
+
}, (res) => {
|
|
2841
|
+
const statusCode = res.statusCode ?? 0;
|
|
2842
|
+
if (statusCode >= 300 && statusCode < 400) {
|
|
2843
|
+
req.destroy();
|
|
2844
|
+
finish(new Error("redirected media URL"));
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
2848
|
+
req.destroy();
|
|
2849
|
+
finish(new Error(`media download failed: HTTP ${statusCode}`));
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const contentLength = getHeaderNumber(res.headers, "content-length");
|
|
2853
|
+
if (contentLength !== void 0 && contentLength > MAX_MEDIA_BYTES) {
|
|
2854
|
+
req.destroy();
|
|
2855
|
+
finish(new Error(`oversized attachment (${contentLength} bytes)`));
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const chunks = [];
|
|
2859
|
+
let total = 0;
|
|
2860
|
+
res.on("data", (chunk) => {
|
|
2861
|
+
const buffer = Buffer2.isBuffer(chunk) ? chunk : Buffer2.from(chunk);
|
|
2862
|
+
total += buffer.byteLength;
|
|
2863
|
+
if (total > MAX_MEDIA_BYTES) {
|
|
2864
|
+
req.destroy();
|
|
2865
|
+
finish(new Error("oversized attachment"));
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
chunks.push(buffer);
|
|
2869
|
+
});
|
|
2870
|
+
res.on("end", () => {
|
|
2871
|
+
const buffer = Buffer2.concat(chunks);
|
|
2872
|
+
if (buffer.byteLength === 0) {
|
|
2873
|
+
finish(new Error("empty media response"));
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
finish(void 0, {
|
|
2877
|
+
buffer,
|
|
2878
|
+
...parseContentDispositionFileName(res.headers)
|
|
2879
|
+
});
|
|
2880
|
+
});
|
|
2881
|
+
res.on("error", (err) => {
|
|
2882
|
+
req.destroy();
|
|
2883
|
+
finish(err);
|
|
2884
|
+
});
|
|
2885
|
+
});
|
|
2886
|
+
req.setTimeout(1e4, () => {
|
|
2887
|
+
req.destroy();
|
|
2888
|
+
finish(new Error("media download timed out"));
|
|
2889
|
+
});
|
|
2890
|
+
cleanup.absoluteTimeout = setTimeout(() => {
|
|
2891
|
+
req.destroy();
|
|
2892
|
+
finish(new Error("media download absolute timeout"));
|
|
2893
|
+
}, 6e4);
|
|
2894
|
+
cleanup.absoluteTimeout.unref?.();
|
|
2895
|
+
req.on("error", (err) => finish(err));
|
|
2896
|
+
req.end();
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
__name(guardedHttpsDownload, "guardedHttpsDownload");
|
|
2900
|
+
var safePublicLookup = /* @__PURE__ */ __name((hostname, options, callback) => {
|
|
2901
|
+
lookup(hostname, { all: true }).then((records) => {
|
|
2902
|
+
const unsafeRecord = records.find((record2) => !isPublicIpAddress(record2.address));
|
|
2903
|
+
if (records.length === 0 || unsafeRecord) {
|
|
2904
|
+
const reason = records.length === 0 ? `no DNS records for ${hostname}` : `${hostname} resolved to private address ${unsafeRecord.address}`;
|
|
2905
|
+
callback(new Error(`unsafe resolved media address: ${reason}`), "", 0);
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
if (options.all) {
|
|
2909
|
+
callback(null, records);
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
const record = records[0];
|
|
2913
|
+
callback(null, record.address, record.family);
|
|
2914
|
+
}).catch((err) => callback(err instanceof Error ? err : new Error(String(err)), "", 0));
|
|
2915
|
+
}, "safePublicLookup");
|
|
2916
|
+
function getHeaderNumber(headers, name) {
|
|
2917
|
+
const value = getHeaderValue(headers, name);
|
|
2918
|
+
if (value === void 0)
|
|
2919
|
+
return void 0;
|
|
2920
|
+
const size = Number(value);
|
|
2921
|
+
return Number.isFinite(size) && size >= 0 ? size : void 0;
|
|
2922
|
+
}
|
|
2923
|
+
__name(getHeaderNumber, "getHeaderNumber");
|
|
2924
|
+
function getHeaderValue(headers, name) {
|
|
2925
|
+
const value = headers[name.toLowerCase()];
|
|
2926
|
+
if (Array.isArray(value))
|
|
2927
|
+
return value[0];
|
|
2928
|
+
return value;
|
|
2929
|
+
}
|
|
2930
|
+
__name(getHeaderValue, "getHeaderValue");
|
|
2931
|
+
function parseContentDispositionFileName(headers) {
|
|
2932
|
+
const value = getHeaderValue(headers, "content-disposition");
|
|
2933
|
+
if (!value)
|
|
2934
|
+
return {};
|
|
2935
|
+
const encoded = value.match(/filename\*=UTF-8''([^;\s]+)/i)?.[1];
|
|
2936
|
+
if (encoded) {
|
|
2937
|
+
try {
|
|
2938
|
+
return { filename: decodeURIComponent(encoded) };
|
|
2939
|
+
} catch {
|
|
2940
|
+
return { filename: encoded };
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
const plain = value.match(/filename="?([^";]+)"?/i)?.[1];
|
|
2944
|
+
return plain ? { filename: plain } : {};
|
|
2945
|
+
}
|
|
2946
|
+
__name(parseContentDispositionFileName, "parseContentDispositionFileName");
|
|
2947
|
+
function isPublicIpAddress(address) {
|
|
2948
|
+
const host = address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
2949
|
+
const ipVersion = isIP(host);
|
|
2950
|
+
if (ipVersion === 4) {
|
|
2951
|
+
const parts = parseIpv4Parts(host);
|
|
2952
|
+
return parts ? isPublicIpv4(parts) : false;
|
|
2953
|
+
}
|
|
2954
|
+
if (ipVersion === 6) {
|
|
2955
|
+
const embedded = parseEmbeddedIpv4(host);
|
|
2956
|
+
if (embedded)
|
|
2957
|
+
return isPublicIpv4(embedded);
|
|
2958
|
+
const groups = expandIpv6Groups(host);
|
|
2959
|
+
if (!groups)
|
|
2960
|
+
return false;
|
|
2961
|
+
const first = groups[0] ?? 0;
|
|
2962
|
+
const isAllZeros = groups.every((group) => group === 0);
|
|
2963
|
+
const isLoopback = groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
|
|
2964
|
+
const lowZeroEmbeddedIpv4 = hexGroupsToIpv4(groups[6], groups[7]);
|
|
2965
|
+
const hasLowZeroPrivateIpv4 = groups.slice(0, 5).every((group) => group === 0) && lowZeroEmbeddedIpv4 !== void 0 && (groups[6] !== 0 || groups[7] !== 0) && !isPublicIpv4(lowZeroEmbeddedIpv4);
|
|
2966
|
+
return !(isAllZeros || isLoopback || hasLowZeroPrivateIpv4 || first >= 64512 && first <= 65023 || first >= 65280 && first <= 65535 || first === 256 && groups.slice(1, 4).every((g) => g === 0) || first === 8193 && groups[1] === 2 || first === 8193 && groups[1] === 3512 || first === 8193 && (groups[1] & 65520) === 16 || first === 8193 && (groups[1] & 65520) === 48 || isIpv6LinkLocalGroup(first));
|
|
2967
|
+
}
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
__name(isPublicIpAddress, "isPublicIpAddress");
|
|
2971
|
+
function isIpv6LinkLocalGroup(firstGroup) {
|
|
2972
|
+
return firstGroup >= 65152 && firstGroup <= 65279;
|
|
2973
|
+
}
|
|
2974
|
+
__name(isIpv6LinkLocalGroup, "isIpv6LinkLocalGroup");
|
|
2975
|
+
function parseIpv4Parts(host) {
|
|
2976
|
+
const parts = host.split(".");
|
|
2977
|
+
if (parts.length !== 4) {
|
|
2978
|
+
return void 0;
|
|
2979
|
+
}
|
|
2980
|
+
const nums = parts.map((part) => {
|
|
2981
|
+
if (!/^(0|[1-9]\d{0,2})$/u.test(part))
|
|
2982
|
+
return NaN;
|
|
2983
|
+
const n = Number(part);
|
|
2984
|
+
return n >= 0 && n <= 255 ? n : NaN;
|
|
2985
|
+
});
|
|
2986
|
+
if (nums.some((part) => Number.isNaN(part))) {
|
|
2987
|
+
return void 0;
|
|
2988
|
+
}
|
|
2989
|
+
return nums;
|
|
2990
|
+
}
|
|
2991
|
+
__name(parseIpv4Parts, "parseIpv4Parts");
|
|
2992
|
+
function parseMappedIpv4(host) {
|
|
2993
|
+
if (!host.startsWith("::ffff:"))
|
|
2994
|
+
return void 0;
|
|
2995
|
+
const suffix = host.slice("::ffff:".length);
|
|
2996
|
+
if (suffix.includes(".")) {
|
|
2997
|
+
return parseIpv4Parts(suffix);
|
|
2998
|
+
}
|
|
2999
|
+
const groups = suffix.split(":");
|
|
3000
|
+
if (groups.length !== 2)
|
|
3001
|
+
return void 0;
|
|
3002
|
+
const high = parseHexGroup(groups[0]);
|
|
3003
|
+
const low = parseHexGroup(groups[1]);
|
|
3004
|
+
if (high === void 0 || low === void 0)
|
|
3005
|
+
return void 0;
|
|
3006
|
+
return [high >> 8, high & 255, low >> 8, low & 255];
|
|
3007
|
+
}
|
|
3008
|
+
__name(parseMappedIpv4, "parseMappedIpv4");
|
|
3009
|
+
function parseEmbeddedIpv4(host) {
|
|
3010
|
+
const mapped = parseMappedIpv4(host);
|
|
3011
|
+
if (mapped)
|
|
3012
|
+
return mapped;
|
|
3013
|
+
const groups = expandIpv6Groups(host);
|
|
3014
|
+
if (!groups)
|
|
3015
|
+
return void 0;
|
|
3016
|
+
if (groups.slice(0, 4).every((group) => group === 0) && groups[4] === 65535 && groups[5] === 0 && (groups[6] !== 0 || groups[7] !== 0)) {
|
|
3017
|
+
return hexGroupsToIpv4(groups[6], groups[7]);
|
|
3018
|
+
}
|
|
3019
|
+
if (groups.slice(0, 5).every((group) => group === 0) && groups[5] === 65535 && (groups[6] !== 0 || groups[7] !== 0)) {
|
|
3020
|
+
return hexGroupsToIpv4(groups[6], groups[7]);
|
|
3021
|
+
}
|
|
3022
|
+
if (groups.slice(0, 6).every((group) => group === 0) && (groups[6] !== 0 || groups[7] !== 0)) {
|
|
3023
|
+
return hexGroupsToIpv4(groups[6], groups[7]);
|
|
3024
|
+
}
|
|
3025
|
+
if (groups[0] === 8194) {
|
|
3026
|
+
return hexGroupsToIpv4(groups[1], groups[2]);
|
|
3027
|
+
}
|
|
3028
|
+
if (groups[0] === 8193 && groups[1] === 0) {
|
|
3029
|
+
return hexGroupsToIpv4(groups[6] ^ 65535, groups[7] ^ 65535);
|
|
3030
|
+
}
|
|
3031
|
+
if (groups[0] === 100 && groups[1] === 65435) {
|
|
3032
|
+
return hexGroupsToIpv4(groups[6], groups[7]);
|
|
3033
|
+
}
|
|
3034
|
+
return void 0;
|
|
3035
|
+
}
|
|
3036
|
+
__name(parseEmbeddedIpv4, "parseEmbeddedIpv4");
|
|
3037
|
+
function expandIpv6Groups(host) {
|
|
3038
|
+
const normalized = normalizeIpv6DottedSuffix(host);
|
|
3039
|
+
if (!normalized)
|
|
3040
|
+
return void 0;
|
|
3041
|
+
const parts = normalized.split("::");
|
|
3042
|
+
if (parts.length > 2)
|
|
3043
|
+
return void 0;
|
|
3044
|
+
const left = parseIpv6GroupList(parts[0]);
|
|
3045
|
+
const right = parseIpv6GroupList(parts[1]);
|
|
3046
|
+
if (!left || !right)
|
|
3047
|
+
return void 0;
|
|
3048
|
+
if (parts.length === 1) {
|
|
3049
|
+
return left.length === 8 ? left : void 0;
|
|
3050
|
+
}
|
|
3051
|
+
const fill = 8 - left.length - right.length;
|
|
3052
|
+
if (fill < 1)
|
|
3053
|
+
return void 0;
|
|
3054
|
+
return [...left, ...Array(fill).fill(0), ...right];
|
|
3055
|
+
}
|
|
3056
|
+
__name(expandIpv6Groups, "expandIpv6Groups");
|
|
3057
|
+
function normalizeIpv6DottedSuffix(host) {
|
|
3058
|
+
if (!host.includes("."))
|
|
3059
|
+
return host;
|
|
3060
|
+
const lastColon = host.lastIndexOf(":");
|
|
3061
|
+
if (lastColon === -1)
|
|
3062
|
+
return void 0;
|
|
3063
|
+
const ipv4 = parseIpv4Parts(host.slice(lastColon + 1));
|
|
3064
|
+
if (!ipv4)
|
|
3065
|
+
return void 0;
|
|
3066
|
+
const high = ipv4[0] << 8 | ipv4[1];
|
|
3067
|
+
const low = ipv4[2] << 8 | ipv4[3];
|
|
3068
|
+
return `${host.slice(0, lastColon + 1)}${high.toString(16)}:${low.toString(16)}`;
|
|
3069
|
+
}
|
|
3070
|
+
__name(normalizeIpv6DottedSuffix, "normalizeIpv6DottedSuffix");
|
|
3071
|
+
function parseIpv6GroupList(value) {
|
|
3072
|
+
if (!value)
|
|
3073
|
+
return [];
|
|
3074
|
+
const groups = [];
|
|
3075
|
+
for (const group of value.split(":")) {
|
|
3076
|
+
const parsed = parseHexGroup(group);
|
|
3077
|
+
if (parsed === void 0)
|
|
3078
|
+
return void 0;
|
|
3079
|
+
groups.push(parsed);
|
|
3080
|
+
}
|
|
3081
|
+
return groups;
|
|
3082
|
+
}
|
|
3083
|
+
__name(parseIpv6GroupList, "parseIpv6GroupList");
|
|
3084
|
+
function hexGroupsToIpv4(high, low) {
|
|
3085
|
+
if (high === void 0 || low === void 0)
|
|
3086
|
+
return void 0;
|
|
3087
|
+
return [high >> 8, high & 255, low >> 8, low & 255];
|
|
3088
|
+
}
|
|
3089
|
+
__name(hexGroupsToIpv4, "hexGroupsToIpv4");
|
|
3090
|
+
function parseHexGroup(value) {
|
|
3091
|
+
if (!value || !/^[\da-f]{1,4}$/i.test(value))
|
|
3092
|
+
return void 0;
|
|
3093
|
+
const parsed = Number.parseInt(value, 16);
|
|
3094
|
+
return parsed >= 0 && parsed <= 65535 ? parsed : void 0;
|
|
3095
|
+
}
|
|
3096
|
+
__name(parseHexGroup, "parseHexGroup");
|
|
3097
|
+
function isPublicIpv4(parts) {
|
|
3098
|
+
const [a = 0, b = 0, c = 0] = parts;
|
|
3099
|
+
return !(a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 0 && (c === 0 || c === 2) || a === 192 && b === 88 && c === 99 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19 || b === 51 && c === 100) || a === 203 && b === 0 && c === 113 || a >= 224);
|
|
3100
|
+
}
|
|
3101
|
+
__name(isPublicIpv4, "isPublicIpv4");
|
|
3102
|
+
function extractMediaId(value) {
|
|
3103
|
+
const record = asRecord(value);
|
|
3104
|
+
return getString(record, "media_id") || getString(record, "mediaId") || getString(getRecord(record, "body"), "media_id") || void 0;
|
|
3105
|
+
}
|
|
3106
|
+
__name(extractMediaId, "extractMediaId");
|
|
3107
|
+
function detectImageMime(data) {
|
|
3108
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) {
|
|
3109
|
+
return "image/png";
|
|
3110
|
+
}
|
|
3111
|
+
if (data[0] === 71 && data[1] === 73 && data[2] === 70) {
|
|
3112
|
+
return "image/gif";
|
|
3113
|
+
}
|
|
3114
|
+
if (data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70 && data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) {
|
|
3115
|
+
return "image/webp";
|
|
3116
|
+
}
|
|
3117
|
+
if (data[0] === 255 && data[1] === 216 && data[2] === 255) {
|
|
3118
|
+
return "image/jpeg";
|
|
3119
|
+
}
|
|
3120
|
+
return "application/octet-stream";
|
|
3121
|
+
}
|
|
3122
|
+
__name(detectImageMime, "detectImageMime");
|
|
3123
|
+
function mediaTypeToMime(type) {
|
|
3124
|
+
switch (type) {
|
|
3125
|
+
case "video":
|
|
3126
|
+
return "video/mp4";
|
|
3127
|
+
case "voice":
|
|
3128
|
+
return "audio/amr";
|
|
3129
|
+
default:
|
|
3130
|
+
return "application/octet-stream";
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
__name(mediaTypeToMime, "mediaTypeToMime");
|
|
3134
|
+
function sanitizeFileName(name) {
|
|
3135
|
+
const base = basename(name || "").replace(/\0/g, "");
|
|
3136
|
+
return base.replace(/[^\p{L}\p{N}._-]/gu, "_").replace(/^\.+/, "_");
|
|
3137
|
+
}
|
|
3138
|
+
__name(sanitizeFileName, "sanitizeFileName");
|
|
3139
|
+
function asRecord(value) {
|
|
3140
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3141
|
+
}
|
|
3142
|
+
__name(asRecord, "asRecord");
|
|
3143
|
+
function getRecord(value, key) {
|
|
3144
|
+
return asRecord(value?.[key]);
|
|
3145
|
+
}
|
|
3146
|
+
__name(getRecord, "getRecord");
|
|
3147
|
+
function getArray(value, key) {
|
|
3148
|
+
const raw = value?.[key];
|
|
3149
|
+
return Array.isArray(raw) ? raw : [];
|
|
3150
|
+
}
|
|
3151
|
+
__name(getArray, "getArray");
|
|
3152
|
+
function getString(value, key) {
|
|
3153
|
+
const raw = value?.[key];
|
|
3154
|
+
return typeof raw === "string" ? raw : "";
|
|
3155
|
+
}
|
|
3156
|
+
__name(getString, "getString");
|
|
3157
|
+
function getBoolean(value, key) {
|
|
3158
|
+
const raw = value?.[key];
|
|
3159
|
+
return typeof raw === "boolean" ? raw : void 0;
|
|
3160
|
+
}
|
|
3161
|
+
__name(getBoolean, "getBoolean");
|
|
3162
|
+
function getExplicitMention(body, botId) {
|
|
3163
|
+
const botSpecificMention = getBoolean(body, "isInAtList") ?? getBoolean(body, "is_in_at_list");
|
|
3164
|
+
if (botSpecificMention !== void 0)
|
|
3165
|
+
return botSpecificMention;
|
|
3166
|
+
const mentions = collectMentionValues(body);
|
|
3167
|
+
if (!mentions.present)
|
|
3168
|
+
return getBoolean(body, "isMentioned");
|
|
3169
|
+
return mentions.values.some((mention) => mention === botId || mention === "@all" || mention === "all");
|
|
3170
|
+
}
|
|
3171
|
+
__name(getExplicitMention, "getExplicitMention");
|
|
3172
|
+
function collectMentionValues(body) {
|
|
3173
|
+
const values = [];
|
|
3174
|
+
let present = false;
|
|
3175
|
+
for (const key of [
|
|
3176
|
+
"mentions",
|
|
3177
|
+
"mentioned_list",
|
|
3178
|
+
"mentionedList",
|
|
3179
|
+
"at_list",
|
|
3180
|
+
"atList",
|
|
3181
|
+
"at_userids",
|
|
3182
|
+
"atUserIds"
|
|
3183
|
+
]) {
|
|
3184
|
+
if (Object.prototype.hasOwnProperty.call(body, key)) {
|
|
3185
|
+
present = true;
|
|
3186
|
+
}
|
|
3187
|
+
for (const item of getArray(body, key)) {
|
|
3188
|
+
if (typeof item === "string") {
|
|
3189
|
+
values.push(item);
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
const record = asRecord(item);
|
|
3193
|
+
if (!record)
|
|
3194
|
+
continue;
|
|
3195
|
+
for (const itemKey of ["userid", "userId", "id", "open_id", "openId"]) {
|
|
3196
|
+
const value = getString(record, itemKey);
|
|
3197
|
+
if (value)
|
|
3198
|
+
values.push(value);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
return { present, values };
|
|
3203
|
+
}
|
|
3204
|
+
__name(collectMentionValues, "collectMentionValues");
|
|
3205
|
+
|
|
3206
|
+
// packages/channels/wecom/dist/index.js
|
|
3207
|
+
var plugin = {
|
|
3208
|
+
channelType: "wecom",
|
|
3209
|
+
displayName: "WeCom",
|
|
3210
|
+
requiredConfigFields: ["botId", "secret"],
|
|
3211
|
+
envResolvableConfigFields: ["wsUrl"],
|
|
3212
|
+
createChannel: /* @__PURE__ */ __name((name, config, bridge, options) => new WeComChannel(name, config, bridge, options), "createChannel")
|
|
3213
|
+
};
|
|
3214
|
+
export {
|
|
3215
|
+
WeComChannel,
|
|
3216
|
+
plugin
|
|
3217
|
+
};
|