onbuzz 4.7.0 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/debug-balance-probe.mjs +35 -0
- package/src/__tests__/autoUpdateConfig.test.js +48 -12
- package/src/core/__tests__/messageProcessor.sourceHeader.test.js +7 -2
- package/src/index.js +16 -0
- package/src/interfaces/__tests__/accountRoutes.test.js +60 -2
- package/src/interfaces/accountRoutes.js +155 -7
- package/src/interfaces/webServer.js +41 -5
- package/src/services/__tests__/channelRelay.integration.test.js +49 -12
- package/src/services/__tests__/messageSource.test.js +6 -2
- package/src/services/__tests__/telegramBlockParser.test.js +183 -0
- package/src/services/__tests__/telegramService.test.js +838 -0
- package/src/services/messageSource.js +11 -11
- package/src/services/telegramBlockParser.js +145 -0
- package/src/services/telegramService.js +1273 -431
- package/web-ui/build/index.html +2 -2
- package/web-ui/build/static/1c-CuE_dLW8.js +1 -0
- package/web-ui/build/static/abap-LOu14tBm.js +1 -0
- package/web-ui/build/static/abnf-CJpfHQLV.js +1 -0
- package/web-ui/build/static/abnf-LdAi7OAY.js +1 -0
- package/web-ui/build/static/accesslog-BvrhdZIJ.js +1 -0
- package/web-ui/build/static/actionscript-B3NHp9II.js +1 -0
- package/web-ui/build/static/actionscript-DVfqv5DS.js +1 -0
- package/web-ui/build/static/ada-BVkvHgSu.js +1 -0
- package/web-ui/build/static/ada-d33rlRPC.js +1 -0
- package/web-ui/build/static/agda-t1mot-jT.js +1 -0
- package/web-ui/build/static/al-D2Aw8sYi.js +1 -0
- package/web-ui/build/static/angelscript-DGjq6ZLh.js +1 -0
- package/web-ui/build/static/antlr4-DQWExQhj.js +1 -0
- package/web-ui/build/static/apache-CRX2mGHA.js +1 -0
- package/web-ui/build/static/apacheconf-BBhhyVVR.js +1 -0
- package/web-ui/build/static/apex-DIJyAF3g.js +1 -0
- package/web-ui/build/static/apl-COGQUhmX.js +1 -0
- package/web-ui/build/static/applescript-DLPa_bvL.js +1 -0
- package/web-ui/build/static/applescript-iEepawXM.js +1 -0
- package/web-ui/build/static/aql-vDYotUbW.js +1 -0
- package/web-ui/build/static/arcade-5bpnh2Oi.js +1 -0
- package/web-ui/build/static/arduino-4OxI8HS8.js +1 -0
- package/web-ui/build/static/arduino-zJUNF7Xl.js +1 -0
- package/web-ui/build/static/arff-DHcdYO6-.js +1 -0
- package/web-ui/build/static/armasm-DVyTHmiD.js +1 -0
- package/web-ui/build/static/asciidoc-BRYIR6BF.js +1 -0
- package/web-ui/build/static/asciidoc-bd_scw1t.js +1 -0
- package/web-ui/build/static/asm6502-DfRcm1MU.js +1 -0
- package/web-ui/build/static/asmatmel--fvKJXxu.js +1 -0
- package/web-ui/build/static/aspectj-Cnm2Ccyg.js +1 -0
- package/web-ui/build/static/aspnet-BK2pQltF.js +1 -0
- package/web-ui/build/static/autohotkey-COkKlASn.js +1 -0
- package/web-ui/build/static/autohotkey-DNYipPv0.js +1 -0
- package/web-ui/build/static/autoit-DiwhqkEo.js +1 -0
- package/web-ui/build/static/autoit-DkQ_YDdk.js +1 -0
- package/web-ui/build/static/avisynth-DFrkQWrl.js +1 -0
- package/web-ui/build/static/avrasm-BbFtsKVm.js +1 -0
- package/web-ui/build/static/avro-idl-06_wFrxE.js +1 -0
- package/web-ui/build/static/awk-BNO1Hu3Y.js +1 -0
- package/web-ui/build/static/axapta-CW0rBEQf.js +1 -0
- package/web-ui/build/static/bash-DAP5H41u.js +1 -0
- package/web-ui/build/static/bash-DsCT4fHh.js +1 -0
- package/web-ui/build/static/basic-DqCcX17x.js +1 -0
- package/web-ui/build/static/basic-J3jqGQaz.js +1 -0
- package/web-ui/build/static/batch-BzKb0QuF.js +1 -0
- package/web-ui/build/static/bbcode-Cxys0qL9.js +1 -0
- package/web-ui/build/static/bicep-DPpVhf9I.js +1 -0
- package/web-ui/build/static/birb-BDF00uY2.js +1 -0
- package/web-ui/build/static/bison-DerSCeTc.js +1 -0
- package/web-ui/build/static/bnf--Nxb91Mj.js +1 -0
- package/web-ui/build/static/bnf-DKp1OqQh.js +1 -0
- package/web-ui/build/static/brainfuck-BgKburBi.js +1 -0
- package/web-ui/build/static/brainfuck-CLRabJhl.js +1 -0
- package/web-ui/build/static/brightscript-DJ0LrgUU.js +1 -0
- package/web-ui/build/static/bro-FVn3137a.js +1 -0
- package/web-ui/build/static/bsl-DGcfC5Qq.js +1 -0
- package/web-ui/build/static/c-CtBaJxj_.js +1 -0
- package/web-ui/build/static/c-DcJBPsOs.js +1 -0
- package/web-ui/build/static/c-like-BMAPMOT0.js +1 -0
- package/web-ui/build/static/cal-Dwvf5LQJ.js +1 -0
- package/web-ui/build/static/capnproto-Be5xeQu8.js +1 -0
- package/web-ui/build/static/ceylon-BV9kQegc.js +1 -0
- package/web-ui/build/static/cfscript-DqyqkqRV.js +1 -0
- package/web-ui/build/static/chaiscript-ab8S5sGz.js +1 -0
- package/web-ui/build/static/cil-DU5bEU16.js +1 -0
- package/web-ui/build/static/clean-CqKiC4l-.js +1 -0
- package/web-ui/build/static/clojure-CdaWManm.js +1 -0
- package/web-ui/build/static/clojure-cVIUydqy.js +1 -0
- package/web-ui/build/static/clojure-repl-mc7sqsOd.js +1 -0
- package/web-ui/build/static/cmake-D2ntzyPv.js +1 -0
- package/web-ui/build/static/cmake-Dt7gdCZl.js +1 -0
- package/web-ui/build/static/cobol-BYMIYxJY.js +1 -0
- package/web-ui/build/static/coffeescript-COUOYE5O.js +1 -0
- package/web-ui/build/static/coffeescript-FDyQzH4X.js +1 -0
- package/web-ui/build/static/concurnas-D8GglsJx.js +1 -0
- package/web-ui/build/static/coq-BvEL_B0N.js +1 -0
- package/web-ui/build/static/coq-C6NZeAuz.js +1 -0
- package/web-ui/build/static/cos-CjjeNVfD.js +1 -0
- package/web-ui/build/static/cpp-BdQQ9k01.js +1 -0
- package/web-ui/build/static/cpp-iYGopslr.js +1 -0
- package/web-ui/build/static/crmsh-COhOWlQB.js +1 -0
- package/web-ui/build/static/crystal-DqVfuZe7.js +1 -0
- package/web-ui/build/static/crystal-fkI7WHLA.js +1 -0
- package/web-ui/build/static/csharp-BKyMR69a.js +1 -0
- package/web-ui/build/static/csharp-KJwk5tFh.js +1 -0
- package/web-ui/build/static/cshtml-D-0Bd5yF.js +1 -0
- package/web-ui/build/static/csp-C6xrFQUZ.js +1 -0
- package/web-ui/build/static/csp-CKOZZjS5.js +1 -0
- package/web-ui/build/static/css-DhttYfIQ.js +1 -0
- package/web-ui/build/static/css-extras-Cwksdn14.js +1 -0
- package/web-ui/build/static/csv-3xOycrIm.js +1 -0
- package/web-ui/build/static/cypher-DIQ_IXvU.js +1 -0
- package/web-ui/build/static/d-CA3ONaPZ.js +1 -0
- package/web-ui/build/static/d-Cpj6yr1P.js +1 -0
- package/web-ui/build/static/dart-CStXueOh.js +1 -0
- package/web-ui/build/static/dart-DyyN2hCZ.js +1 -0
- package/web-ui/build/static/dataweave-C1Z1lf_p.js +1 -0
- package/web-ui/build/static/dax-Bra_xxin.js +1 -0
- package/web-ui/build/static/delphi-BSp32sTR.js +1 -0
- package/web-ui/build/static/dhall-DqULvWhD.js +1 -0
- package/web-ui/build/static/diff-DZWp0eai.js +1 -0
- package/web-ui/build/static/diff-Dl7Q9ZLJ.js +1 -0
- package/web-ui/build/static/django-BKb-9srn.js +1 -0
- package/web-ui/build/static/django-D4ZBsr7A.js +1 -0
- package/web-ui/build/static/dns-BIl1lqfE.js +1 -0
- package/web-ui/build/static/dns-zone-file-BHOpqtwv.js +1 -0
- package/web-ui/build/static/docker-DhYlo5KM.js +1 -0
- package/web-ui/build/static/dockerfile-DkKcxplU.js +1 -0
- package/web-ui/build/static/dos-BL8RAxlP.js +1 -0
- package/web-ui/build/static/dot-BLokvOTb.js +1 -0
- package/web-ui/build/static/dsconfig-C-HwSveF.js +1 -0
- package/web-ui/build/static/dts-D7N7kY9U.js +1 -0
- package/web-ui/build/static/dust-BDDbpTgO.js +1 -0
- package/web-ui/build/static/ebnf-BXq1reNT.js +1 -0
- package/web-ui/build/static/ebnf-DbPDw4Bq.js +1 -0
- package/web-ui/build/static/editorconfig-w2ZfPGuw.js +1 -0
- package/web-ui/build/static/eiffel-c2MQNBkW.js +1 -0
- package/web-ui/build/static/ejs-3QktTSvM.js +1 -0
- package/web-ui/build/static/elixir-BpckBbUo.js +1 -0
- package/web-ui/build/static/elixir-CdGx--Zx.js +1 -0
- package/web-ui/build/static/elm-BZQYORvs.js +1 -0
- package/web-ui/build/static/elm-CuB52FFf.js +1 -0
- package/web-ui/build/static/erb-BBShdpRp.js +1 -0
- package/web-ui/build/static/erb-C6oyD8uR.js +1 -0
- package/web-ui/build/static/erlang-CTR2NiVN.js +1 -0
- package/web-ui/build/static/erlang-Gyvyd8Bt.js +1 -0
- package/web-ui/build/static/erlang-repl-DQ9M2hTB.js +1 -0
- package/web-ui/build/static/etlua-WvCpEBL7.js +1 -0
- package/web-ui/build/static/excel-Cti8qyDh.js +1 -0
- package/web-ui/build/static/excel-formula-CbyjGhLT.js +1 -0
- package/web-ui/build/static/factor-DsS7xUlQ.js +1 -0
- package/web-ui/build/static/false-By7DT7NY.js +1 -0
- package/web-ui/build/static/firestore-security-rules-D818j5mX.js +1 -0
- package/web-ui/build/static/fix-Z9CNHO5B.js +1 -0
- package/web-ui/build/static/flix-CQ5tU7oG.js +1 -0
- package/web-ui/build/static/flow-TAcj5V8G.js +1 -0
- package/web-ui/build/static/fortran-CSo4-IB9.js +1 -0
- package/web-ui/build/static/fortran-D8ckpcuk.js +1 -0
- package/web-ui/build/static/fsharp-CX5IhzHG.js +1 -0
- package/web-ui/build/static/fsharp-CjgdWVzI.js +1 -0
- package/web-ui/build/static/ftl-BniZfpaP.js +1 -0
- package/web-ui/build/static/gams-Dx_Ej7b0.js +1 -0
- package/web-ui/build/static/gap-BW8rQPKU.js +1 -0
- package/web-ui/build/static/gauss-DP_j2AoM.js +1 -0
- package/web-ui/build/static/gcode-C_p0RHK2.js +1 -0
- package/web-ui/build/static/gcode-CvPzqlIr.js +1 -0
- package/web-ui/build/static/gdscript-BQxanI3X.js +1 -0
- package/web-ui/build/static/gedcom-DX22sB95.js +1 -0
- package/web-ui/build/static/gherkin-BuM2TN92.js +1 -0
- package/web-ui/build/static/gherkin-SyKtGAnl.js +1 -0
- package/web-ui/build/static/git-CZsBJbLj.js +1 -0
- package/web-ui/build/static/glsl-0gMMTGpT.js +1 -0
- package/web-ui/build/static/glsl-DdxyDP7O.js +1 -0
- package/web-ui/build/static/gml-Bmf5f_3a.js +1 -0
- package/web-ui/build/static/gml-DygwvUqd.js +1 -0
- package/web-ui/build/static/gn-CmJ9lVHR.js +1 -0
- package/web-ui/build/static/go-DcXIw3ql.js +1 -0
- package/web-ui/build/static/go-Dp-1fwju.js +1 -0
- package/web-ui/build/static/go-module-CVe5dTg-.js +1 -0
- package/web-ui/build/static/golo-Dqz3BdPW.js +1 -0
- package/web-ui/build/static/gradle-PY8Q_ptd.js +1 -0
- package/web-ui/build/static/graphql-7ynBfuuQ.js +1 -0
- package/web-ui/build/static/groovy-CKe3t1A3.js +1 -0
- package/web-ui/build/static/groovy-DK_6c_Dy.js +1 -0
- package/web-ui/build/static/haml-BjjIreuG.js +1 -0
- package/web-ui/build/static/haml-knbHtzU7.js +1 -0
- package/web-ui/build/static/handlebars-Bvicc2ZP.js +1 -0
- package/web-ui/build/static/handlebars-CWjisUes.js +1 -0
- package/web-ui/build/static/haskell-0vOFJmtj.js +1 -0
- package/web-ui/build/static/haskell-BjwWvVul.js +1 -0
- package/web-ui/build/static/haxe-CXIYhI9-.js +1 -0
- package/web-ui/build/static/haxe-Msa_nat5.js +1 -0
- package/web-ui/build/static/hcl-DADfqiqO.js +1 -0
- package/web-ui/build/static/hlsl-BoecwvZ1.js +1 -0
- package/web-ui/build/static/hoon-DoMcF4bN.js +1 -0
- package/web-ui/build/static/hpkp-DOkWWV9B.js +1 -0
- package/web-ui/build/static/hsp-KvanktXQ.js +1 -0
- package/web-ui/build/static/hsts-DP5sAe8p.js +1 -0
- package/web-ui/build/static/htmlbars-DpevdeqY.js +1 -0
- package/web-ui/build/static/http-CTyudxeK.js +1 -0
- package/web-ui/build/static/http-D0_pALtR.js +1 -0
- package/web-ui/build/static/hy-KpsNGTaA.js +1 -0
- package/web-ui/build/static/ichigojam-B9szb3eY.js +1 -0
- package/web-ui/build/static/icon-nqAgdt4t.js +1 -0
- package/web-ui/build/static/icu-message-format-DvgC-cKZ.js +1 -0
- package/web-ui/build/static/idris-D5VcDmFg.js +1 -0
- package/web-ui/build/static/iecst-LbbDhK7w.js +1 -0
- package/web-ui/build/static/ignore-rThoz4zA.js +1 -0
- package/web-ui/build/static/index-Cm1gK3R7.css +1 -0
- package/web-ui/build/static/index-D6blIL5t.js +13 -0
- package/web-ui/build/static/index-DqCutaVi.js +1164 -0
- package/web-ui/build/static/index-_UVy_UGY.js +1 -0
- package/web-ui/build/static/inform7-B2u0-vkK.js +1 -0
- package/web-ui/build/static/inform7-Cj2uMx5P.js +1 -0
- package/web-ui/build/static/ini-DDC_WfpB.js +1 -0
- package/web-ui/build/static/ini-Dafi1wkS.js +1 -0
- package/web-ui/build/static/io-Cs5RmCdQ.js +1 -0
- package/web-ui/build/static/irpf90-5KuyCx_k.js +1 -0
- package/web-ui/build/static/isbl-CZGAUFN3.js +1 -0
- package/web-ui/build/static/j-C2U5CNx3.js +1 -0
- package/web-ui/build/static/java-B-L3bHbG.js +1 -0
- package/web-ui/build/static/java-Dhfb499l.js +1 -0
- package/web-ui/build/static/javadoc-_ln2gn5I.js +1 -0
- package/web-ui/build/static/javadoclike-CnV_ddKg.js +1 -0
- package/web-ui/build/static/javascript-DBUUUlif.js +1 -0
- package/web-ui/build/static/javastacktrace-BS1l3Yf6.js +1 -0
- package/web-ui/build/static/jboss-cli-RdKEfNzL.js +1 -0
- package/web-ui/build/static/jexl-Bu5vdWnx.js +1 -0
- package/web-ui/build/static/jolie-LS18mxou.js +1 -0
- package/web-ui/build/static/jq-u9QEjWr_.js +1 -0
- package/web-ui/build/static/js-extras-C1cS_85G.js +1 -0
- package/web-ui/build/static/js-templates-DQxgBfhh.js +1 -0
- package/web-ui/build/static/jsdoc-DhLXAtzJ.js +1 -0
- package/web-ui/build/static/json-B8DgcmbV.js +1 -0
- package/web-ui/build/static/json-ByLJzbD8.js +1 -0
- package/web-ui/build/static/json5-DSnccLNQ.js +1 -0
- package/web-ui/build/static/jsonp-CcwF40Vu.js +1 -0
- package/web-ui/build/static/jsstacktrace-7YvddNWa.js +1 -0
- package/web-ui/build/static/jsx-DnHkAc3-.js +1 -0
- package/web-ui/build/static/julia-BmadVpf3.js +1 -0
- package/web-ui/build/static/julia-GSKCoIBJ.js +1 -0
- package/web-ui/build/static/julia-repl-ks3HTctE.js +1 -0
- package/web-ui/build/static/keepalived-CWDkM2-Z.js +1 -0
- package/web-ui/build/static/keyman-C9jA4-3Y.js +1 -0
- package/web-ui/build/static/kotlin-BqzUXT-K.js +1 -0
- package/web-ui/build/static/kotlin-BxhEUQAU.js +1 -0
- package/web-ui/build/static/kumir-BFYLWJq-.js +1 -0
- package/web-ui/build/static/kusto-CQRzYXm2.js +1 -0
- package/web-ui/build/static/lasso-D44z_uI8.js +1 -0
- package/web-ui/build/static/latex-BEJdTqYC.js +1 -0
- package/web-ui/build/static/latex-CStS4ql1.js +1 -0
- package/web-ui/build/static/latte-BPXasl8T.js +1 -0
- package/web-ui/build/static/ldif-Wxrk2-xQ.js +1 -0
- package/web-ui/build/static/leaf-DrTXK-pT.js +1 -0
- package/web-ui/build/static/less-BmoAe98U.js +1 -0
- package/web-ui/build/static/less-DzWEFsZj.js +1 -0
- package/web-ui/build/static/lilypond-C3ZnRm_L.js +1 -0
- package/web-ui/build/static/liquid-eXmypvP_.js +1 -0
- package/web-ui/build/static/lisp-DcidyoAl.js +1 -0
- package/web-ui/build/static/lisp-DhmUig93.js +1 -0
- package/web-ui/build/static/livecodeserver-D5lnfCkg.js +1 -0
- package/web-ui/build/static/livescript-Bpb_EpXS.js +1 -0
- package/web-ui/build/static/livescript-rF5whmjG.js +1 -0
- package/web-ui/build/static/llvm-BD9QG7T-.js +1 -0
- package/web-ui/build/static/llvm-CVP-jOx8.js +1 -0
- package/web-ui/build/static/log-DRzysjS2.js +1 -0
- package/web-ui/build/static/lolcode-BmKgPOwi.js +1 -0
- package/web-ui/build/static/lsl-DEO2mV0F.js +1 -0
- package/web-ui/build/static/lua-BNDDIa9F.js +1 -0
- package/web-ui/build/static/lua-C5n4hyV-.js +1 -0
- package/web-ui/build/static/magma-UZvvdXue.js +1 -0
- package/web-ui/build/static/makefile-BDQ4tARf.js +1 -0
- package/web-ui/build/static/makefile-Bjg-ueA2.js +1 -0
- package/web-ui/build/static/markdown-BTXvpQos.js +1 -0
- package/web-ui/build/static/markdown-BvMdFo1Y.js +1 -0
- package/web-ui/build/static/markup-templating-xSAHnnIS.js +1 -0
- package/web-ui/build/static/mathematica-CqNsUy6n.js +1 -0
- package/web-ui/build/static/matlab-D-A1WMHc.js +1 -0
- package/web-ui/build/static/matlab-Us_SxUWG.js +1 -0
- package/web-ui/build/static/maxima-BSyBp09v.js +1 -0
- package/web-ui/build/static/maxscript-wyb-3dgT.js +1 -0
- package/web-ui/build/static/mel-Bpo7JK_A.js +1 -0
- package/web-ui/build/static/mel-eZCDX4iS.js +1 -0
- package/web-ui/build/static/mercury-CEgBiaXI.js +1 -0
- package/web-ui/build/static/mermaid-BJIHmQsO.js +1 -0
- package/web-ui/build/static/mipsasm-c70dMcw9.js +1 -0
- package/web-ui/build/static/mizar-CeCpdHS-.js +1 -0
- package/web-ui/build/static/mizar-DDlOFZXr.js +1 -0
- package/web-ui/build/static/mojolicious-b4UGVld0.js +1 -0
- package/web-ui/build/static/mongodb-DwkU2uSm.js +1 -0
- package/web-ui/build/static/monkey-CNkGqOfo.js +1 -0
- package/web-ui/build/static/monkey-DwmdAwUx.js +1 -0
- package/web-ui/build/static/moonscript-C2vv4alq.js +1 -0
- package/web-ui/build/static/moonscript-GOmGojxm.js +1 -0
- package/web-ui/build/static/n1ql-B-uZXYG_.js +1 -0
- package/web-ui/build/static/n1ql-CqKgc7lF.js +1 -0
- package/web-ui/build/static/n4js-CLkxsn24.js +1 -0
- package/web-ui/build/static/nand2tetris-hdl-CAJAzliK.js +1 -0
- package/web-ui/build/static/naniscript-DxWjsrnl.js +1 -0
- package/web-ui/build/static/nasm-C-GI38un.js +1 -0
- package/web-ui/build/static/neon-DWKBFo--.js +1 -0
- package/web-ui/build/static/nevod-DGDg1w8a.js +1 -0
- package/web-ui/build/static/nginx-BQbAompS.js +1 -0
- package/web-ui/build/static/nginx-BQbgG8J2.js +1 -0
- package/web-ui/build/static/nim-Bf864jik.js +1 -0
- package/web-ui/build/static/nim-DG4Tf5sg.js +1 -0
- package/web-ui/build/static/nix-BBq54ypW.js +1 -0
- package/web-ui/build/static/nix-DiVQTSyM.js +1 -0
- package/web-ui/build/static/node-repl-Dg0rNxhv.js +1 -0
- package/web-ui/build/static/nsis-BG8Lm28v.js +1 -0
- package/web-ui/build/static/nsis-CwA_MNyF.js +1 -0
- package/web-ui/build/static/objectivec-83FM4mu0.js +1 -0
- package/web-ui/build/static/objectivec-CbDgPZGc.js +1 -0
- package/web-ui/build/static/ocaml-CXrksX9b.js +1 -0
- package/web-ui/build/static/ocaml-aktipfk_.js +1 -0
- package/web-ui/build/static/opencl-B1Wu4nP5.js +1 -0
- package/web-ui/build/static/openqasm-BnUjC0Rq.js +1 -0
- package/web-ui/build/static/openscad-CNEF75nQ.js +1 -0
- package/web-ui/build/static/oxygene-B5nQHVvu.js +1 -0
- package/web-ui/build/static/oz-Dw07HFRJ.js +1 -0
- package/web-ui/build/static/parigp-Dkh7PqN8.js +1 -0
- package/web-ui/build/static/parser-CAW1btwt.js +1 -0
- package/web-ui/build/static/parser3-DO4cmbnD.js +1 -0
- package/web-ui/build/static/pascal-B3XwEzrN.js +1 -0
- package/web-ui/build/static/pascaligo-CMKmAZ_B.js +1 -0
- package/web-ui/build/static/pcaxis-B5RrdB6p.js +1 -0
- package/web-ui/build/static/peoplecode-CGkgPJl9.js +1 -0
- package/web-ui/build/static/perl-BWBuZ5Vt.js +1 -0
- package/web-ui/build/static/perl-CW0_w5xG.js +1 -0
- package/web-ui/build/static/pf-B1aiG4iq.js +1 -0
- package/web-ui/build/static/pgsql-BgXadqYx.js +1 -0
- package/web-ui/build/static/php-BpB9heTH.js +1 -0
- package/web-ui/build/static/php-S3LJn70M.js +1 -0
- package/web-ui/build/static/php-extras-Dmblzt0I.js +1 -0
- package/web-ui/build/static/php-template-BS_uyUS1.js +1 -0
- package/web-ui/build/static/phpdoc-BqP_4LKt.js +1 -0
- package/web-ui/build/static/plaintext-BvDBRLjE.js +1 -0
- package/web-ui/build/static/plsql-DEj_qOIm.js +1 -0
- package/web-ui/build/static/pony-DpvrOLZf.js +1 -0
- package/web-ui/build/static/powerquery-C0Wobunx.js +1 -0
- package/web-ui/build/static/powershell-CY7ZBlTW.js +1 -0
- package/web-ui/build/static/powershell-DiEDDrOo.js +1 -0
- package/web-ui/build/static/processing-IXybgMWQ.js +1 -0
- package/web-ui/build/static/processing-i136WolF.js +1 -0
- package/web-ui/build/static/profile-guIih27Q.js +1 -0
- package/web-ui/build/static/prolog-CvmO0VDt.js +1 -0
- package/web-ui/build/static/prolog-Dmf4klwf.js +1 -0
- package/web-ui/build/static/promql-DObZhyF5.js +1 -0
- package/web-ui/build/static/properties-CK08qg7k.js +1 -0
- package/web-ui/build/static/properties-DmwePx7J.js +1 -0
- package/web-ui/build/static/protobuf-BDkUYcAx.js +1 -0
- package/web-ui/build/static/protobuf-CBMNyvhU.js +1 -0
- package/web-ui/build/static/psl-eW3BB0n6.js +1 -0
- package/web-ui/build/static/pug-D0LF-keh.js +1 -0
- package/web-ui/build/static/puppet-BUYZQfB-.js +1 -0
- package/web-ui/build/static/puppet-Dbo5UDT-.js +1 -0
- package/web-ui/build/static/pure-C5ukDHhg.js +1 -0
- package/web-ui/build/static/purebasic-B8eqrPTw.js +1 -0
- package/web-ui/build/static/purebasic-BA8g0-l-.js +1 -0
- package/web-ui/build/static/purescript-CKa_o5qx.js +1 -0
- package/web-ui/build/static/python-C-iEDsum.js +1 -0
- package/web-ui/build/static/python-CJcHyEGQ.js +1 -0
- package/web-ui/build/static/python-repl-WvDxhQ8t.js +1 -0
- package/web-ui/build/static/q-Ben5hioy.js +1 -0
- package/web-ui/build/static/q-CenUx0Za.js +1 -0
- package/web-ui/build/static/qml-CkUJB-7R.js +1 -0
- package/web-ui/build/static/qml-G6uEqbt_.js +1 -0
- package/web-ui/build/static/qore-CASrYvaT.js +1 -0
- package/web-ui/build/static/qsharp-CJMvScJV.js +1 -0
- package/web-ui/build/static/r-C3qOLTEa.js +1 -0
- package/web-ui/build/static/r-pFTIkWVX.js +1 -0
- package/web-ui/build/static/racket-Dj2Pnxz8.js +1 -0
- package/web-ui/build/static/reason-BLo81vJj.js +1 -0
- package/web-ui/build/static/reasonml-Uso3eiRd.js +1 -0
- package/web-ui/build/static/regex-CRRrWtDv.js +1 -0
- package/web-ui/build/static/rego-Pd9N41dQ.js +1 -0
- package/web-ui/build/static/renpy-B5nJRJ6W.js +1 -0
- package/web-ui/build/static/rest-C4KktBYV.js +1 -0
- package/web-ui/build/static/rib-WhwJFXjw.js +1 -0
- package/web-ui/build/static/rip-BRMu9y5Q.js +1 -0
- package/web-ui/build/static/roboconf-BC-HzyUK.js +1 -0
- package/web-ui/build/static/roboconf-Wo-TPzlW.js +1 -0
- package/web-ui/build/static/robotframework-Cvw8u8mL.js +1 -0
- package/web-ui/build/static/routeros-1EUCLy_j.js +1 -0
- package/web-ui/build/static/rsl-DcVNeKkF.js +1 -0
- package/web-ui/build/static/ruby-B1fqRC3j.js +1 -0
- package/web-ui/build/static/ruby-Bl1wXNzL.js +1 -0
- package/web-ui/build/static/ruleslanguage-C3rzl2Ma.js +1 -0
- package/web-ui/build/static/rust-4iyLrpFQ.js +1 -0
- package/web-ui/build/static/rust-V8MRv3os.js +1 -0
- package/web-ui/build/static/sas-ClkrCklD.js +1 -0
- package/web-ui/build/static/sas-CyFFy3mS.js +1 -0
- package/web-ui/build/static/sass-Cege3fu1.js +1 -0
- package/web-ui/build/static/scala-DNb7CDvN.js +1 -0
- package/web-ui/build/static/scala-c5k_CS9h.js +1 -0
- package/web-ui/build/static/scheme-BXrEBzXP.js +1 -0
- package/web-ui/build/static/scheme-COq0pstl.js +1 -0
- package/web-ui/build/static/scilab-CRMCfvft.js +1 -0
- package/web-ui/build/static/scss-C6MDSdKd.js +1 -0
- package/web-ui/build/static/scss-D1INSi-r.js +1 -0
- package/web-ui/build/static/shell-BHTFz9Xv.js +1 -0
- package/web-ui/build/static/shell-session-B6KXgRjz.js +1 -0
- package/web-ui/build/static/smali-CXGHC3zj.js +1 -0
- package/web-ui/build/static/smali-DFRkIqFc.js +1 -0
- package/web-ui/build/static/smalltalk-DWI2TbuJ.js +1 -0
- package/web-ui/build/static/smalltalk-jkgxBKRZ.js +1 -0
- package/web-ui/build/static/smarty-BPJMXwnj.js +1 -0
- package/web-ui/build/static/sml-BHT8CCeD.js +1 -0
- package/web-ui/build/static/sml-D5fkPJU1.js +1 -0
- package/web-ui/build/static/solidity-BfFvCWqp.js +1 -0
- package/web-ui/build/static/solution-file-DmSVfk_X.js +1 -0
- package/web-ui/build/static/soy-CXqMJVf8.js +1 -0
- package/web-ui/build/static/sparql-BxIpMyhJ.js +1 -0
- package/web-ui/build/static/splunk-spl-DMekyRuD.js +1 -0
- package/web-ui/build/static/sqf-DeQ-Lyaa.js +1 -0
- package/web-ui/build/static/sqf-hkl5BOBV.js +1 -0
- package/web-ui/build/static/sql-Di-rn3Od.js +1 -0
- package/web-ui/build/static/sql-DuF5aX9R.js +1 -0
- package/web-ui/build/static/sql_more-CCmzwYYA.js +1 -0
- package/web-ui/build/static/squirrel-fGLPlBkB.js +1 -0
- package/web-ui/build/static/stan-BveEpl1w.js +1 -0
- package/web-ui/build/static/stan-CM09XtOD.js +1 -0
- package/web-ui/build/static/stata-DeFUbv91.js +1 -0
- package/web-ui/build/static/step21-DnUE2ZTl.js +1 -0
- package/web-ui/build/static/stylus-Bz9XG6KK.js +1 -0
- package/web-ui/build/static/stylus-h33ZUR4b.js +1 -0
- package/web-ui/build/static/subunit-v4SRMSnP.js +1 -0
- package/web-ui/build/static/swift-BMCxsbHv.js +1 -0
- package/web-ui/build/static/swift-qFBaTSsZ.js +1 -0
- package/web-ui/build/static/systemd-9w7KWsst.js +1 -0
- package/web-ui/build/static/t4-cs-DdBrJkO9.js +1 -0
- package/web-ui/build/static/t4-templating-B-3iQJ4y.js +1 -0
- package/web-ui/build/static/t4-vb-BBuTbPK8.js +1 -0
- package/web-ui/build/static/taggerscript-BPvYYpaJ.js +1 -0
- package/web-ui/build/static/tap-DiFULuZV.js +1 -0
- package/web-ui/build/static/tap-Q9DMgrzh.js +1 -0
- package/web-ui/build/static/tcl-BZcwcRUU.js +1 -0
- package/web-ui/build/static/tcl-C4DHKxjE.js +1 -0
- package/web-ui/build/static/textile-ByLAjlo8.js +1 -0
- package/web-ui/build/static/thrift-C_isEMNl.js +1 -0
- package/web-ui/build/static/toml-C6ffwJpv.js +1 -0
- package/web-ui/build/static/tp-aV8nCrpp.js +1 -0
- package/web-ui/build/static/tremor-Bs1SSWo9.js +1 -0
- package/web-ui/build/static/tsx-DdryYBa4.js +1 -0
- package/web-ui/build/static/tt2-BeCMXvbZ.js +1 -0
- package/web-ui/build/static/turtle-B2FzT5Ov.js +1 -0
- package/web-ui/build/static/twig-fVEYqAxR.js +1 -0
- package/web-ui/build/static/twig-p9k58-hx.js +1 -0
- package/web-ui/build/static/typescript-8bhStN7p.js +1 -0
- package/web-ui/build/static/typescript-D2zmnuDR.js +1 -0
- package/web-ui/build/static/typoscript-DQePQcKL.js +1 -0
- package/web-ui/build/static/unrealscript-BZYSnJjB.js +1 -0
- package/web-ui/build/static/uorazor-CGaRtqR5.js +1 -0
- package/web-ui/build/static/uri-PJ3dz1qj.js +1 -0
- package/web-ui/build/static/v-CI9qDsYZ.js +1 -0
- package/web-ui/build/static/vala-B4ses542.js +1 -0
- package/web-ui/build/static/vala-BLxHpeKp.js +1 -0
- package/web-ui/build/static/vbnet-B7Dzk1rR.js +1 -0
- package/web-ui/build/static/vbnet-HOI9SMLZ.js +1 -0
- package/web-ui/build/static/vbscript-ERv5iDpi.js +1 -0
- package/web-ui/build/static/vbscript-html-D-mjpNAX.js +1 -0
- package/web-ui/build/static/velocity-B754qmeE.js +1 -0
- package/web-ui/build/static/verilog-3PA83Bxs.js +1 -0
- package/web-ui/build/static/verilog-D-lqP5ci.js +1 -0
- package/web-ui/build/static/vhdl-BjRYdIRF.js +1 -0
- package/web-ui/build/static/vhdl-DgrorvTt.js +1 -0
- package/web-ui/build/static/vim-3Oixnlee.js +1 -0
- package/web-ui/build/static/vim-BJTKcXdU.js +1 -0
- package/web-ui/build/static/visual-basic-BG8-3339.js +1 -0
- package/web-ui/build/static/warpscript-CEIgv5UG.js +1 -0
- package/web-ui/build/static/wasm-BJlpp9rs.js +1 -0
- package/web-ui/build/static/web-idl-Dlh9DutO.js +1 -0
- package/web-ui/build/static/wiki-CCcmRzkd.js +1 -0
- package/web-ui/build/static/wolfram--Xk-fAUi.js +1 -0
- package/web-ui/build/static/wren-B9Ajd9OC.js +1 -0
- package/web-ui/build/static/x86asm-DJJ72Zy-.js +1 -0
- package/web-ui/build/static/xeora-C0tNsSDj.js +1 -0
- package/web-ui/build/static/xl-CzEpyjRJ.js +1 -0
- package/web-ui/build/static/xml-CdtQTsBH.js +1 -0
- package/web-ui/build/static/xml-doc-TtPAAmc6.js +1 -0
- package/web-ui/build/static/xojo-B92fAnQE.js +1 -0
- package/web-ui/build/static/xquery-D0Z1FZVO.js +1 -0
- package/web-ui/build/static/xquery-DwlcRJ7Q.js +1 -0
- package/web-ui/build/static/yaml-DySZjPvD.js +1 -0
- package/web-ui/build/static/yaml-YgOVzvAS.js +1 -0
- package/web-ui/build/static/yang-BJokKPTo.js +1 -0
- package/web-ui/build/static/zephir-BWMUFIRD.js +1 -0
- package/web-ui/build/static/zig-Bpzj48cM.js +1 -0
- package/web-ui/build/static/1c-D_MLuenD.js +0 -1
- package/web-ui/build/static/abap-BNvFMksw.js +0 -1
- package/web-ui/build/static/abnf-WlnRJIp2.js +0 -1
- package/web-ui/build/static/abnf-mx1Wbtnp.js +0 -1
- package/web-ui/build/static/accesslog-CJumJuuV.js +0 -1
- package/web-ui/build/static/actionscript-B6CbK7n7.js +0 -1
- package/web-ui/build/static/actionscript-DZ5xfmPr.js +0 -1
- package/web-ui/build/static/ada-CCVAXM0c.js +0 -1
- package/web-ui/build/static/ada-DtGkgqC4.js +0 -1
- package/web-ui/build/static/agda-gMFRTChb.js +0 -1
- package/web-ui/build/static/al-88fYQh4r.js +0 -1
- package/web-ui/build/static/angelscript-Bibc8YWJ.js +0 -1
- package/web-ui/build/static/antlr4-DgibPJaJ.js +0 -1
- package/web-ui/build/static/apache-_pZ-s8l6.js +0 -1
- package/web-ui/build/static/apacheconf-YDldOH3Q.js +0 -1
- package/web-ui/build/static/apex-CIj23tyF.js +0 -1
- package/web-ui/build/static/apl-DtRSC0Xv.js +0 -1
- package/web-ui/build/static/applescript-CKhcPsla.js +0 -1
- package/web-ui/build/static/applescript-DuJeHJ1r.js +0 -1
- package/web-ui/build/static/aql-BgmI8Ors.js +0 -1
- package/web-ui/build/static/arcade-BX83978b.js +0 -1
- package/web-ui/build/static/arduino-BVdatcJb.js +0 -1
- package/web-ui/build/static/arduino-h6ifuWka.js +0 -1
- package/web-ui/build/static/arff-DHmbMeIA.js +0 -1
- package/web-ui/build/static/armasm-DiDgI5h5.js +0 -1
- package/web-ui/build/static/asciidoc-CBro7xXU.js +0 -1
- package/web-ui/build/static/asciidoc-DRC2X4bq.js +0 -1
- package/web-ui/build/static/asm6502-53VLu0lq.js +0 -1
- package/web-ui/build/static/asmatmel-DlRHdXes.js +0 -1
- package/web-ui/build/static/aspectj-DTSWMNl9.js +0 -1
- package/web-ui/build/static/aspnet-DEUO05ai.js +0 -1
- package/web-ui/build/static/autohotkey-3gt8xVo-.js +0 -1
- package/web-ui/build/static/autohotkey-BWcbMbe_.js +0 -1
- package/web-ui/build/static/autoit-CackGf4X.js +0 -1
- package/web-ui/build/static/autoit-Dyf0FSt2.js +0 -1
- package/web-ui/build/static/avisynth-DfCuxmn5.js +0 -1
- package/web-ui/build/static/avrasm-DdDiRWhx.js +0 -1
- package/web-ui/build/static/avro-idl-DppsvvwR.js +0 -1
- package/web-ui/build/static/awk-Cjk3U-Y4.js +0 -1
- package/web-ui/build/static/axapta-WwnY3TVf.js +0 -1
- package/web-ui/build/static/bash-C5Xkqken.js +0 -1
- package/web-ui/build/static/bash-ckPXa-RG.js +0 -1
- package/web-ui/build/static/basic-BCVHqHd2.js +0 -1
- package/web-ui/build/static/basic-D0dZbGrs.js +0 -1
- package/web-ui/build/static/batch-DUo60tbI.js +0 -1
- package/web-ui/build/static/bbcode-BVzBKrPV.js +0 -1
- package/web-ui/build/static/bicep-DkTjKL2N.js +0 -1
- package/web-ui/build/static/birb-C0rKI0h6.js +0 -1
- package/web-ui/build/static/bison-ChzwcDVc.js +0 -1
- package/web-ui/build/static/bnf-COhTgjpr.js +0 -1
- package/web-ui/build/static/bnf-DUYKVI2p.js +0 -1
- package/web-ui/build/static/brainfuck-BBJLbMxW.js +0 -1
- package/web-ui/build/static/brainfuck-DufOqE73.js +0 -1
- package/web-ui/build/static/brightscript-DXhkwfCc.js +0 -1
- package/web-ui/build/static/bro-BOFEsT-K.js +0 -1
- package/web-ui/build/static/bsl-ClV9GpNm.js +0 -1
- package/web-ui/build/static/c-Cl_3N1WB.js +0 -1
- package/web-ui/build/static/c-D0hFoAJ3.js +0 -1
- package/web-ui/build/static/c-like-D4BQ8VhL.js +0 -1
- package/web-ui/build/static/cal-CVIQ5DKG.js +0 -1
- package/web-ui/build/static/capnproto-l7oAqnCr.js +0 -1
- package/web-ui/build/static/ceylon-BEhCsFMw.js +0 -1
- package/web-ui/build/static/cfscript-DMh-xvVk.js +0 -1
- package/web-ui/build/static/chaiscript-DCvDoIGS.js +0 -1
- package/web-ui/build/static/cil-Ccn-j3TB.js +0 -1
- package/web-ui/build/static/clean-CjrEwcC-.js +0 -1
- package/web-ui/build/static/clojure-Dgo-8qFd.js +0 -1
- package/web-ui/build/static/clojure-M1AknfQi.js +0 -1
- package/web-ui/build/static/clojure-repl-BY1VLOSH.js +0 -1
- package/web-ui/build/static/cmake-CdHJ7Mp0.js +0 -1
- package/web-ui/build/static/cmake-DDGvi7kU.js +0 -1
- package/web-ui/build/static/cobol-BdrXYRxi.js +0 -1
- package/web-ui/build/static/coffeescript-BMDvMAy2.js +0 -1
- package/web-ui/build/static/coffeescript-CAYcMTRg.js +0 -1
- package/web-ui/build/static/concurnas-B8vcX0ec.js +0 -1
- package/web-ui/build/static/coq-CqvluN1S.js +0 -1
- package/web-ui/build/static/coq-vhnnRdWb.js +0 -1
- package/web-ui/build/static/cos-DY4p0y_u.js +0 -1
- package/web-ui/build/static/cpp-C3m_aEUX.js +0 -1
- package/web-ui/build/static/cpp-DRs9bbEm.js +0 -1
- package/web-ui/build/static/crmsh-CY6InunC.js +0 -1
- package/web-ui/build/static/crystal-Cwk1figA.js +0 -1
- package/web-ui/build/static/crystal-DeCJ5Pz7.js +0 -1
- package/web-ui/build/static/csharp-WmvubeKF.js +0 -1
- package/web-ui/build/static/csharp-zL1j7hXr.js +0 -1
- package/web-ui/build/static/cshtml-CbvGCaf_.js +0 -1
- package/web-ui/build/static/csp-BH2nxxKs.js +0 -1
- package/web-ui/build/static/csp-CHlC7KoB.js +0 -1
- package/web-ui/build/static/css-_X0ZO87y.js +0 -1
- package/web-ui/build/static/css-extras-wgFh56PY.js +0 -1
- package/web-ui/build/static/csv-momqe7r6.js +0 -1
- package/web-ui/build/static/cypher-hOeHFCfk.js +0 -1
- package/web-ui/build/static/d-B3d-tfyQ.js +0 -1
- package/web-ui/build/static/d-C6iyl9wX.js +0 -1
- package/web-ui/build/static/dart-BDKSE6LE.js +0 -1
- package/web-ui/build/static/dart-Bjchg8qI.js +0 -1
- package/web-ui/build/static/dataweave-ZgvdD-pH.js +0 -1
- package/web-ui/build/static/dax-yfGYNpUh.js +0 -1
- package/web-ui/build/static/delphi-CRLCdiTE.js +0 -1
- package/web-ui/build/static/dhall-yl1p1XUr.js +0 -1
- package/web-ui/build/static/diff-5SMtcFK7.js +0 -1
- package/web-ui/build/static/diff-y2RTBWsQ.js +0 -1
- package/web-ui/build/static/django-CtjG6w9L.js +0 -1
- package/web-ui/build/static/django-DNXOxI3v.js +0 -1
- package/web-ui/build/static/dns-D7Czwn_S.js +0 -1
- package/web-ui/build/static/dns-zone-file-D3yPxpjz.js +0 -1
- package/web-ui/build/static/docker-CWC5NqQQ.js +0 -1
- package/web-ui/build/static/dockerfile-CKFBVY9d.js +0 -1
- package/web-ui/build/static/dos-B7gYphQ-.js +0 -1
- package/web-ui/build/static/dot-BsisNMX4.js +0 -1
- package/web-ui/build/static/dsconfig-CEWwz7T6.js +0 -1
- package/web-ui/build/static/dts-UM5u5dMt.js +0 -1
- package/web-ui/build/static/dust-DlPmuh4d.js +0 -1
- package/web-ui/build/static/ebnf-BE6vq-fo.js +0 -1
- package/web-ui/build/static/ebnf-Czd8moGp.js +0 -1
- package/web-ui/build/static/editorconfig-Ctn83uRn.js +0 -1
- package/web-ui/build/static/eiffel-CZP01HVF.js +0 -1
- package/web-ui/build/static/ejs-BAXhrtLI.js +0 -1
- package/web-ui/build/static/elixir-C9PFjTwP.js +0 -1
- package/web-ui/build/static/elixir-DMjZPYCh.js +0 -1
- package/web-ui/build/static/elm-BH_KfOxJ.js +0 -1
- package/web-ui/build/static/elm-DRnNerE2.js +0 -1
- package/web-ui/build/static/erb-BhzUuTTy.js +0 -1
- package/web-ui/build/static/erb-DNbmX3N2.js +0 -1
- package/web-ui/build/static/erlang-BBPIw_V_.js +0 -1
- package/web-ui/build/static/erlang-BWdcSiKs.js +0 -1
- package/web-ui/build/static/erlang-repl-DQ9I6Dh_.js +0 -1
- package/web-ui/build/static/etlua-BeVNUecy.js +0 -1
- package/web-ui/build/static/excel-DYFvX5ms.js +0 -1
- package/web-ui/build/static/excel-formula-BtiqixBz.js +0 -1
- package/web-ui/build/static/factor-BCHyN6XZ.js +0 -1
- package/web-ui/build/static/false-UDWS0Dzk.js +0 -1
- package/web-ui/build/static/firestore-security-rules-B1-wJ42j.js +0 -1
- package/web-ui/build/static/fix-DrXU-V21.js +0 -1
- package/web-ui/build/static/flix-BjF0RPW9.js +0 -1
- package/web-ui/build/static/flow-DovuDACs.js +0 -1
- package/web-ui/build/static/fortran-DOqQP_9c.js +0 -1
- package/web-ui/build/static/fortran-DPASe5gH.js +0 -1
- package/web-ui/build/static/fsharp-Ckup0uPw.js +0 -1
- package/web-ui/build/static/fsharp-DyfRmGjn.js +0 -1
- package/web-ui/build/static/ftl-Coj24bDj.js +0 -1
- package/web-ui/build/static/gams-GwREZQh1.js +0 -1
- package/web-ui/build/static/gap-B7d6Bkmk.js +0 -1
- package/web-ui/build/static/gauss-DrLvPj5Z.js +0 -1
- package/web-ui/build/static/gcode-40iTJ2U1.js +0 -1
- package/web-ui/build/static/gcode-CISHXDj4.js +0 -1
- package/web-ui/build/static/gdscript-DmopCB0R.js +0 -1
- package/web-ui/build/static/gedcom-LiECv7gg.js +0 -1
- package/web-ui/build/static/gherkin-CYp_kcve.js +0 -1
- package/web-ui/build/static/gherkin-C_QgMjxf.js +0 -1
- package/web-ui/build/static/git-CfTT-600.js +0 -1
- package/web-ui/build/static/glsl-DRFMhvlG.js +0 -1
- package/web-ui/build/static/glsl-PNmpuSn0.js +0 -1
- package/web-ui/build/static/gml-3qmqfrt3.js +0 -1
- package/web-ui/build/static/gml-BTCmr4y5.js +0 -1
- package/web-ui/build/static/gn-BX0L2dQ-.js +0 -1
- package/web-ui/build/static/go-BXYOf5mS.js +0 -1
- package/web-ui/build/static/go-BnvPVHID.js +0 -1
- package/web-ui/build/static/go-module-BsPBvEmG.js +0 -1
- package/web-ui/build/static/golo-B6l7i4bX.js +0 -1
- package/web-ui/build/static/gradle-9DvvuDBJ.js +0 -1
- package/web-ui/build/static/graphql-OcCfd-Bc.js +0 -1
- package/web-ui/build/static/groovy-B33LZAt0.js +0 -1
- package/web-ui/build/static/groovy-Uyg0ruyE.js +0 -1
- package/web-ui/build/static/haml-D_FDAM6q.js +0 -1
- package/web-ui/build/static/haml-DjG31i1A.js +0 -1
- package/web-ui/build/static/handlebars-BKPDMksf.js +0 -1
- package/web-ui/build/static/handlebars-PP4M-qJ8.js +0 -1
- package/web-ui/build/static/haskell-HD9Gv3Wo.js +0 -1
- package/web-ui/build/static/haskell-nUf9ckz5.js +0 -1
- package/web-ui/build/static/haxe-BSsJNrQU.js +0 -1
- package/web-ui/build/static/haxe-D7yTwhdv.js +0 -1
- package/web-ui/build/static/hcl-_dqjptFs.js +0 -1
- package/web-ui/build/static/hlsl-DKixrsyy.js +0 -1
- package/web-ui/build/static/hoon-Di28eQfV.js +0 -1
- package/web-ui/build/static/hpkp-CXYG9zoQ.js +0 -1
- package/web-ui/build/static/hsp-lYl9KN6o.js +0 -1
- package/web-ui/build/static/hsts-CaYq9R8-.js +0 -1
- package/web-ui/build/static/htmlbars-eoY2PWS6.js +0 -1
- package/web-ui/build/static/http-2DXUmN2h.js +0 -1
- package/web-ui/build/static/http-Yjic3oWm.js +0 -1
- package/web-ui/build/static/hy-DVImg5uG.js +0 -1
- package/web-ui/build/static/ichigojam-Bdk-szlv.js +0 -1
- package/web-ui/build/static/icon-BEs9UbI_.js +0 -1
- package/web-ui/build/static/icu-message-format-DMfiLVs5.js +0 -1
- package/web-ui/build/static/idris-meksg0Ce.js +0 -1
- package/web-ui/build/static/iecst-DMPBiTmw.js +0 -1
- package/web-ui/build/static/ignore-BOQ9ZPvw.js +0 -1
- package/web-ui/build/static/index-BSEOP86D.js +0 -1
- package/web-ui/build/static/index-Be9WvMkI.js +0 -1162
- package/web-ui/build/static/index-Cr9q1fNq.css +0 -1
- package/web-ui/build/static/index-CuZ_erP9.js +0 -13
- package/web-ui/build/static/inform7-CNqu8FhU.js +0 -1
- package/web-ui/build/static/inform7-Cvfrdb0X.js +0 -1
- package/web-ui/build/static/ini-4DHUSW37.js +0 -1
- package/web-ui/build/static/ini-BgZ18Nk6.js +0 -1
- package/web-ui/build/static/io-c519ulw2.js +0 -1
- package/web-ui/build/static/irpf90-Kzq81vn5.js +0 -1
- package/web-ui/build/static/isbl-CeBN_o-W.js +0 -1
- package/web-ui/build/static/j-BmSuFx5E.js +0 -1
- package/web-ui/build/static/java-BezG0JQd.js +0 -1
- package/web-ui/build/static/java-BxBhTA1D.js +0 -1
- package/web-ui/build/static/javadoc-b96LRy9-.js +0 -1
- package/web-ui/build/static/javadoclike-BTNVp6pR.js +0 -1
- package/web-ui/build/static/javascript-B75gzQUI.js +0 -1
- package/web-ui/build/static/javastacktrace-D4xVOdoe.js +0 -1
- package/web-ui/build/static/jboss-cli-DH30pghK.js +0 -1
- package/web-ui/build/static/jexl-DF2RGmtX.js +0 -1
- package/web-ui/build/static/jolie-BlwO_Eyn.js +0 -1
- package/web-ui/build/static/jq-C7YhIw1G.js +0 -1
- package/web-ui/build/static/js-extras-JVZxoi4Q.js +0 -1
- package/web-ui/build/static/js-templates-D1CnsXgJ.js +0 -1
- package/web-ui/build/static/jsdoc-vf63NOxo.js +0 -1
- package/web-ui/build/static/json-CiKwLA5O.js +0 -1
- package/web-ui/build/static/json-pgLQeaes.js +0 -1
- package/web-ui/build/static/json5-_HMAYh1c.js +0 -1
- package/web-ui/build/static/jsonp-BeHQpb2r.js +0 -1
- package/web-ui/build/static/jsstacktrace-nw21JEtY.js +0 -1
- package/web-ui/build/static/jsx-Nxt_XhcW.js +0 -1
- package/web-ui/build/static/julia-BFczE4HB.js +0 -1
- package/web-ui/build/static/julia-DeDNID0K.js +0 -1
- package/web-ui/build/static/julia-repl-CoPTZMue.js +0 -1
- package/web-ui/build/static/keepalived-DP1sFl1b.js +0 -1
- package/web-ui/build/static/keyman-CCQVTHgo.js +0 -1
- package/web-ui/build/static/kotlin-CdZP58gi.js +0 -1
- package/web-ui/build/static/kotlin-CzNKVbXq.js +0 -1
- package/web-ui/build/static/kumir-lPok55sq.js +0 -1
- package/web-ui/build/static/kusto-oLek_zqj.js +0 -1
- package/web-ui/build/static/lasso-BFOYENIx.js +0 -1
- package/web-ui/build/static/latex-BH6YJXW6.js +0 -1
- package/web-ui/build/static/latex-IweVcDOS.js +0 -1
- package/web-ui/build/static/latte-Cmzytj93.js +0 -1
- package/web-ui/build/static/ldif-Bp4o5m10.js +0 -1
- package/web-ui/build/static/leaf-BFbMre84.js +0 -1
- package/web-ui/build/static/less-BVpV_iBx.js +0 -1
- package/web-ui/build/static/less-vdx-pGtM.js +0 -1
- package/web-ui/build/static/lilypond-Da6mCtwg.js +0 -1
- package/web-ui/build/static/liquid-RVC3EWUf.js +0 -1
- package/web-ui/build/static/lisp-Bgt-EmTk.js +0 -1
- package/web-ui/build/static/lisp-E8gUigGo.js +0 -1
- package/web-ui/build/static/livecodeserver-DDFQieJg.js +0 -1
- package/web-ui/build/static/livescript-C7FEDmfm.js +0 -1
- package/web-ui/build/static/livescript-Cc5SDmvx.js +0 -1
- package/web-ui/build/static/llvm-C22gmUEJ.js +0 -1
- package/web-ui/build/static/llvm-CjnVIiX4.js +0 -1
- package/web-ui/build/static/log-duIQICZV.js +0 -1
- package/web-ui/build/static/lolcode-Chrz9tTZ.js +0 -1
- package/web-ui/build/static/lsl-D1sdqECW.js +0 -1
- package/web-ui/build/static/lua-BFoJUb0S.js +0 -1
- package/web-ui/build/static/lua-D8RyRvqw.js +0 -1
- package/web-ui/build/static/magma-Cb3OhPkH.js +0 -1
- package/web-ui/build/static/makefile-BoVTcm8r.js +0 -1
- package/web-ui/build/static/makefile-LpYbnzZi.js +0 -1
- package/web-ui/build/static/markdown-CtQc_FZV.js +0 -1
- package/web-ui/build/static/markdown-DKz9qNdH.js +0 -1
- package/web-ui/build/static/markup-templating-D4EP19Lc.js +0 -1
- package/web-ui/build/static/mathematica-CIjlHVkd.js +0 -1
- package/web-ui/build/static/matlab-2b895p29.js +0 -1
- package/web-ui/build/static/matlab-BuluE4GS.js +0 -1
- package/web-ui/build/static/maxima-BGR8Puew.js +0 -1
- package/web-ui/build/static/maxscript-B429y77A.js +0 -1
- package/web-ui/build/static/mel-CYh_3LnF.js +0 -1
- package/web-ui/build/static/mel-CbYDscql.js +0 -1
- package/web-ui/build/static/mercury-DOee0RcU.js +0 -1
- package/web-ui/build/static/mermaid-Bgnlfred.js +0 -1
- package/web-ui/build/static/mipsasm-CB-uene8.js +0 -1
- package/web-ui/build/static/mizar-7qF42uX5.js +0 -1
- package/web-ui/build/static/mizar-Clogrskx.js +0 -1
- package/web-ui/build/static/mojolicious-BSIpTTKj.js +0 -1
- package/web-ui/build/static/mongodb--rWzrKDS.js +0 -1
- package/web-ui/build/static/monkey-C0lS2_fv.js +0 -1
- package/web-ui/build/static/monkey-DvqsFbfK.js +0 -1
- package/web-ui/build/static/moonscript-D6zFHk1D.js +0 -1
- package/web-ui/build/static/moonscript-JTxm8n_4.js +0 -1
- package/web-ui/build/static/n1ql-BwY55fjF.js +0 -1
- package/web-ui/build/static/n1ql-D8h3fDkJ.js +0 -1
- package/web-ui/build/static/n4js-5XIl8zQT.js +0 -1
- package/web-ui/build/static/nand2tetris-hdl-C4b_TExX.js +0 -1
- package/web-ui/build/static/naniscript-h2vnytgY.js +0 -1
- package/web-ui/build/static/nasm-SJmR3Ebn.js +0 -1
- package/web-ui/build/static/neon-B7dnnKEh.js +0 -1
- package/web-ui/build/static/nevod-zunhA02o.js +0 -1
- package/web-ui/build/static/nginx-BR_D0je7.js +0 -1
- package/web-ui/build/static/nginx-Cf7oQt-G.js +0 -1
- package/web-ui/build/static/nim-D-MjIxvw.js +0 -1
- package/web-ui/build/static/nim-D6YOyDC-.js +0 -1
- package/web-ui/build/static/nix-DCZEZqSO.js +0 -1
- package/web-ui/build/static/nix-Dx16U5rS.js +0 -1
- package/web-ui/build/static/node-repl-P-vGpiQ7.js +0 -1
- package/web-ui/build/static/nsis-CjyZMgjR.js +0 -1
- package/web-ui/build/static/nsis-X8NQeZjE.js +0 -1
- package/web-ui/build/static/objectivec-BejNoH7m.js +0 -1
- package/web-ui/build/static/objectivec-CGy2F8n6.js +0 -1
- package/web-ui/build/static/ocaml-C15hcRxR.js +0 -1
- package/web-ui/build/static/ocaml-U7N8L5Z7.js +0 -1
- package/web-ui/build/static/opencl-Bkm-SovU.js +0 -1
- package/web-ui/build/static/openqasm-BOc5975b.js +0 -1
- package/web-ui/build/static/openscad-CRpFnhV_.js +0 -1
- package/web-ui/build/static/oxygene-eHBZbxHy.js +0 -1
- package/web-ui/build/static/oz-B6RWw36L.js +0 -1
- package/web-ui/build/static/parigp-rifRy-ff.js +0 -1
- package/web-ui/build/static/parser-C0HBjZDk.js +0 -1
- package/web-ui/build/static/parser3-DSK81uS5.js +0 -1
- package/web-ui/build/static/pascal-BWha02r-.js +0 -1
- package/web-ui/build/static/pascaligo-B5sQ2hGz.js +0 -1
- package/web-ui/build/static/pcaxis-B1HLYPhm.js +0 -1
- package/web-ui/build/static/peoplecode-C4FPD4Yt.js +0 -1
- package/web-ui/build/static/perl-CyNx_mIa.js +0 -1
- package/web-ui/build/static/perl-rKxEWgtc.js +0 -1
- package/web-ui/build/static/pf-DbL4T2HZ.js +0 -1
- package/web-ui/build/static/pgsql-BKrwbRTY.js +0 -1
- package/web-ui/build/static/php-Cbd5TyJ-.js +0 -1
- package/web-ui/build/static/php-extras-DoVpsRj3.js +0 -1
- package/web-ui/build/static/php-template-DvdLavxo.js +0 -1
- package/web-ui/build/static/php-wYQJLv7J.js +0 -1
- package/web-ui/build/static/phpdoc-DTzZ5d0C.js +0 -1
- package/web-ui/build/static/plaintext-BRfzJjyj.js +0 -1
- package/web-ui/build/static/plsql-BLiE3aGC.js +0 -1
- package/web-ui/build/static/pony-CzLDxYVb.js +0 -1
- package/web-ui/build/static/powerquery-BdGgwM1a.js +0 -1
- package/web-ui/build/static/powershell-Cxl47yK_.js +0 -1
- package/web-ui/build/static/powershell-DrOFoqBE.js +0 -1
- package/web-ui/build/static/processing-9GbETrF_.js +0 -1
- package/web-ui/build/static/processing-M8gRGxck.js +0 -1
- package/web-ui/build/static/profile-CpNlvCnw.js +0 -1
- package/web-ui/build/static/prolog-CKZzbIr9.js +0 -1
- package/web-ui/build/static/prolog-mmIPeMRS.js +0 -1
- package/web-ui/build/static/promql-sDn1PHGN.js +0 -1
- package/web-ui/build/static/properties-CMFMCgWo.js +0 -1
- package/web-ui/build/static/properties-DQUf7KOp.js +0 -1
- package/web-ui/build/static/protobuf-CeY2Q-pH.js +0 -1
- package/web-ui/build/static/protobuf-WpfASK2n.js +0 -1
- package/web-ui/build/static/psl-D2GfEOkk.js +0 -1
- package/web-ui/build/static/pug-OMqhtHt8.js +0 -1
- package/web-ui/build/static/puppet-B68-xib8.js +0 -1
- package/web-ui/build/static/puppet-DEWzLqRg.js +0 -1
- package/web-ui/build/static/pure-C_CchY8g.js +0 -1
- package/web-ui/build/static/purebasic-D6vSqsja.js +0 -1
- package/web-ui/build/static/purebasic-Dy_z6LTs.js +0 -1
- package/web-ui/build/static/purescript-1f1OoM-A.js +0 -1
- package/web-ui/build/static/python-BssLWmNd.js +0 -1
- package/web-ui/build/static/python-CbOivp-X.js +0 -1
- package/web-ui/build/static/python-repl-Dsbi7aBP.js +0 -1
- package/web-ui/build/static/q-BC6w8B4U.js +0 -1
- package/web-ui/build/static/q-Brd1mf_C.js +0 -1
- package/web-ui/build/static/qml-BG4KN74g.js +0 -1
- package/web-ui/build/static/qml-BW-AE2Ma.js +0 -1
- package/web-ui/build/static/qore-m870d7ZA.js +0 -1
- package/web-ui/build/static/qsharp-BC9vEXOd.js +0 -1
- package/web-ui/build/static/r-BQySF8sO.js +0 -1
- package/web-ui/build/static/r-Cur0B9lo.js +0 -1
- package/web-ui/build/static/racket-CiJlajVU.js +0 -1
- package/web-ui/build/static/reason-C3VZcXa9.js +0 -1
- package/web-ui/build/static/reasonml-CpaxZwMO.js +0 -1
- package/web-ui/build/static/regex-bY-o17az.js +0 -1
- package/web-ui/build/static/rego-WPkEW7us.js +0 -1
- package/web-ui/build/static/renpy-DQr5LC4O.js +0 -1
- package/web-ui/build/static/rest-qmfCQ86m.js +0 -1
- package/web-ui/build/static/rib-DYzEblkc.js +0 -1
- package/web-ui/build/static/rip-D-NWUqHZ.js +0 -1
- package/web-ui/build/static/roboconf-CVmdy543.js +0 -1
- package/web-ui/build/static/roboconf-Ray0NGMb.js +0 -1
- package/web-ui/build/static/robotframework-DlxPBTF_.js +0 -1
- package/web-ui/build/static/routeros-S_f-nuVM.js +0 -1
- package/web-ui/build/static/rsl-CHtR71Pc.js +0 -1
- package/web-ui/build/static/ruby-ByuU-3cy.js +0 -1
- package/web-ui/build/static/ruby-DOin939S.js +0 -1
- package/web-ui/build/static/ruleslanguage-zhFrFxeU.js +0 -1
- package/web-ui/build/static/rust-B606UvZv.js +0 -1
- package/web-ui/build/static/rust-BJMuS7if.js +0 -1
- package/web-ui/build/static/sas-Bzk_0rOu.js +0 -1
- package/web-ui/build/static/sas-C-C55Hy4.js +0 -1
- package/web-ui/build/static/sass-DKkkoHtH.js +0 -1
- package/web-ui/build/static/scala-BKlnHttP.js +0 -1
- package/web-ui/build/static/scala-IsJjjIKA.js +0 -1
- package/web-ui/build/static/scheme-Bq_tfrRE.js +0 -1
- package/web-ui/build/static/scheme-DiXFvLKG.js +0 -1
- package/web-ui/build/static/scilab-DVyT87si.js +0 -1
- package/web-ui/build/static/scss-BtDpAVzH.js +0 -1
- package/web-ui/build/static/scss-D8cn4aNM.js +0 -1
- package/web-ui/build/static/shell-YROU1_yo.js +0 -1
- package/web-ui/build/static/shell-session-D06UXOLk.js +0 -1
- package/web-ui/build/static/smali-B1Wl_1dD.js +0 -1
- package/web-ui/build/static/smali-D2R-11j_.js +0 -1
- package/web-ui/build/static/smalltalk-M2S1lL6Z.js +0 -1
- package/web-ui/build/static/smalltalk-OWg84676.js +0 -1
- package/web-ui/build/static/smarty-l1m-zZmR.js +0 -1
- package/web-ui/build/static/sml-B6gSTHMm.js +0 -1
- package/web-ui/build/static/sml-BKURhRNM.js +0 -1
- package/web-ui/build/static/solidity-BcrqESJt.js +0 -1
- package/web-ui/build/static/solution-file-BYpCOi9k.js +0 -1
- package/web-ui/build/static/soy-DOQhQ7Di.js +0 -1
- package/web-ui/build/static/sparql-BwiIX_wO.js +0 -1
- package/web-ui/build/static/splunk-spl-DuIl6XxN.js +0 -1
- package/web-ui/build/static/sqf-8cYPidUR.js +0 -1
- package/web-ui/build/static/sqf-n8nRTmXc.js +0 -1
- package/web-ui/build/static/sql-D0WSLeXV.js +0 -1
- package/web-ui/build/static/sql-D8KWxFl_.js +0 -1
- package/web-ui/build/static/sql_more-D3ozDsf6.js +0 -1
- package/web-ui/build/static/squirrel-C4U4fAL5.js +0 -1
- package/web-ui/build/static/stan-BbFEvT-l.js +0 -1
- package/web-ui/build/static/stan-CJ4ny6dZ.js +0 -1
- package/web-ui/build/static/stata-BKEkbqzD.js +0 -1
- package/web-ui/build/static/step21-52PURHjy.js +0 -1
- package/web-ui/build/static/stylus-buszfxDg.js +0 -1
- package/web-ui/build/static/stylus-y0WOYXDO.js +0 -1
- package/web-ui/build/static/subunit-DU_iInw0.js +0 -1
- package/web-ui/build/static/swift-1VeEUHZi.js +0 -1
- package/web-ui/build/static/swift-Iw0x_Dum.js +0 -1
- package/web-ui/build/static/systemd-BT_5EOz8.js +0 -1
- package/web-ui/build/static/t4-cs-DlHeFrua.js +0 -1
- package/web-ui/build/static/t4-templating-Bz3RFfzp.js +0 -1
- package/web-ui/build/static/t4-vb-BMG0Q3qg.js +0 -1
- package/web-ui/build/static/taggerscript-CxwafGpT.js +0 -1
- package/web-ui/build/static/tap-6QugbkZA.js +0 -1
- package/web-ui/build/static/tap-SB1ST166.js +0 -1
- package/web-ui/build/static/tcl-CpV5bsKx.js +0 -1
- package/web-ui/build/static/tcl-Dikgd33H.js +0 -1
- package/web-ui/build/static/textile-aPmNtrkM.js +0 -1
- package/web-ui/build/static/thrift-B4Snltg5.js +0 -1
- package/web-ui/build/static/toml-B8SfswHQ.js +0 -1
- package/web-ui/build/static/tp-CdO-cn9g.js +0 -1
- package/web-ui/build/static/tremor-CqqCLe4A.js +0 -1
- package/web-ui/build/static/tsx-Ck0ZDqEy.js +0 -1
- package/web-ui/build/static/tt2-BrqZXSkJ.js +0 -1
- package/web-ui/build/static/turtle-DpnSIzmN.js +0 -1
- package/web-ui/build/static/twig-B5Et_eTY.js +0 -1
- package/web-ui/build/static/twig-C3vsy99c.js +0 -1
- package/web-ui/build/static/typescript-DH7tzpeO.js +0 -1
- package/web-ui/build/static/typescript-s1qBbHGm.js +0 -1
- package/web-ui/build/static/typoscript-DD8499cq.js +0 -1
- package/web-ui/build/static/unrealscript-C1zerCVR.js +0 -1
- package/web-ui/build/static/uorazor-CtRNfzg2.js +0 -1
- package/web-ui/build/static/uri-CAfIv8vn.js +0 -1
- package/web-ui/build/static/v-B6_f9ZGJ.js +0 -1
- package/web-ui/build/static/vala-BIpejmrf.js +0 -1
- package/web-ui/build/static/vala-BYLjm9ih.js +0 -1
- package/web-ui/build/static/vbnet-DEW7adbx.js +0 -1
- package/web-ui/build/static/vbnet-D_CuPCaz.js +0 -1
- package/web-ui/build/static/vbscript-Do9hsQ_u.js +0 -1
- package/web-ui/build/static/vbscript-html-4bMQ0-FJ.js +0 -1
- package/web-ui/build/static/velocity-CQjo2-fz.js +0 -1
- package/web-ui/build/static/verilog-C-Xvd__o.js +0 -1
- package/web-ui/build/static/verilog-Dn1ZGsRG.js +0 -1
- package/web-ui/build/static/vhdl-DZ-dTOLA.js +0 -1
- package/web-ui/build/static/vhdl-bDNJfpa7.js +0 -1
- package/web-ui/build/static/vim-BDAo5YHC.js +0 -1
- package/web-ui/build/static/vim-ClkOafc3.js +0 -1
- package/web-ui/build/static/visual-basic-DXIeLZZ8.js +0 -1
- package/web-ui/build/static/warpscript-f7veA-3J.js +0 -1
- package/web-ui/build/static/wasm-TToDlmNk.js +0 -1
- package/web-ui/build/static/web-idl-CrrjMFQT.js +0 -1
- package/web-ui/build/static/wiki-aicisDW2.js +0 -1
- package/web-ui/build/static/wolfram-C_A80rAh.js +0 -1
- package/web-ui/build/static/wren-DkVyeDoy.js +0 -1
- package/web-ui/build/static/x86asm-fx1o_ltw.js +0 -1
- package/web-ui/build/static/xeora-D-uaDMXn.js +0 -1
- package/web-ui/build/static/xl-Dnyvb9_S.js +0 -1
- package/web-ui/build/static/xml-BGV0zG9K.js +0 -1
- package/web-ui/build/static/xml-doc-DOGLcenw.js +0 -1
- package/web-ui/build/static/xojo-B3Qgvb5A.js +0 -1
- package/web-ui/build/static/xquery-C-B5-0Q9.js +0 -1
- package/web-ui/build/static/xquery-KbuiAVhj.js +0 -1
- package/web-ui/build/static/yaml-CBWBoq6m.js +0 -1
- package/web-ui/build/static/yaml-IghlDZf6.js +0 -1
- package/web-ui/build/static/yang-CArp62P5.js +0 -1
- package/web-ui/build/static/zephir-DMLTJ9_9.js +0 -1
- package/web-ui/build/static/zig-CWG2311v.js +0 -1
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TelegramService —
|
|
2
|
+
* TelegramService — Conversational agent interface over Telegram.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Public surface (unchanged):
|
|
5
|
+
* getTelegramService(logger) → singleton
|
|
6
|
+
* .setOrchestrator() .setAgentPool() .setWebSocketManager() .setFlowExecutor()
|
|
7
|
+
* .autoConnect() / .connect(botToken) / .disconnect()
|
|
8
|
+
* .getStatus() / .sendTestMessage()
|
|
9
|
+
* .getBridgedChannels(agentId) / .isAgentBridged(agentId)
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Capabilities (UX-focused):
|
|
12
|
+
* • Multi-chat — any number of private chats can /start the bot; the
|
|
13
|
+
* first-chat-wins lock is gone. Each chat keeps its own sticky agent
|
|
14
|
+
* + watch state. (`chats: Map<chatId, ChatState>`)
|
|
15
|
+
* • Group chat — bots in Telegram groups respond ONLY when @mentioned
|
|
16
|
+
* by their username (Telegram convention; prevents bot spam).
|
|
17
|
+
* • Voice notes — downloaded, base64-posted to the backend's
|
|
18
|
+
* /llm/transcribe endpoint (which proxies to whichever Azure AI
|
|
19
|
+
* Foundry speech-to-text deployment is in the model catalog —
|
|
20
|
+
* gpt-4o-mini-transcribe by default), then handled as text. Falls
|
|
21
|
+
* back to a friendly "voice not configured" message when the
|
|
22
|
+
* backend route returns 503.
|
|
23
|
+
* • Typing indicator + "thinking" placeholder — emitted before the
|
|
24
|
+
* orchestrator call so the user sees instant feedback; replaced
|
|
25
|
+
* with the real reply via editMessageText.
|
|
26
|
+
* • Rich reply blocks via `<actions>` / `<image>` / `<attachment>`
|
|
27
|
+
* tags inside `<external>` — see telegramBlockParser.js.
|
|
28
|
+
* • Errors and timeouts always relayed (not gated by /watch).
|
|
29
|
+
* • Prompt-request timeout sends a "your prompt expired" message
|
|
30
|
+
* instead of silently dropping the request.
|
|
31
|
+
* • Paginated `/agents` and `/flows` for large pools.
|
|
32
|
+
* • Long code-only replies are sent as a `.txt` document so
|
|
33
|
+
* Telegram's MarkdownV2 escaping never mangles them.
|
|
34
|
+
*
|
|
35
|
+
* Config persistence (backward-compatible):
|
|
36
|
+
* Old: `{ chatId, watchEnabled }` (scalar)
|
|
37
|
+
* New: `{ chats: [{ chatId, addedAt, watchEnabled, lastAgentId }], … }`
|
|
38
|
+
* On load, a legacy scalar `chatId` is migrated to a single-entry
|
|
39
|
+
* chats array. Writes always emit the new shape; the legacy field
|
|
40
|
+
* is dropped silently.
|
|
16
41
|
*/
|
|
17
42
|
|
|
18
43
|
import { promises as fs } from 'fs';
|
|
@@ -20,18 +45,22 @@ import path from 'path';
|
|
|
20
45
|
import { getUserDataPaths, ensureUserDataDirs } from '../utilities/userDataDir.js';
|
|
21
46
|
import { filterContentForExternalRelay, resolveBlockTargets } from './channelFilter.js';
|
|
22
47
|
import { createTelegramSource } from './messageSource.js';
|
|
48
|
+
import { parseTelegramBlock } from './telegramBlockParser.js';
|
|
23
49
|
|
|
24
50
|
const TELEGRAM_STATUS = {
|
|
25
51
|
DISCONNECTED: 'disconnected',
|
|
26
|
-
CONNECTING:
|
|
27
|
-
CONNECTED:
|
|
28
|
-
FAILED:
|
|
52
|
+
CONNECTING: 'connecting',
|
|
53
|
+
CONNECTED: 'connected',
|
|
54
|
+
FAILED: 'failed',
|
|
29
55
|
};
|
|
30
56
|
|
|
31
|
-
const MAX_MESSAGE_LENGTH = 4000;
|
|
57
|
+
const MAX_MESSAGE_LENGTH = 4000; // Telegram limit is 4096 — leave room for headers
|
|
32
58
|
const NOTIFICATION_BATCH_INTERVAL_MS = 10000;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
59
|
+
const PROMPT_REMINDER_MS = 180000; // 3 min — "still waiting" nudge
|
|
60
|
+
const PROMPT_TIMEOUT_MS = 600000; // 10 min — hard timeout, send cancel
|
|
61
|
+
const PAGE_SIZE = 8; // /agents and /flows page size
|
|
62
|
+
const CODE_AS_FILE_THRESHOLD = 3200; // chars; above this, send code as .txt
|
|
63
|
+
const IMAGE_PENDING_TTL_MS = 5 * 60 * 1000; // 5 min — buffer window for imageGenerated → stream_complete
|
|
35
64
|
|
|
36
65
|
class TelegramService {
|
|
37
66
|
constructor(logger = null) {
|
|
@@ -46,29 +75,70 @@ class TelegramService {
|
|
|
46
75
|
// Bot state
|
|
47
76
|
this.bot = null;
|
|
48
77
|
this.status = TELEGRAM_STATUS.DISCONNECTED;
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
this.botUsername = null; // populated by getMe() at connect
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Per-chat state. Keyed by chatId (string). Each value:
|
|
82
|
+
* {
|
|
83
|
+
* type: 'private' | 'group' | 'supergroup' | 'channel',
|
|
84
|
+
* title?: string,
|
|
85
|
+
* addedAt: ISO string,
|
|
86
|
+
* watchEnabled: boolean,
|
|
87
|
+
* lastAgentId: string|null,
|
|
88
|
+
* activeAgentIds: Set<string>,
|
|
89
|
+
* // Tracking for in-flight "🔄 Thinking" placeholders:
|
|
90
|
+
* thinkingByAgentId: Map<agentId, messageId>,
|
|
91
|
+
* }
|
|
92
|
+
*/
|
|
93
|
+
this.chats = new Map();
|
|
94
|
+
|
|
95
|
+
// Relay state (global — keyed by requestId)
|
|
96
|
+
this.pendingRelays = new Map(); // requestId → { type, agentId, chatId, timeoutId, reminderId }
|
|
97
|
+
this.replyContext = new Map(); // chatId → { type, requestId, agentId }
|
|
98
|
+
|
|
99
|
+
// Notifications (batched per-chat)
|
|
100
|
+
this.notificationQueue = new Map(); // chatId → string[]
|
|
101
|
+
this.notificationTimers = new Map(); // chatId → timeout handle
|
|
61
102
|
|
|
62
103
|
// Config
|
|
63
104
|
this.dataDir = null;
|
|
64
105
|
this.configPath = null;
|
|
65
106
|
this.config = {};
|
|
66
107
|
|
|
108
|
+
// Backend (for transcription) — falls back to the same default
|
|
109
|
+
// the rest of the CLI uses. webServer.js sets this via
|
|
110
|
+
// setBackendBaseUrl().
|
|
111
|
+
this.backendBaseUrl = 'https://autopilot-api.azurewebsites.net';
|
|
112
|
+
// Platform API key resolver. May be set as either:
|
|
113
|
+
// a) a function () => string|null (preferred — re-resolves on
|
|
114
|
+
// every call, so signin/signout after boot is handled
|
|
115
|
+
// automatically)
|
|
116
|
+
// b) a plain string (legacy — captured once)
|
|
117
|
+
// Both are exposed through `_resolvePlatformKey()` below.
|
|
118
|
+
this._platformApiKey = null;
|
|
119
|
+
|
|
67
120
|
// Original broadcast (saved before wrapping)
|
|
68
121
|
this._originalBroadcast = null;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Pending generated images, keyed by agentId. The imageTool emits
|
|
125
|
+
* its own `imageGenerated` broadcast independently of the agent's
|
|
126
|
+
* text completion; we buffer those URLs here and flush them in
|
|
127
|
+
* `_relayAgentResponse` so the image reaches Telegram even if the
|
|
128
|
+
* agent's final reply text doesn't embed the image markdown.
|
|
129
|
+
*
|
|
130
|
+
* Shape: Map<agentId, Array<{ imageUrl, prompt, addedAt }>>
|
|
131
|
+
*
|
|
132
|
+
* Entries older than IMAGE_PENDING_TTL_MS are pruned at flush time
|
|
133
|
+
* — guards against a stranded image surfacing later in an unrelated
|
|
134
|
+
* conversation turn. The TTL is long enough for slow generations
|
|
135
|
+
* (DALL-E 3, video previews) but short enough to keep the buffer
|
|
136
|
+
* from accumulating across hours of idle.
|
|
137
|
+
*/
|
|
138
|
+
this._pendingImagesByAgent = new Map();
|
|
69
139
|
}
|
|
70
140
|
|
|
71
|
-
//
|
|
141
|
+
// ── Dependency Injection ───────────────────────────────────────────
|
|
72
142
|
|
|
73
143
|
setOrchestrator(orchestrator) { this.orchestrator = orchestrator; }
|
|
74
144
|
setAgentPool(agentPool) { this.agentPool = agentPool; }
|
|
@@ -77,8 +147,20 @@ class TelegramService {
|
|
|
77
147
|
this._interceptBroadcasts(wsManager);
|
|
78
148
|
}
|
|
79
149
|
setFlowExecutor(flowExecutor) { this.flowExecutor = flowExecutor; }
|
|
150
|
+
setBackendBaseUrl(url) { if (url) this.backendBaseUrl = String(url).replace(/\/$/, ''); }
|
|
151
|
+
/** Accept a string OR a () => string|null getter. The getter form is
|
|
152
|
+
* preferred so a sign-in / sign-out cycle after boot is reflected
|
|
153
|
+
* without re-wiring. */
|
|
154
|
+
setPlatformApiKey(keyOrGetter) { this._platformApiKey = keyOrGetter || null; }
|
|
155
|
+
_resolvePlatformKey() {
|
|
156
|
+
const k = this._platformApiKey;
|
|
157
|
+
if (typeof k === 'function') {
|
|
158
|
+
try { return k() || null; } catch { return null; }
|
|
159
|
+
}
|
|
160
|
+
return k || null;
|
|
161
|
+
}
|
|
80
162
|
|
|
81
|
-
//
|
|
163
|
+
// ── Config Persistence ─────────────────────────────────────────────
|
|
82
164
|
|
|
83
165
|
async _ensureDataDir() {
|
|
84
166
|
if (!this.dataDir) {
|
|
@@ -95,22 +177,58 @@ class TelegramService {
|
|
|
95
177
|
try {
|
|
96
178
|
const data = await fs.readFile(this.configPath, 'utf8');
|
|
97
179
|
this.config = JSON.parse(data);
|
|
98
|
-
this.chatId = this.config.chatId || null;
|
|
99
|
-
this.watchEnabled = this.config.watchEnabled || false;
|
|
100
180
|
} catch {
|
|
101
181
|
this.config = {};
|
|
102
182
|
}
|
|
183
|
+
|
|
184
|
+
// Migration: legacy `{ chatId, watchEnabled }` → `{ chats: [...] }`.
|
|
185
|
+
// We intentionally do NOT delete the legacy field from this.config
|
|
186
|
+
// until the next save so a hand-edited file roundtrips cleanly.
|
|
187
|
+
if (this.config.chatId && !Array.isArray(this.config.chats)) {
|
|
188
|
+
this.config.chats = [{
|
|
189
|
+
chatId: String(this.config.chatId),
|
|
190
|
+
type: 'private',
|
|
191
|
+
title: null,
|
|
192
|
+
addedAt: this.config.updatedAt || new Date().toISOString(),
|
|
193
|
+
watchEnabled: !!this.config.watchEnabled,
|
|
194
|
+
lastAgentId: null,
|
|
195
|
+
}];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.chats = new Map();
|
|
199
|
+
for (const entry of (this.config.chats || [])) {
|
|
200
|
+
if (!entry?.chatId) continue;
|
|
201
|
+
this.chats.set(String(entry.chatId), {
|
|
202
|
+
type: entry.type || 'private',
|
|
203
|
+
title: entry.title || null,
|
|
204
|
+
addedAt: entry.addedAt || new Date().toISOString(),
|
|
205
|
+
watchEnabled: !!entry.watchEnabled,
|
|
206
|
+
lastAgentId: entry.lastAgentId || null,
|
|
207
|
+
activeAgentIds: new Set(entry.activeAgentIds || []),
|
|
208
|
+
thinkingByAgentId: new Map(),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
103
211
|
}
|
|
104
212
|
|
|
105
213
|
async _saveConfig() {
|
|
106
214
|
await this._ensureDataDir();
|
|
107
|
-
|
|
108
|
-
this.config.
|
|
215
|
+
// Re-shape in-memory state for persistence.
|
|
216
|
+
this.config.chats = [...this.chats.entries()].map(([chatId, s]) => ({
|
|
217
|
+
chatId,
|
|
218
|
+
type: s.type,
|
|
219
|
+
title: s.title,
|
|
220
|
+
addedAt: s.addedAt,
|
|
221
|
+
watchEnabled: s.watchEnabled,
|
|
222
|
+
lastAgentId: s.lastAgentId,
|
|
223
|
+
activeAgentIds: [...s.activeAgentIds],
|
|
224
|
+
}));
|
|
225
|
+
delete this.config.chatId;
|
|
226
|
+
delete this.config.watchEnabled;
|
|
109
227
|
this.config.updatedAt = new Date().toISOString();
|
|
110
228
|
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
|
|
111
229
|
}
|
|
112
230
|
|
|
113
|
-
//
|
|
231
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
114
232
|
|
|
115
233
|
async autoConnect() {
|
|
116
234
|
await this._loadConfig();
|
|
@@ -127,7 +245,6 @@ class TelegramService {
|
|
|
127
245
|
if (this.status === TELEGRAM_STATUS.CONNECTED) {
|
|
128
246
|
await this.disconnect();
|
|
129
247
|
}
|
|
130
|
-
|
|
131
248
|
this.status = TELEGRAM_STATUS.CONNECTING;
|
|
132
249
|
this.logger?.info('[TelegramService] Connecting...');
|
|
133
250
|
|
|
@@ -135,18 +252,18 @@ class TelegramService {
|
|
|
135
252
|
const TelegramBot = (await import('node-telegram-bot-api')).default;
|
|
136
253
|
this.bot = new TelegramBot(botToken, { polling: true });
|
|
137
254
|
|
|
138
|
-
// Verify token by getting bot info
|
|
139
255
|
const me = await this.bot.getMe();
|
|
140
|
-
this.
|
|
256
|
+
this.botUsername = me.username || null;
|
|
257
|
+
this.logger?.info('[TelegramService] Connected', { botName: this.botUsername });
|
|
141
258
|
|
|
142
259
|
this.config.botToken = botToken;
|
|
143
|
-
this.config.botUsername =
|
|
260
|
+
this.config.botUsername = this.botUsername;
|
|
144
261
|
await this._saveConfig();
|
|
145
262
|
|
|
146
263
|
this._setupHandlers();
|
|
147
264
|
this.status = TELEGRAM_STATUS.CONNECTED;
|
|
148
265
|
|
|
149
|
-
return { username:
|
|
266
|
+
return { username: this.botUsername, id: me.id };
|
|
150
267
|
} catch (error) {
|
|
151
268
|
this.status = TELEGRAM_STATUS.FAILED;
|
|
152
269
|
this.logger?.error('[TelegramService] Connection failed', { error: error.message });
|
|
@@ -156,11 +273,13 @@ class TelegramService {
|
|
|
156
273
|
|
|
157
274
|
async disconnect() {
|
|
158
275
|
if (this.bot) {
|
|
159
|
-
try { await this.bot.stopPolling(); } catch {}
|
|
276
|
+
try { await this.bot.stopPolling(); } catch { /* ignore */ }
|
|
160
277
|
this.bot = null;
|
|
161
278
|
}
|
|
162
279
|
this.status = TELEGRAM_STATUS.DISCONNECTED;
|
|
163
|
-
this.
|
|
280
|
+
for (const t of this.notificationTimers.values()) clearTimeout(t);
|
|
281
|
+
this.notificationTimers.clear();
|
|
282
|
+
this.notificationQueue.clear();
|
|
164
283
|
this.logger?.info('[TelegramService] Disconnected');
|
|
165
284
|
}
|
|
166
285
|
|
|
@@ -168,130 +287,303 @@ class TelegramService {
|
|
|
168
287
|
return {
|
|
169
288
|
status: this.status,
|
|
170
289
|
connected: this.status === TELEGRAM_STATUS.CONNECTED,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
290
|
+
botUsername: this.botUsername,
|
|
291
|
+
chatCount: this.chats.size,
|
|
292
|
+
// Surface the legacy `chatId` as the first chat for UI back-compat;
|
|
293
|
+
// any new UI should switch to `chats`.
|
|
294
|
+
chatId: this.chats.size > 0 ? [...this.chats.keys()][0] : null,
|
|
295
|
+
chats: [...this.chats.entries()].map(([chatId, s]) => ({
|
|
296
|
+
chatId,
|
|
297
|
+
type: s.type,
|
|
298
|
+
title: s.title,
|
|
299
|
+
watchEnabled: s.watchEnabled,
|
|
300
|
+
})),
|
|
301
|
+
watchEnabled: [...this.chats.values()].some(s => s.watchEnabled),
|
|
174
302
|
};
|
|
175
303
|
}
|
|
176
304
|
|
|
177
|
-
//
|
|
305
|
+
// ── Command & Message Handlers ─────────────────────────────────────
|
|
178
306
|
|
|
179
307
|
_setupHandlers() {
|
|
180
308
|
if (!this.bot) return;
|
|
181
309
|
|
|
182
|
-
this.bot.onText(
|
|
183
|
-
this.bot.onText(
|
|
184
|
-
this.bot.onText(
|
|
185
|
-
this.bot.onText(
|
|
186
|
-
this.bot.onText(
|
|
187
|
-
this.bot.onText(
|
|
188
|
-
this.bot.onText(
|
|
189
|
-
this.bot.onText(
|
|
190
|
-
this.bot.onText(
|
|
191
|
-
this.bot.onText(
|
|
192
|
-
this.bot.onText(
|
|
193
|
-
this.bot.onText(
|
|
194
|
-
this.bot.onText(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
this.bot.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
310
|
+
this.bot.onText(/^\/start(@\w+)?(\s|$)/, (msg) => this._cmdStart(msg));
|
|
311
|
+
this.bot.onText(/^\/help(@\w+)?(\s|$)/, (msg) => this._cmdHelp(msg));
|
|
312
|
+
this.bot.onText(/^\/status(@\w+)?(\s|$)/, (msg) => this._cmdStatus(msg));
|
|
313
|
+
this.bot.onText(/^\/agents(@\w+)?(\s|$)/, (msg) => this._cmdAgents(msg, 0));
|
|
314
|
+
this.bot.onText(/^\/agent(@\w+)?\s+(.+)/, (msg, m) => this._cmdAgentDetail(msg, m[2].trim()));
|
|
315
|
+
this.bot.onText(/^\/flows(@\w+)?(\s|$)/, (msg) => this._cmdFlows(msg, 0));
|
|
316
|
+
this.bot.onText(/^\/run(@\w+)?\s+(.+)/, (msg, m) => this._cmdRunFlow(msg, m[2].trim()));
|
|
317
|
+
this.bot.onText(/^\/stop(@\w+)?\s+(.+)/, (msg, m) => this._cmdStopAgent(msg, m[2].trim()));
|
|
318
|
+
this.bot.onText(/^\/following(@\w+)?(\s|$)/, (msg) => this._cmdFollowing(msg));
|
|
319
|
+
this.bot.onText(/^\/unfollow(@\w+)?\s+(.+)/, (msg, m) => this._cmdUnfollow(msg, m[2].trim()));
|
|
320
|
+
this.bot.onText(/^\/watch(@\w+)?(\s|$)/, (msg) => this._cmdWatch(msg));
|
|
321
|
+
this.bot.onText(/^\/unwatch(@\w+)?(\s|$)/, (msg) => this._cmdUnwatch(msg));
|
|
322
|
+
this.bot.onText(/^\/watching(@\w+)?(\s|$)/, (msg) => this._cmdWatching(msg));
|
|
323
|
+
this.bot.onText(/^\/reset(@\w+)?(\s|$)/, (msg) => this._cmdReset(msg));
|
|
324
|
+
this.bot.onText(/^\/new(@\w+)?\s+(.+)/, (msg, m) => this._cmdNew(msg, m[2].trim()));
|
|
325
|
+
this.bot.onText(/^\/logout(@\w+)?(\s|$)/, (msg) => this._cmdLogout(msg));
|
|
326
|
+
|
|
327
|
+
// Generic message handler — branches on type.
|
|
328
|
+
this.bot.on('message', (msg) => this._onMessage(msg));
|
|
329
|
+
|
|
330
|
+
// Inline-keyboard callbacks.
|
|
331
|
+
this.bot.on('callback_query', (q) => this._handleCallbackQuery(q));
|
|
332
|
+
}
|
|
202
333
|
|
|
203
|
-
|
|
204
|
-
|
|
334
|
+
async _onMessage(msg) {
|
|
335
|
+
// Skip slash commands; their handlers fire via onText.
|
|
336
|
+
if (typeof msg.text === 'string' && msg.text.startsWith('/')) return;
|
|
337
|
+
|
|
338
|
+
if (msg.voice) {
|
|
339
|
+
await this._handleVoiceMessage(msg);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (msg.text) {
|
|
343
|
+
await this._handleTextMessage(msg);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Other media types (photos, documents, stickers) are intentionally
|
|
347
|
+
// ignored for now — agents can't yet consume them. Future work:
|
|
348
|
+
// forward image_url to a vision-capable agent.
|
|
205
349
|
}
|
|
206
350
|
|
|
207
351
|
_isAuthorized(msg) {
|
|
208
|
-
|
|
352
|
+
const chatId = String(msg.chat.id);
|
|
353
|
+
return this.chats.has(chatId);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Parse `@agent-name ...` (or just `agent-name ...`) at the start of
|
|
358
|
+
* a message, doing longest-prefix matching against the live agent
|
|
359
|
+
* list so names with spaces work without quoting.
|
|
360
|
+
*
|
|
361
|
+
* Accepts:
|
|
362
|
+
* "@Cody hello" → { agentName: 'Cody', matchedAgent: <agent>, messageText: 'hello' }
|
|
363
|
+
* "@John Smith report" → { agentName: 'John Smith', matchedAgent: <agent>, messageText: 'report' }
|
|
364
|
+
* "@John_Smith report" → same — underscore + dash are accepted as a
|
|
365
|
+
* space-substitute for slug-style addressing
|
|
366
|
+
* "no prefix" → { agentName: null, matchedAgent: null, messageText: 'no prefix' }
|
|
367
|
+
* "@Unknown msg" → { agentName: 'Unknown', matchedAgent: null, messageText: 'msg' }
|
|
368
|
+
* (caller emits a friendly "not found" reply)
|
|
369
|
+
*
|
|
370
|
+
* Algorithm:
|
|
371
|
+
* 1. Strip leading `@` if present. Remember whether one was there.
|
|
372
|
+
* 2. For each agent, see if the remaining text starts with the
|
|
373
|
+
* agent's name (case-insensitive). Try TWO normalisations:
|
|
374
|
+
* a) literal name with all spaces
|
|
375
|
+
* b) name with spaces replaced by [ _\-]+ (slug form)
|
|
376
|
+
* Treat a match as valid only when followed by whitespace or
|
|
377
|
+
* end-of-string (so "@John" doesn't accidentally match "Johnson").
|
|
378
|
+
* 3. Among valid matches, pick the LONGEST agent name — handles
|
|
379
|
+
* the "@John Smith" vs "@John" ambiguity correctly.
|
|
380
|
+
* 4. If no agent name matched but there WAS an `@` prefix, fall
|
|
381
|
+
* back to the legacy single-token capture so the caller can
|
|
382
|
+
* surface a "not found" error with the user-typed text.
|
|
383
|
+
*
|
|
384
|
+
* @param {string} text
|
|
385
|
+
* @param {Array<{id:string, name:string}>} agents
|
|
386
|
+
* @returns {{ agentName: string|null, matchedAgent: object|null, messageText: string }}
|
|
387
|
+
*/
|
|
388
|
+
_resolveAtPrefix(text, agents) {
|
|
389
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
390
|
+
return { agentName: null, matchedAgent: null, messageText: text || '' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const hasAt = text.startsWith('@');
|
|
394
|
+
const body = hasAt ? text.slice(1) : text;
|
|
395
|
+
|
|
396
|
+
// Try to find the longest agent name that the body starts with.
|
|
397
|
+
let best = null; // { agent, matchedRaw, restStart }
|
|
398
|
+
for (const agent of agents || []) {
|
|
399
|
+
const name = String(agent?.name || '');
|
|
400
|
+
if (!name) continue;
|
|
401
|
+
// Build a regex that matches either the literal name or its slug
|
|
402
|
+
// variant (spaces → any of `[ _\-]+`). Case-insensitive. Must be
|
|
403
|
+
// followed by whitespace or end-of-string so partial-word
|
|
404
|
+
// collisions don't false-match.
|
|
405
|
+
const slugPattern = name
|
|
406
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex metas
|
|
407
|
+
.replace(/\s+/g, '[ _\\-]+'); // any space substitute
|
|
408
|
+
const re = new RegExp(`^(${slugPattern})(?:\\s|$)`, 'i');
|
|
409
|
+
const m = body.match(re);
|
|
410
|
+
if (m) {
|
|
411
|
+
if (!best || m[1].length > best.matchedRaw.length) {
|
|
412
|
+
best = { agent, matchedRaw: m[1], restStart: m[1].length };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (best) {
|
|
418
|
+
const rest = body.slice(best.restStart).replace(/^\s+/, '');
|
|
419
|
+
return {
|
|
420
|
+
agentName: best.agent.name,
|
|
421
|
+
matchedAgent: best.agent,
|
|
422
|
+
messageText: rest,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!hasAt) {
|
|
427
|
+
return { agentName: null, matchedAgent: null, messageText: text };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// No agent matched but the user clearly intended an @-prefix
|
|
431
|
+
// (typo, agent renamed, etc.). Fall back to the legacy "first
|
|
432
|
+
// whitespace-free token after @" capture so the caller can quote
|
|
433
|
+
// the user-typed name in the "not found" reply.
|
|
434
|
+
const legacy = body.match(/^(\S+)(?:\s+([\s\S]+))?/);
|
|
435
|
+
if (legacy) {
|
|
436
|
+
return {
|
|
437
|
+
agentName: legacy[1],
|
|
438
|
+
matchedAgent: null,
|
|
439
|
+
messageText: (legacy[2] || '').trim(),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return { agentName: null, matchedAgent: null, messageText: text };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* In a group chat, the bot should only respond to a text message
|
|
447
|
+
* when @-mentioned. This prevents bot spam in shared channels.
|
|
448
|
+
* Returns the message text with the @mention stripped when matched.
|
|
449
|
+
* In private chats, the text is returned unchanged.
|
|
450
|
+
*/
|
|
451
|
+
_stripMentionOrSkip(msg) {
|
|
452
|
+
const isGroup = msg.chat.type === 'group' || msg.chat.type === 'supergroup';
|
|
453
|
+
const text = (msg.text || msg.caption || '').trim();
|
|
454
|
+
if (!isGroup) return { addressed: true, text };
|
|
455
|
+
if (!this.botUsername) return { addressed: false, text };
|
|
456
|
+
|
|
457
|
+
const mention = `@${this.botUsername.toLowerCase()}`;
|
|
458
|
+
const lower = text.toLowerCase();
|
|
459
|
+
if (lower.includes(mention)) {
|
|
460
|
+
const stripped = text.replace(new RegExp(`@${this.botUsername}\\b`, 'gi'), '').replace(/\s+/g, ' ').trim();
|
|
461
|
+
return { addressed: true, text: stripped };
|
|
462
|
+
}
|
|
463
|
+
return { addressed: false, text };
|
|
209
464
|
}
|
|
210
465
|
|
|
466
|
+
// ── Commands ───────────────────────────────────────────────────────
|
|
467
|
+
|
|
211
468
|
async _cmdStart(msg) {
|
|
212
469
|
const chatId = String(msg.chat.id);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
470
|
+
const type = msg.chat.type || 'private';
|
|
471
|
+
const title = msg.chat.title || null;
|
|
472
|
+
|
|
473
|
+
if (!this.chats.has(chatId)) {
|
|
474
|
+
this.chats.set(chatId, {
|
|
475
|
+
type, title,
|
|
476
|
+
addedAt: new Date().toISOString(),
|
|
477
|
+
watchEnabled: false,
|
|
478
|
+
lastAgentId: null,
|
|
479
|
+
activeAgentIds: new Set(),
|
|
480
|
+
thinkingByAgentId: new Map(),
|
|
481
|
+
});
|
|
216
482
|
await this._saveConfig();
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
483
|
+
const greeting = type === 'private'
|
|
484
|
+
? this._md`*Loxia Autopilot connected\\!* 🚀\n\nThis chat is now linked\\. Use /help for commands\\.\n\nAddress agents with \`@agent\\-name your message\` or just start typing once you’ve picked one\\.`
|
|
485
|
+
: this._md`*Loxia Autopilot connected\\!* 🚀\n\nGroup registered: *${title || chatId}*\\. Mention me with @${this.botUsername || 'loxia\\_bot'} to address an agent\\.`;
|
|
486
|
+
await this._send(chatId, greeting);
|
|
220
487
|
} else {
|
|
221
|
-
await this._send(chatId, this.
|
|
488
|
+
await this._send(chatId, this._md`Already connected\\. Use /help for commands\\.`);
|
|
222
489
|
}
|
|
223
490
|
}
|
|
224
491
|
|
|
225
492
|
async _cmdHelp(msg) {
|
|
226
493
|
if (!this._isAuthorized(msg)) return;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
494
|
+
// Hand-written MarkdownV2. Special chars (`.`, `-`, `!`, `(`, `)`,
|
|
495
|
+
// `>`) MUST be escaped per Telegram's MarkdownV2 spec. The
|
|
496
|
+
// _escapeMarkdown wrapper that used to wrap this whole thing
|
|
497
|
+
// turned every `*` and backtick into literal text — that's why
|
|
498
|
+
// the help message displayed asterisks instead of bold.
|
|
499
|
+
const help =
|
|
500
|
+
`*Loxia Autopilot — Telegram*
|
|
501
|
+
|
|
502
|
+
*Chat with agents:*
|
|
503
|
+
\`@agent\\-name message\` — send to a specific agent
|
|
504
|
+
Names with spaces work too: \`@John Smith hello\` or \`@John\\_Smith hello\`
|
|
505
|
+
Type without prefix — sends to your last\\-used agent
|
|
506
|
+
|
|
507
|
+
*Commands:*
|
|
508
|
+
/agents — list all agents \\(paginated\\)
|
|
509
|
+
/agent <name> — agent detail card
|
|
510
|
+
/status — system overview
|
|
511
|
+
/following — agents you have spoken with from this chat
|
|
512
|
+
/unfollow <name> — stop receiving replies from an agent
|
|
513
|
+
/flows — list flows \\(paginated\\)
|
|
514
|
+
/run <flow> — start a flow
|
|
515
|
+
/stop <agent> — halt agent execution
|
|
516
|
+
/reset — clear the conversation with your active agent
|
|
517
|
+
/new <agent> — switch to a different agent with fresh context
|
|
518
|
+
/watch — subscribe to event notifications
|
|
519
|
+
/unwatch — unsubscribe
|
|
520
|
+
/watching — show notification status
|
|
521
|
+
/logout — disconnect this chat from Loxia
|
|
522
|
+
/help — this message
|
|
523
|
+
|
|
524
|
+
_Voice notes are transcribed automatically\\._`;
|
|
525
|
+
await this._send(msg.chat.id, help);
|
|
247
526
|
}
|
|
248
527
|
|
|
249
528
|
async _cmdStatus(msg) {
|
|
250
529
|
if (!this._isAuthorized(msg)) return;
|
|
251
|
-
|
|
252
530
|
try {
|
|
253
531
|
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
254
532
|
const active = agents.filter(a => a.status === 'active' || a.mode === 'auto');
|
|
255
533
|
const idle = agents.filter(a => a.mode === 'chat' || a.status === 'idle');
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
text
|
|
260
|
-
|
|
534
|
+
const state = this._chatState(msg);
|
|
535
|
+
const watch = state?.watchEnabled ? '🔔 On' : '🔕 Off';
|
|
536
|
+
|
|
537
|
+
const text = [
|
|
538
|
+
'*System Status*',
|
|
539
|
+
'',
|
|
540
|
+
`Agents: ${agents.length} total, ${active.length} active, ${idle.length} idle`,
|
|
541
|
+
`Notifications: ${watch}`,
|
|
542
|
+
`Linked chats: ${this.chats.size}`,
|
|
543
|
+
].join('\n');
|
|
261
544
|
await this._send(msg.chat.id, this._escapeMarkdown(text));
|
|
262
545
|
} catch (error) {
|
|
263
546
|
await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
|
|
264
547
|
}
|
|
265
548
|
}
|
|
266
549
|
|
|
267
|
-
async _cmdAgents(msg) {
|
|
550
|
+
async _cmdAgents(msg, page = 0) {
|
|
268
551
|
if (!this._isAuthorized(msg)) return;
|
|
269
|
-
|
|
270
552
|
try {
|
|
271
553
|
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
272
554
|
if (agents.length === 0) {
|
|
273
555
|
await this._send(msg.chat.id, this._escapeMarkdown('No agents loaded.'));
|
|
274
556
|
return;
|
|
275
557
|
}
|
|
276
|
-
|
|
277
|
-
let text = `*Agents (${agents.length}):*\n\n`;
|
|
278
|
-
const buttons = [];
|
|
279
|
-
|
|
280
|
-
for (const agent of agents) {
|
|
281
|
-
const status = agent.mode === 'auto' ? '🟢' : agent.status === 'active' ? '🟡' : '⚪';
|
|
282
|
-
const mode = agent.mode === 'auto' ? 'autonomous' : 'chat';
|
|
283
|
-
text += `${status} *${this._escapeMarkdown(agent.name)}* — ${mode}\n`;
|
|
284
|
-
buttons.push([{ text: agent.name, callback_data: `agent_detail:${agent.id}` }]);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
await this._send(msg.chat.id, text, {
|
|
288
|
-
reply_markup: { inline_keyboard: buttons }
|
|
289
|
-
});
|
|
558
|
+
await this._renderAgentsPage(msg.chat.id, agents, page);
|
|
290
559
|
} catch (error) {
|
|
291
560
|
await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
|
|
292
561
|
}
|
|
293
562
|
}
|
|
294
563
|
|
|
564
|
+
async _renderAgentsPage(chatId, agents, page) {
|
|
565
|
+
const pages = Math.max(1, Math.ceil(agents.length / PAGE_SIZE));
|
|
566
|
+
const safePage = Math.min(Math.max(0, page), pages - 1);
|
|
567
|
+
const slice = agents.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);
|
|
568
|
+
|
|
569
|
+
let text = `*Agents (${agents.length})* — page ${safePage + 1}/${pages}\n\n`;
|
|
570
|
+
const buttons = [];
|
|
571
|
+
for (const agent of slice) {
|
|
572
|
+
const dot = agent.mode === 'auto' ? '🟢' : agent.status === 'active' ? '🟡' : '⚪';
|
|
573
|
+
const mode = agent.mode === 'auto' ? 'autonomous' : 'chat';
|
|
574
|
+
text += `${dot} *${this._escapeMarkdown(agent.name)}* — ${mode}\n`;
|
|
575
|
+
buttons.push([{ text: agent.name, callback_data: `agent_detail:${agent.id}` }]);
|
|
576
|
+
}
|
|
577
|
+
if (pages > 1) {
|
|
578
|
+
const nav = [];
|
|
579
|
+
if (safePage > 0) nav.push({ text: '‹ Prev', callback_data: `agents_page:${safePage - 1}` });
|
|
580
|
+
nav.push({ text: `${safePage + 1}/${pages}`, callback_data: 'noop' });
|
|
581
|
+
if (safePage < pages - 1) nav.push({ text: 'Next ›', callback_data: `agents_page:${safePage + 1}` });
|
|
582
|
+
buttons.push(nav);
|
|
583
|
+
}
|
|
584
|
+
await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
|
|
585
|
+
}
|
|
586
|
+
|
|
295
587
|
async _cmdAgentDetail(msg, agentName) {
|
|
296
588
|
if (!this._isAuthorized(msg)) return;
|
|
297
589
|
await this._showAgentDetail(msg.chat.id, agentName);
|
|
@@ -304,12 +596,10 @@ class TelegramService {
|
|
|
304
596
|
a.name.toLowerCase() === agentNameOrId.toLowerCase() ||
|
|
305
597
|
a.id === agentNameOrId
|
|
306
598
|
);
|
|
307
|
-
|
|
308
599
|
if (!agent) {
|
|
309
600
|
await this._send(chatId, this._escapeMarkdown(`Agent "${agentNameOrId}" not found.`));
|
|
310
601
|
return;
|
|
311
602
|
}
|
|
312
|
-
|
|
313
603
|
const status = agent.mode === 'auto' ? '🟢 Autonomous' : '🟡 Chat';
|
|
314
604
|
let text = `*${this._escapeMarkdown(agent.name)}*\n\n`;
|
|
315
605
|
text += `Status: ${status}\n`;
|
|
@@ -317,52 +607,56 @@ class TelegramService {
|
|
|
317
607
|
if (agent.lastActivity) {
|
|
318
608
|
text += `Last active: ${new Date(agent.lastActivity).toLocaleTimeString()}\n`;
|
|
319
609
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
{ text: '⏹ Stop', callback_data: `stop_agent:${agent.id}` }
|
|
325
|
-
]
|
|
326
|
-
];
|
|
327
|
-
|
|
610
|
+
const buttons = [[
|
|
611
|
+
{ text: '💬 Send Message', callback_data: `msg_agent:${agent.id}` },
|
|
612
|
+
{ text: '⏹ Stop', callback_data: `stop_agent:${agent.id}` },
|
|
613
|
+
]];
|
|
328
614
|
await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
|
|
329
615
|
} catch (error) {
|
|
330
616
|
await this._send(chatId, this._escapeMarkdown(`❌ Error: ${error.message}`));
|
|
331
617
|
}
|
|
332
618
|
}
|
|
333
619
|
|
|
334
|
-
async _cmdFlows(msg) {
|
|
620
|
+
async _cmdFlows(msg, page = 0) {
|
|
335
621
|
if (!this._isAuthorized(msg)) return;
|
|
336
|
-
|
|
337
622
|
try {
|
|
338
623
|
if (!this.orchestrator?.stateManager) {
|
|
339
624
|
await this._send(msg.chat.id, this._escapeMarkdown('Flows not available.'));
|
|
340
625
|
return;
|
|
341
626
|
}
|
|
342
|
-
|
|
343
627
|
const projectDir = this.orchestrator.config?.project?.directory || process.cwd();
|
|
344
628
|
const flowIndex = await this.orchestrator.stateManager.loadFlowIndex?.(projectDir) || {};
|
|
345
629
|
const flows = Object.entries(flowIndex);
|
|
346
|
-
|
|
347
630
|
if (flows.length === 0) {
|
|
348
631
|
await this._send(msg.chat.id, this._escapeMarkdown('No flows defined.'));
|
|
349
632
|
return;
|
|
350
633
|
}
|
|
351
|
-
|
|
352
|
-
let text = `*Flows (${flows.length}):*\n\n`;
|
|
353
|
-
const buttons = [];
|
|
354
|
-
|
|
355
|
-
for (const [id, flow] of flows) {
|
|
356
|
-
text += `📋 *${this._escapeMarkdown(flow.name || id)}*\n`;
|
|
357
|
-
buttons.push([{ text: `▶️ Run ${flow.name || id}`, callback_data: `run_flow:${id}` }]);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
await this._send(msg.chat.id, text, { reply_markup: { inline_keyboard: buttons } });
|
|
634
|
+
await this._renderFlowsPage(msg.chat.id, flows, page);
|
|
361
635
|
} catch (error) {
|
|
362
636
|
await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
|
|
363
637
|
}
|
|
364
638
|
}
|
|
365
639
|
|
|
640
|
+
async _renderFlowsPage(chatId, flows, page) {
|
|
641
|
+
const pages = Math.max(1, Math.ceil(flows.length / PAGE_SIZE));
|
|
642
|
+
const safePage = Math.min(Math.max(0, page), pages - 1);
|
|
643
|
+
const slice = flows.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);
|
|
644
|
+
let text = `*Flows (${flows.length})* — page ${safePage + 1}/${pages}\n\n`;
|
|
645
|
+
const buttons = [];
|
|
646
|
+
for (const [id, flow] of slice) {
|
|
647
|
+
text += `📋 *${this._escapeMarkdown(flow.name || id)}*\n`;
|
|
648
|
+
buttons.push([{ text: `▶️ Run ${flow.name || id}`, callback_data: `run_flow:${id}` }]);
|
|
649
|
+
}
|
|
650
|
+
if (pages > 1) {
|
|
651
|
+
const nav = [];
|
|
652
|
+
if (safePage > 0) nav.push({ text: '‹ Prev', callback_data: `flows_page:${safePage - 1}` });
|
|
653
|
+
nav.push({ text: `${safePage + 1}/${pages}`, callback_data: 'noop' });
|
|
654
|
+
if (safePage < pages - 1) nav.push({ text: 'Next ›', callback_data: `flows_page:${safePage + 1}` });
|
|
655
|
+
buttons.push(nav);
|
|
656
|
+
}
|
|
657
|
+
await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
|
|
658
|
+
}
|
|
659
|
+
|
|
366
660
|
async _cmdRunFlow(msg, flowName) {
|
|
367
661
|
if (!this._isAuthorized(msg)) return;
|
|
368
662
|
await this._runFlow(msg.chat.id, flowName);
|
|
@@ -374,9 +668,7 @@ class TelegramService {
|
|
|
374
668
|
await this._send(chatId, this._escapeMarkdown('Flow executor not available.'));
|
|
375
669
|
return;
|
|
376
670
|
}
|
|
377
|
-
|
|
378
671
|
await this._send(chatId, this._escapeMarkdown(`▶️ Starting flow: ${flowNameOrId}...`));
|
|
379
|
-
// Flow execution is async — completion will be captured by broadcast listener
|
|
380
672
|
const projectDir = this.orchestrator?.config?.project?.directory || process.cwd();
|
|
381
673
|
await this.flowExecutor.executeFlow(flowNameOrId, { projectDir });
|
|
382
674
|
} catch (error) {
|
|
@@ -386,7 +678,6 @@ class TelegramService {
|
|
|
386
678
|
|
|
387
679
|
async _cmdStopAgent(msg, agentName) {
|
|
388
680
|
if (!this._isAuthorized(msg)) return;
|
|
389
|
-
|
|
390
681
|
try {
|
|
391
682
|
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
392
683
|
const agent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
|
|
@@ -394,11 +685,9 @@ class TelegramService {
|
|
|
394
685
|
await this._send(msg.chat.id, this._escapeMarkdown(`Agent "${agentName}" not found.`));
|
|
395
686
|
return;
|
|
396
687
|
}
|
|
397
|
-
|
|
398
688
|
if (this.orchestrator?.messageProcessor) {
|
|
399
689
|
await this.orchestrator.messageProcessor.stopAutonomousExecution(agent.id);
|
|
400
690
|
}
|
|
401
|
-
|
|
402
691
|
await this._send(msg.chat.id, this._escapeMarkdown(`⏹ Stopped ${agent.name}`));
|
|
403
692
|
} catch (error) {
|
|
404
693
|
await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
|
|
@@ -407,106 +696,241 @@ class TelegramService {
|
|
|
407
696
|
|
|
408
697
|
async _cmdFollowing(msg) {
|
|
409
698
|
if (!this._isAuthorized(msg)) return;
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
await this._send(msg.chat.id, this._escapeMarkdown('Not following any agents. Send @agent-name to start
|
|
699
|
+
const state = this._chatState(msg);
|
|
700
|
+
if (!state || state.activeAgentIds.size === 0) {
|
|
701
|
+
await this._send(msg.chat.id, this._escapeMarkdown('Not following any agents in this chat. Send @agent-name to start.'));
|
|
413
702
|
return;
|
|
414
703
|
}
|
|
415
|
-
|
|
416
704
|
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
417
|
-
let text = `*Following ${
|
|
705
|
+
let text = `*Following ${state.activeAgentIds.size} agent\\(s\\):*\n\n`;
|
|
418
706
|
const buttons = [];
|
|
419
|
-
|
|
420
|
-
for (const id of this.activeAgentIds) {
|
|
707
|
+
for (const id of state.activeAgentIds) {
|
|
421
708
|
const agent = agents.find(a => a.id === id);
|
|
422
709
|
const name = agent?.name || id;
|
|
423
|
-
const isLast = id ===
|
|
710
|
+
const isLast = id === state.lastAgentId;
|
|
424
711
|
text += `${isLast ? '💬' : '👁'} *${this._escapeMarkdown(name)}*${isLast ? ' \\(active\\)' : ''}\n`;
|
|
425
712
|
buttons.push([{ text: `❌ Unfollow ${name}`, callback_data: `unfollow:${id}` }]);
|
|
426
713
|
}
|
|
427
|
-
|
|
428
714
|
text += '\n_Active = default for messages without @prefix_';
|
|
429
715
|
await this._send(msg.chat.id, text, { reply_markup: { inline_keyboard: buttons } });
|
|
430
716
|
}
|
|
431
717
|
|
|
432
718
|
async _cmdUnfollow(msg, agentName) {
|
|
433
719
|
if (!this._isAuthorized(msg)) return;
|
|
434
|
-
|
|
720
|
+
const state = this._chatState(msg);
|
|
435
721
|
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
436
722
|
const agent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
|
|
437
|
-
if (!agent || !
|
|
723
|
+
if (!agent || !state.activeAgentIds.has(agent.id)) {
|
|
438
724
|
await this._send(msg.chat.id, this._escapeMarkdown(`Not following "${agentName}".`));
|
|
439
725
|
return;
|
|
440
726
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
727
|
+
state.activeAgentIds.delete(agent.id);
|
|
728
|
+
if (state.lastAgentId === agent.id) {
|
|
729
|
+
state.lastAgentId = state.activeAgentIds.size > 0
|
|
730
|
+
? [...state.activeAgentIds][state.activeAgentIds.size - 1]
|
|
731
|
+
: null;
|
|
446
732
|
}
|
|
447
|
-
await this.
|
|
733
|
+
await this._saveConfig();
|
|
734
|
+
await this._send(msg.chat.id, this._escapeMarkdown(`Unfollowed ${agent.name}.`));
|
|
448
735
|
}
|
|
449
736
|
|
|
450
737
|
async _cmdWatch(msg) {
|
|
451
738
|
if (!this._isAuthorized(msg)) return;
|
|
452
|
-
|
|
739
|
+
const state = this._chatState(msg);
|
|
740
|
+
state.watchEnabled = true;
|
|
453
741
|
await this._saveConfig();
|
|
454
|
-
await this._send(msg.chat.id, this._escapeMarkdown('🔔 Notifications enabled. You\'ll receive alerts for
|
|
742
|
+
await this._send(msg.chat.id, this._escapeMarkdown('🔔 Notifications enabled. You\'ll receive alerts for completions and prompts.'));
|
|
455
743
|
}
|
|
456
744
|
|
|
457
745
|
async _cmdUnwatch(msg) {
|
|
458
746
|
if (!this._isAuthorized(msg)) return;
|
|
459
|
-
|
|
460
|
-
|
|
747
|
+
const state = this._chatState(msg);
|
|
748
|
+
state.watchEnabled = false;
|
|
461
749
|
await this._saveConfig();
|
|
462
|
-
await this._send(msg.chat.id, this._escapeMarkdown('🔕 Notifications disabled.'));
|
|
750
|
+
await this._send(msg.chat.id, this._escapeMarkdown('🔕 Notifications disabled. (Errors and timeouts are still relayed.)'));
|
|
463
751
|
}
|
|
464
752
|
|
|
465
753
|
async _cmdWatching(msg) {
|
|
466
754
|
if (!this._isAuthorized(msg)) return;
|
|
467
|
-
|
|
755
|
+
const state = this._chatState(msg);
|
|
756
|
+
await this._send(msg.chat.id, this._escapeMarkdown(state.watchEnabled
|
|
468
757
|
? '🔔 Notifications are ON. Use /unwatch to disable.'
|
|
469
758
|
: '🔕 Notifications are OFF. Use /watch to enable.'
|
|
470
759
|
));
|
|
471
760
|
}
|
|
472
761
|
|
|
473
|
-
|
|
762
|
+
async _cmdReset(msg) {
|
|
763
|
+
if (!this._isAuthorized(msg)) return;
|
|
764
|
+
const state = this._chatState(msg);
|
|
765
|
+
if (!state.lastAgentId) {
|
|
766
|
+
await this._send(msg.chat.id, this._escapeMarkdown('No active agent to reset. Use /agents to pick one.'));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const agentId = state.lastAgentId;
|
|
770
|
+
const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
|
|
771
|
+
const name = agent?.name || agentId;
|
|
772
|
+
try {
|
|
773
|
+
// Reset agent conversation if the orchestrator exposes it.
|
|
774
|
+
if (this.orchestrator?.messageProcessor?.resetAgentConversation) {
|
|
775
|
+
await this.orchestrator.messageProcessor.resetAgentConversation(agentId);
|
|
776
|
+
} else if (this.agentPool?.clearAgentMessages) {
|
|
777
|
+
await this.agentPool.clearAgentMessages(agentId);
|
|
778
|
+
}
|
|
779
|
+
await this._send(msg.chat.id, this._escapeMarkdown(`🔄 Cleared conversation with ${name}.`));
|
|
780
|
+
} catch (error) {
|
|
781
|
+
await this._send(msg.chat.id, this._escapeMarkdown(`❌ Reset failed: ${error.message}`));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async _cmdNew(msg, agentName) {
|
|
786
|
+
if (!this._isAuthorized(msg)) return;
|
|
787
|
+
const state = this._chatState(msg);
|
|
788
|
+
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
789
|
+
const agent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
|
|
790
|
+
if (!agent) {
|
|
791
|
+
await this._send(msg.chat.id, this._escapeMarkdown(`Agent "${agentName}" not found.`));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
state.lastAgentId = agent.id;
|
|
795
|
+
state.activeAgentIds.add(agent.id);
|
|
796
|
+
// Reset their context.
|
|
797
|
+
try {
|
|
798
|
+
if (this.orchestrator?.messageProcessor?.resetAgentConversation) {
|
|
799
|
+
await this.orchestrator.messageProcessor.resetAgentConversation(agent.id);
|
|
800
|
+
} else if (this.agentPool?.clearAgentMessages) {
|
|
801
|
+
await this.agentPool.clearAgentMessages(agent.id);
|
|
802
|
+
}
|
|
803
|
+
} catch { /* non-fatal */ }
|
|
804
|
+
await this._saveConfig();
|
|
805
|
+
await this._send(msg.chat.id, this._escapeMarkdown(`✨ Switched to ${agent.name} with a fresh context.`));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async _cmdLogout(msg) {
|
|
809
|
+
const chatId = String(msg.chat.id);
|
|
810
|
+
if (!this.chats.has(chatId)) {
|
|
811
|
+
await this._send(chatId, this._escapeMarkdown('Not registered.'));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
this.chats.delete(chatId);
|
|
815
|
+
await this._saveConfig();
|
|
816
|
+
await this._send(chatId, this._escapeMarkdown('👋 This chat has been disconnected from Loxia. /start to re-link.'));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
_chatState(msg) {
|
|
820
|
+
return this.chats.get(String(msg?.chat?.id));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ── Agent Conversation ─────────────────────────────────────────────
|
|
474
824
|
|
|
475
825
|
async _handleTextMessage(msg) {
|
|
476
826
|
if (!this._isAuthorized(msg)) return;
|
|
477
|
-
const
|
|
478
|
-
if (!
|
|
827
|
+
const state = this._chatState(msg);
|
|
828
|
+
if (!state) return;
|
|
479
829
|
|
|
480
|
-
//
|
|
481
|
-
if (this.replyContext) {
|
|
830
|
+
// Pending prompt reply check uses chat-local context.
|
|
831
|
+
if (this.replyContext.has(String(msg.chat.id))) {
|
|
482
832
|
await this._handlePromptReply(msg);
|
|
483
833
|
return;
|
|
484
834
|
}
|
|
485
835
|
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
836
|
+
// Group chats: require @mention.
|
|
837
|
+
const { addressed, text } = this._stripMentionOrSkip(msg);
|
|
838
|
+
if (!addressed) return;
|
|
839
|
+
if (!text) return;
|
|
840
|
+
|
|
841
|
+
await this._dispatchToAgent(msg, text);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async _handleVoiceMessage(msg) {
|
|
845
|
+
if (!this._isAuthorized(msg)) return;
|
|
846
|
+
const state = this._chatState(msg);
|
|
847
|
+
if (!state) return;
|
|
848
|
+
|
|
849
|
+
// Groups: skip voice unless the chat has had at least one @mention
|
|
850
|
+
// (we can't transcribe-and-then-check; too costly to transcribe
|
|
851
|
+
// every voice). For now, ignore voice in groups entirely.
|
|
852
|
+
if (msg.chat.type !== 'private') return;
|
|
489
853
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
854
|
+
try {
|
|
855
|
+
await this.bot.sendChatAction(msg.chat.id, 'typing');
|
|
856
|
+
} catch { /* ignore */ }
|
|
857
|
+
|
|
858
|
+
let transcribed;
|
|
859
|
+
try {
|
|
860
|
+
transcribed = await this._transcribeVoice(msg.voice, msg);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
// Log at ERROR level (not warn) so operators see the failure
|
|
863
|
+
// in the local server console. Then surface a friendly but
|
|
864
|
+
// diagnostic message to the user — include the upstream status
|
|
865
|
+
// / code when present so the cause is visible without server
|
|
866
|
+
// log access.
|
|
867
|
+
this.logger?.error?.('[TelegramService] voice transcription failed', {
|
|
868
|
+
error: err.message, code: err.code, status: err.status,
|
|
869
|
+
});
|
|
870
|
+
let userHint;
|
|
871
|
+
if (err.code === 'not_configured') {
|
|
872
|
+
userHint = 'Voice transcription needs an Azure speech-to-text deployment (e.g. `gpt-4o-mini-transcribe`). Please type your message for now.';
|
|
873
|
+
} else if (err.status === 401 || err.status === 403) {
|
|
874
|
+
userHint = 'Your account isn\'t authorised to call the transcription endpoint. Sign out and back in, then try again.';
|
|
875
|
+
} else if (err.status === 502 || err.status === 503) {
|
|
876
|
+
userHint = 'The transcription service is temporarily unavailable. Please try again in a moment.';
|
|
877
|
+
} else {
|
|
878
|
+
// Surface the upstream message itself so subtle bugs (audio
|
|
879
|
+
// format, file size, etc.) self-diagnose. Truncated to keep
|
|
880
|
+
// the Telegram bubble readable.
|
|
881
|
+
const tail = (err.message || 'unknown error').slice(0, 200);
|
|
882
|
+
userHint = `Please try again, or type your message. (debug: ${tail})`;
|
|
883
|
+
}
|
|
884
|
+
await this._send(msg.chat.id, this._escapeMarkdown(`🎤 Couldn't transcribe that voice note. ${userHint}`));
|
|
885
|
+
return;
|
|
494
886
|
}
|
|
495
887
|
|
|
496
|
-
|
|
888
|
+
if (!transcribed) {
|
|
889
|
+
await this._send(msg.chat.id, this._escapeMarkdown('🎤 Voice note was empty. Please try again or type your message.'));
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Show the user what we heard, then act on it as if they had typed.
|
|
894
|
+
// `_Heard:_` is MarkdownV2 italic markup — must stay UNESCAPED, or
|
|
895
|
+
// the user just sees literal underscores. Only the transcribed
|
|
896
|
+
// body gets escaped, since it can contain arbitrary characters
|
|
897
|
+
// that would otherwise break the parse_mode.
|
|
898
|
+
await this._send(msg.chat.id, `🎤 _Heard:_ ${this._escapeMarkdown(transcribed)}`);
|
|
899
|
+
await this._dispatchToAgent(msg, transcribed);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Common path: resolve target agent and submit to the orchestrator.
|
|
904
|
+
* Emits a typing indicator + a "🔄 Thinking…" placeholder which the
|
|
905
|
+
* broadcast handler will edit with the real reply.
|
|
906
|
+
*/
|
|
907
|
+
async _dispatchToAgent(msg, text) {
|
|
908
|
+
const state = this._chatState(msg);
|
|
909
|
+
|
|
910
|
+
// Parse @agent-name prefix. Supports names with spaces by doing
|
|
911
|
+
// longest-prefix matching against the live agent list — see
|
|
912
|
+
// _resolveAtPrefix() for the contract.
|
|
913
|
+
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
914
|
+
const parsed = this._resolveAtPrefix(text, agents);
|
|
915
|
+
const agentName = parsed.agentName; // null when message has no @prefix
|
|
916
|
+
const messageText = parsed.messageText;
|
|
917
|
+
|
|
918
|
+
// Resolve agent.
|
|
497
919
|
let targetAgent = null;
|
|
498
920
|
if (agentName) {
|
|
499
|
-
|
|
500
|
-
targetAgent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
|
|
921
|
+
targetAgent = parsed.matchedAgent || null;
|
|
501
922
|
if (!targetAgent) {
|
|
502
|
-
await this._send(msg.chat.id, this._escapeMarkdown(
|
|
923
|
+
await this._send(msg.chat.id, this._escapeMarkdown(
|
|
924
|
+
`❌ Agent "${agentName}" not found. Use /agents to see available agents. ` +
|
|
925
|
+
'Tip: names with spaces can be written either as `@John Smith` or `@John_Smith`.'
|
|
926
|
+
));
|
|
503
927
|
return;
|
|
504
928
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
targetAgent = this.agentPool ? await this.agentPool.getAgent(
|
|
929
|
+
state.lastAgentId = targetAgent.id;
|
|
930
|
+
state.activeAgentIds.add(targetAgent.id);
|
|
931
|
+
await this._saveConfig();
|
|
932
|
+
} else if (state.lastAgentId) {
|
|
933
|
+
targetAgent = this.agentPool ? await this.agentPool.getAgent(state.lastAgentId) : null;
|
|
510
934
|
if (!targetAgent) {
|
|
511
935
|
await this._send(msg.chat.id, this._escapeMarkdown('No agent selected. Use @agent-name message to address one.'));
|
|
512
936
|
return;
|
|
@@ -516,15 +940,24 @@ class TelegramService {
|
|
|
516
940
|
return;
|
|
517
941
|
}
|
|
518
942
|
|
|
519
|
-
//
|
|
943
|
+
// Begin typing + thinking placeholder. Saved so the broadcast
|
|
944
|
+
// handler can edit it with the agent's actual reply.
|
|
945
|
+
try { await this.bot.sendChatAction(msg.chat.id, 'typing'); } catch { /* ignore */ }
|
|
520
946
|
try {
|
|
521
|
-
|
|
947
|
+
const placeholder = await this.bot.sendMessage(
|
|
948
|
+
msg.chat.id,
|
|
949
|
+
`🔄 *${this._escapeMarkdown(targetAgent.name)}* is thinking…`,
|
|
950
|
+
{ parse_mode: 'MarkdownV2' }
|
|
951
|
+
);
|
|
952
|
+
state.thinkingByAgentId.set(targetAgent.id, placeholder.message_id);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
this.logger?.debug?.('[TelegramService] thinking placeholder send failed', { error: err.message });
|
|
955
|
+
}
|
|
522
956
|
|
|
957
|
+
// Dispatch to orchestrator.
|
|
958
|
+
try {
|
|
523
959
|
if (this.orchestrator) {
|
|
524
|
-
const sessionId = `telegram-${
|
|
525
|
-
// Capture source at ingress — see services/messageSource.js. The
|
|
526
|
-
// source rides the payload into the orchestrator and becomes the
|
|
527
|
-
// `(Message by alice from Telegram > …)` line the agent sees.
|
|
960
|
+
const sessionId = `telegram-${msg.chat.id}`;
|
|
528
961
|
const source = createTelegramSource(msg);
|
|
529
962
|
await this.orchestrator.processRequest({
|
|
530
963
|
interface: 'telegram',
|
|
@@ -536,83 +969,196 @@ class TelegramService {
|
|
|
536
969
|
streamingEnabled: false,
|
|
537
970
|
source,
|
|
538
971
|
},
|
|
539
|
-
projectDir: this.orchestrator.config?.project?.directory || process.cwd()
|
|
972
|
+
projectDir: this.orchestrator.config?.project?.directory || process.cwd(),
|
|
540
973
|
});
|
|
541
974
|
}
|
|
542
975
|
} catch (error) {
|
|
976
|
+
// Clear the placeholder, surface the failure.
|
|
977
|
+
await this._clearThinking(msg.chat.id, targetAgent.id);
|
|
543
978
|
await this._send(msg.chat.id, this._escapeMarkdown(`❌ Failed to send: ${error.message}`));
|
|
544
979
|
}
|
|
545
980
|
}
|
|
546
981
|
|
|
547
|
-
|
|
982
|
+
async _clearThinking(chatId, agentId) {
|
|
983
|
+
const state = this.chats.get(String(chatId));
|
|
984
|
+
if (!state) return;
|
|
985
|
+
const messageId = state.thinkingByAgentId.get(agentId);
|
|
986
|
+
if (!messageId) return;
|
|
987
|
+
state.thinkingByAgentId.delete(agentId);
|
|
988
|
+
try {
|
|
989
|
+
await this.bot.deleteMessage(chatId, messageId);
|
|
990
|
+
} catch { /* if user deleted it, that's fine */ }
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ── Voice transcription (via backend) ──────────────────────────────
|
|
994
|
+
|
|
995
|
+
async _transcribeVoice(voice, msg = null) {
|
|
996
|
+
if (!voice?.file_id) throw Object.assign(new Error('no file_id'), { code: 'bad_request' });
|
|
997
|
+
const platformKey = this._resolvePlatformKey();
|
|
998
|
+
if (!platformKey) {
|
|
999
|
+
throw Object.assign(new Error('platform key not set'), { code: 'not_configured' });
|
|
1000
|
+
}
|
|
1001
|
+
// 1) Get the file URL from Telegram + download bytes.
|
|
1002
|
+
const fileLink = await this.bot.getFileLink(voice.file_id);
|
|
1003
|
+
const audioRes = await fetch(fileLink);
|
|
1004
|
+
if (!audioRes.ok) throw new Error(`audio download failed: ${audioRes.status}`);
|
|
1005
|
+
const audioBuf = Buffer.from(await audioRes.arrayBuffer());
|
|
1006
|
+
const audioBase64 = audioBuf.toString('base64');
|
|
1007
|
+
|
|
1008
|
+
// 2) POST to backend /llm/transcribe. Telegram knows the audio
|
|
1009
|
+
// duration upfront (voice.duration); pass it so the backend can
|
|
1010
|
+
// price the call correctly even when the model's response shape
|
|
1011
|
+
// doesn't include duration (gpt-4o-*-transcribe with json format).
|
|
1012
|
+
// The Telegram message id is a natural idempotency key — a retry
|
|
1013
|
+
// of the same voice note dedups at the recorder layer.
|
|
1014
|
+
const url = `${this.backendBaseUrl}/llm/transcribe`;
|
|
1015
|
+
const apiRes = await fetch(url, {
|
|
1016
|
+
method: 'POST',
|
|
1017
|
+
headers: {
|
|
1018
|
+
'Authorization': `Bearer ${platformKey}`,
|
|
1019
|
+
'Content-Type': 'application/json',
|
|
1020
|
+
},
|
|
1021
|
+
body: JSON.stringify({
|
|
1022
|
+
audioBase64,
|
|
1023
|
+
mimeType: voice.mime_type || 'audio/ogg',
|
|
1024
|
+
filename: `voice_${Date.now()}.ogg`,
|
|
1025
|
+
durationSeconds: typeof voice.duration === 'number' ? voice.duration : undefined,
|
|
1026
|
+
idempotencyKey: msg?.message_id
|
|
1027
|
+
? `tg-${msg.chat?.id}-${msg.message_id}`
|
|
1028
|
+
: undefined,
|
|
1029
|
+
}),
|
|
1030
|
+
});
|
|
1031
|
+
if (apiRes.status === 503) {
|
|
1032
|
+
throw Object.assign(new Error('not configured'), { code: 'not_configured', status: 503 });
|
|
1033
|
+
}
|
|
1034
|
+
if (!apiRes.ok) {
|
|
1035
|
+
const body = await apiRes.text().catch(() => '');
|
|
1036
|
+
let parsedMsg = '';
|
|
1037
|
+
try { parsedMsg = JSON.parse(body)?.error || ''; } catch { /* not JSON */ }
|
|
1038
|
+
const detail = parsedMsg || body.slice(0, 200);
|
|
1039
|
+
throw Object.assign(
|
|
1040
|
+
new Error(`backend transcribe ${apiRes.status}: ${detail}`),
|
|
1041
|
+
{ status: apiRes.status }
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
const data = await apiRes.json();
|
|
1045
|
+
return (data?.text || '').trim();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ── Callback Query Handler ─────────────────────────────────────────
|
|
548
1049
|
|
|
549
1050
|
async _handleCallbackQuery(query) {
|
|
550
|
-
|
|
1051
|
+
const chatId = String(query.message.chat.id);
|
|
1052
|
+
if (!this.chats.has(chatId)) return;
|
|
1053
|
+
const state = this.chats.get(chatId);
|
|
551
1054
|
|
|
552
|
-
const data = query.data;
|
|
553
|
-
try {
|
|
554
|
-
await this.bot.answerCallbackQuery(query.id);
|
|
555
|
-
} catch {}
|
|
1055
|
+
const data = query.data || '';
|
|
1056
|
+
try { await this.bot.answerCallbackQuery(query.id); } catch { /* ignore */ }
|
|
556
1057
|
|
|
1058
|
+
if (data === 'noop') return;
|
|
1059
|
+
|
|
1060
|
+
if (data.startsWith('agents_page:')) {
|
|
1061
|
+
const page = parseInt(data.split(':')[1], 10) || 0;
|
|
1062
|
+
const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
|
|
1063
|
+
await this._renderAgentsPage(chatId, agents, page);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (data.startsWith('flows_page:')) {
|
|
1067
|
+
const page = parseInt(data.split(':')[1], 10) || 0;
|
|
1068
|
+
if (!this.orchestrator?.stateManager) return;
|
|
1069
|
+
const projectDir = this.orchestrator.config?.project?.directory || process.cwd();
|
|
1070
|
+
const flowIndex = await this.orchestrator.stateManager.loadFlowIndex?.(projectDir) || {};
|
|
1071
|
+
await this._renderFlowsPage(chatId, Object.entries(flowIndex), page);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
557
1074
|
if (data.startsWith('agent_detail:')) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1075
|
+
await this._showAgentDetail(chatId, data.slice('agent_detail:'.length));
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (data.startsWith('msg_agent:')) {
|
|
1079
|
+
const agentId = data.slice('msg_agent:'.length);
|
|
1080
|
+
state.lastAgentId = agentId;
|
|
1081
|
+
state.activeAgentIds.add(agentId);
|
|
1082
|
+
await this._saveConfig();
|
|
564
1083
|
const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
1084
|
+
await this._send(chatId, this._escapeMarkdown(`💬 Now chatting with ${agent?.name || agentId}. Type your message.`));
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (data.startsWith('stop_agent:')) {
|
|
1088
|
+
const agentId = data.slice('stop_agent:'.length);
|
|
569
1089
|
if (this.orchestrator?.messageProcessor) {
|
|
570
1090
|
await this.orchestrator.messageProcessor.stopAutonomousExecution(agentId);
|
|
571
1091
|
}
|
|
572
|
-
await this._send(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1092
|
+
await this._send(chatId, this._escapeMarkdown('⏹ Agent stopped.'));
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (data.startsWith('run_flow:')) {
|
|
1096
|
+
await this._runFlow(chatId, data.slice('run_flow:'.length));
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (data.startsWith('unfollow:')) {
|
|
1100
|
+
const agentId = data.slice('unfollow:'.length);
|
|
1101
|
+
state.activeAgentIds.delete(agentId);
|
|
1102
|
+
if (state.lastAgentId === agentId) {
|
|
1103
|
+
state.lastAgentId = state.activeAgentIds.size > 0 ? [...state.activeAgentIds][0] : null;
|
|
581
1104
|
}
|
|
1105
|
+
await this._saveConfig();
|
|
582
1106
|
const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
|
|
583
|
-
await this._send(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1107
|
+
await this._send(chatId, this._escapeMarkdown(`Unfollowed ${agent?.name || agentId}.`));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (data.startsWith('prompt_reply:')) {
|
|
1111
|
+
const m = data.match(/prompt_reply:(.+):(\d+)/);
|
|
1112
|
+
if (m && this.pendingRelays.has(m[1])) {
|
|
1113
|
+
await this._submitPromptReply(m[1], m[2]);
|
|
589
1114
|
}
|
|
1115
|
+
return;
|
|
590
1116
|
}
|
|
1117
|
+
if (data.startsWith('act:')) {
|
|
1118
|
+
// <actions> button tapped — treat value as a user message to the
|
|
1119
|
+
// sticky agent. Format: `act:<agentId>:<value>` where value may
|
|
1120
|
+
// contain colons; split on first two.
|
|
1121
|
+
const idx1 = data.indexOf(':');
|
|
1122
|
+
const idx2 = data.indexOf(':', idx1 + 1);
|
|
1123
|
+
const agentId = data.slice(idx1 + 1, idx2);
|
|
1124
|
+
const value = data.slice(idx2 + 1);
|
|
1125
|
+
if (!agentId || !value) return;
|
|
1126
|
+
|
|
1127
|
+
// Synthesize a message that mirrors what a real user input would
|
|
1128
|
+
// look like in the same handler. Forwarding the tap as a
|
|
1129
|
+
// synthetic user turn keeps the agent's transcript honest.
|
|
1130
|
+
const synthetic = {
|
|
1131
|
+
chat: query.message.chat,
|
|
1132
|
+
from: query.from,
|
|
1133
|
+
message_id: query.message.message_id,
|
|
1134
|
+
text: `@${(await this._agentName(agentId))} ${value}`,
|
|
1135
|
+
};
|
|
1136
|
+
await this._dispatchToAgent(synthetic, synthetic.text);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async _agentName(agentId) {
|
|
1141
|
+
const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
|
|
1142
|
+
return agent?.name || agentId;
|
|
591
1143
|
}
|
|
592
1144
|
|
|
593
|
-
//
|
|
1145
|
+
// ── Broadcast Interceptor ──────────────────────────────────────────
|
|
594
1146
|
|
|
595
1147
|
_interceptBroadcasts(wsManager) {
|
|
596
1148
|
if (!wsManager || this._originalBroadcast) return;
|
|
597
|
-
|
|
598
1149
|
const originalBroadcast = wsManager.broadcastToSession.bind(wsManager);
|
|
599
1150
|
this._originalBroadcast = originalBroadcast;
|
|
600
|
-
|
|
601
1151
|
wsManager.broadcastToSession = (sessionId, message) => {
|
|
602
|
-
// Call original
|
|
603
1152
|
originalBroadcast(sessionId, message);
|
|
604
|
-
// Forward to Telegram
|
|
605
1153
|
this._handleBroadcastEvent(message);
|
|
606
1154
|
};
|
|
607
1155
|
}
|
|
608
1156
|
|
|
609
1157
|
async _handleBroadcastEvent(message) {
|
|
610
|
-
if (!this.bot ||
|
|
611
|
-
|
|
1158
|
+
if (!this.bot || this.chats.size === 0 || this.status !== TELEGRAM_STATUS.CONNECTED) return;
|
|
612
1159
|
const type = message?.type;
|
|
613
1160
|
if (!type) return;
|
|
614
1161
|
|
|
615
|
-
// Always relay prompt/credential requests (they block agent progress)
|
|
616
1162
|
if (type === 'user_prompt_request') {
|
|
617
1163
|
await this._relayPromptRequest(message);
|
|
618
1164
|
return;
|
|
@@ -621,37 +1167,56 @@ class TelegramService {
|
|
|
621
1167
|
await this._relayCredentialRequest(message);
|
|
622
1168
|
return;
|
|
623
1169
|
}
|
|
624
|
-
|
|
625
|
-
// Agent responses — only relay stream_complete (message_added is a duplicate of the same content)
|
|
626
1170
|
if (type === 'stream_complete') {
|
|
627
1171
|
await this._relayAgentResponse(message);
|
|
628
1172
|
return;
|
|
629
1173
|
}
|
|
1174
|
+
if (type === 'imageGenerated') {
|
|
1175
|
+
// Buffer the URL keyed by agentId. The actual send happens when
|
|
1176
|
+
// the matching stream_complete arrives (typical case — agent
|
|
1177
|
+
// says "here's your image: ..." with the image markdown), or as
|
|
1178
|
+
// a standalone send if no text reply ever lands (image-only
|
|
1179
|
+
// turn, e.g. an automated render flow).
|
|
1180
|
+
this._bufferGeneratedImage(message);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
630
1183
|
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1184
|
+
// Errors and timeouts ALWAYS reach the user. Other completions
|
|
1185
|
+
// require /watch.
|
|
1186
|
+
const ALWAYS_RELAY = new Set(['agent_error', 'agent_timeout', 'criticalError', 'flow_run_failed']);
|
|
1187
|
+
const WATCH_GATED = new Set(['execution_stopped', 'flow_run_completed']);
|
|
1188
|
+
|
|
1189
|
+
const headers = {
|
|
1190
|
+
agent_error: '⚠️ *Agent Error*',
|
|
1191
|
+
agent_timeout: '⏰ *Agent Timeout*',
|
|
1192
|
+
criticalError: '🔴 *Critical Error*',
|
|
1193
|
+
flow_run_failed: '❌ *Flow Failed*',
|
|
1194
|
+
execution_stopped: '✅ *Agent Finished*',
|
|
1195
|
+
flow_run_completed: '✅ *Flow Completed*',
|
|
641
1196
|
};
|
|
1197
|
+
if (!headers[type]) return;
|
|
642
1198
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1199
|
+
let text = `${headers[type]}\n`;
|
|
1200
|
+
if (message.agentName || message.data?.agentName) {
|
|
1201
|
+
text += `Agent: \`${message.agentName || message.data?.agentName}\`\n`;
|
|
1202
|
+
}
|
|
1203
|
+
if (message.message || message.data?.message || message.error) {
|
|
1204
|
+
text += `${message.message || message.data?.message || message.error}\n`;
|
|
1205
|
+
}
|
|
646
1206
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1207
|
+
if (ALWAYS_RELAY.has(type)) {
|
|
1208
|
+
this._fanoutNotification(text);
|
|
1209
|
+
} else if (WATCH_GATED.has(type)) {
|
|
1210
|
+
for (const [chatId, state] of this.chats.entries()) {
|
|
1211
|
+
if (state.watchEnabled) this._queueNotification(chatId, text);
|
|
652
1212
|
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
653
1215
|
|
|
654
|
-
|
|
1216
|
+
/** Send `text` to every linked chat regardless of /watch state. */
|
|
1217
|
+
async _fanoutNotification(text) {
|
|
1218
|
+
for (const chatId of this.chats.keys()) {
|
|
1219
|
+
try { await this._send(chatId, text); } catch { /* per-chat failures shouldn't block others */ }
|
|
655
1220
|
}
|
|
656
1221
|
}
|
|
657
1222
|
|
|
@@ -659,188 +1224,415 @@ class TelegramService {
|
|
|
659
1224
|
const agentId = message.agentId || message.data?.agentId;
|
|
660
1225
|
if (!agentId) return;
|
|
661
1226
|
|
|
662
|
-
// Only relay responses for agents the user has addressed from Telegram
|
|
663
|
-
if (!this.activeAgentIds.has(agentId)) return;
|
|
664
|
-
|
|
665
1227
|
const content = message.content || message.data?.content ||
|
|
666
1228
|
message.message?.content || message.data?.message?.content;
|
|
667
|
-
if (!content) return;
|
|
668
1229
|
|
|
669
|
-
// Skip tool
|
|
1230
|
+
// Skip user/tool roles.
|
|
670
1231
|
const role = message.role || message.data?.role || message.message?.role;
|
|
671
1232
|
if (role === 'user' || role === 'tool') return;
|
|
672
1233
|
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
1234
|
+
// Drain any image buffered by the imageTool. We do this even when
|
|
1235
|
+
// there's no text content — image-only completions still need to
|
|
1236
|
+
// reach Telegram. We do it for every active chat with this agent.
|
|
1237
|
+
const pendingImages = this._consumePendingImages(agentId);
|
|
1238
|
+
|
|
1239
|
+
const { blocks } = content ? filterContentForExternalRelay(content) : { blocks: [] };
|
|
1240
|
+
|
|
678
1241
|
if (blocks.length === 0) {
|
|
679
|
-
|
|
1242
|
+
// No <external> text block. Clear thinking placeholders so the
|
|
1243
|
+
// user isn't left looking at "🔄 …", and — critically — still
|
|
1244
|
+
// ship any buffered images for this agent so "draw me a sunset"
|
|
1245
|
+
// through Telegram doesn't go silent when the agent's text
|
|
1246
|
+
// reply omits the image markdown.
|
|
1247
|
+
for (const [chatId, state] of this.chats.entries()) {
|
|
1248
|
+
await this._clearThinking(chatId, agentId);
|
|
1249
|
+
if (pendingImages.length > 0 && state.activeAgentIds.has(agentId)) {
|
|
1250
|
+
for (const img of pendingImages) {
|
|
1251
|
+
await this._sendMedia(chatId, { kind: 'image', src: img.imageUrl, caption: img.prompt || undefined });
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
680
1255
|
return;
|
|
681
1256
|
}
|
|
682
1257
|
|
|
683
|
-
const bridged = this.getBridgedChannels(agentId);
|
|
684
|
-
if (bridged.length === 0) return;
|
|
685
|
-
const ownedAliases = bridged.map(c => c.alias);
|
|
686
|
-
|
|
687
1258
|
const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
|
|
688
1259
|
const agentName = agent?.name || agentId;
|
|
689
1260
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1261
|
+
// For each chat where this agent is active, send the block(s).
|
|
1262
|
+
for (const [chatId, state] of this.chats.entries()) {
|
|
1263
|
+
if (!state.activeAgentIds.has(agentId)) continue;
|
|
1264
|
+
|
|
1265
|
+
const ownedAliases = this._aliasesForChat(chatId);
|
|
1266
|
+
let firstBlock = true;
|
|
1267
|
+
|
|
1268
|
+
for (const block of blocks) {
|
|
1269
|
+
const targets = resolveBlockTargets(block, ownedAliases);
|
|
1270
|
+
if (targets.length === 0) continue;
|
|
1271
|
+
|
|
697
1272
|
try {
|
|
698
|
-
|
|
1273
|
+
const parsed = parseTelegramBlock(block.text);
|
|
1274
|
+
|
|
1275
|
+
// Edit the thinking placeholder on the first block; subsequent
|
|
1276
|
+
// blocks go as new messages so users still see them as separate
|
|
1277
|
+
// turns when an agent emits multiple <external> blocks.
|
|
1278
|
+
if (firstBlock) {
|
|
1279
|
+
await this._replaceThinkingWithReply(chatId, agentId, agentName, parsed);
|
|
1280
|
+
firstBlock = false;
|
|
1281
|
+
} else {
|
|
1282
|
+
await this._sendParsedPayload(chatId, agentName, parsed);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Media is sent after the text body in either path.
|
|
1286
|
+
for (const media of parsed.media) {
|
|
1287
|
+
await this._sendMedia(chatId, media);
|
|
1288
|
+
}
|
|
699
1289
|
} catch (error) {
|
|
700
|
-
this.logger?.warn?.('[TelegramService]
|
|
701
|
-
|
|
1290
|
+
this.logger?.warn?.('[TelegramService] block relay failed', {
|
|
1291
|
+
chatId, agentId, error: error.message,
|
|
702
1292
|
});
|
|
703
1293
|
}
|
|
704
1294
|
}
|
|
1295
|
+
|
|
1296
|
+
// After the text blocks land, ship any imageTool-generated
|
|
1297
|
+
// images that aren't already embedded in a parsed block. We
|
|
1298
|
+
// dedupe by URL against the block parser's media list so we
|
|
1299
|
+
// don't double-post an image the agent already referenced
|
|
1300
|
+
// inline.
|
|
1301
|
+
if (pendingImages.length > 0) {
|
|
1302
|
+
const embeddedUrls = new Set();
|
|
1303
|
+
for (const block of blocks) {
|
|
1304
|
+
try {
|
|
1305
|
+
for (const m of parseTelegramBlock(block.text).media) {
|
|
1306
|
+
if (m?.src) embeddedUrls.add(m.src);
|
|
1307
|
+
}
|
|
1308
|
+
} catch { /* parser failures already logged above */ }
|
|
1309
|
+
}
|
|
1310
|
+
for (const img of pendingImages) {
|
|
1311
|
+
if (embeddedUrls.has(img.imageUrl)) continue;
|
|
1312
|
+
await this._sendMedia(chatId, { kind: 'image', src: img.imageUrl, caption: img.prompt || undefined });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
705
1315
|
}
|
|
706
1316
|
}
|
|
707
1317
|
|
|
708
1318
|
/**
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
* @param {string} agentId
|
|
715
|
-
* @returns {Array<{alias: string, label: string}>}
|
|
1319
|
+
* Stash an `imageGenerated` broadcast for later flush by
|
|
1320
|
+
* `_relayAgentResponse`. Prunes stale entries (>5min) on insert so
|
|
1321
|
+
* the buffer can't grow unboundedly if no stream_complete ever
|
|
1322
|
+
* arrives for that agent.
|
|
716
1323
|
*/
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1324
|
+
_bufferGeneratedImage(message) {
|
|
1325
|
+
const agentId = message.agentId || message.data?.agentId;
|
|
1326
|
+
const imageUrl = message.imageUrl || message.data?.imageUrl;
|
|
1327
|
+
if (!agentId || !imageUrl) return;
|
|
1328
|
+
const prompt = message.prompt || message.data?.prompt || null;
|
|
1329
|
+
|
|
1330
|
+
const arr = this._pendingImagesByAgent.get(agentId) || [];
|
|
1331
|
+
const now = Date.now();
|
|
1332
|
+
// Drop stale entries while we're touching this list.
|
|
1333
|
+
const fresh = arr.filter(e => now - e.addedAt < IMAGE_PENDING_TTL_MS);
|
|
1334
|
+
fresh.push({ imageUrl, prompt, addedAt: now });
|
|
1335
|
+
this._pendingImagesByAgent.set(agentId, fresh);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/** Atomically read-and-clear pending images for `agentId`. */
|
|
1339
|
+
_consumePendingImages(agentId) {
|
|
1340
|
+
const arr = this._pendingImagesByAgent.get(agentId);
|
|
1341
|
+
if (!arr || arr.length === 0) return [];
|
|
1342
|
+
this._pendingImagesByAgent.delete(agentId);
|
|
1343
|
+
const now = Date.now();
|
|
1344
|
+
return arr.filter(e => now - e.addedAt < IMAGE_PENDING_TTL_MS);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
_aliasesForChat(chatId) {
|
|
1348
|
+
// Today we advertise a single chat-scoped alias; expanding later
|
|
1349
|
+
// to per-channel aliases is a one-line change.
|
|
1350
|
+
return ['telegram', `telegram:chat-${chatId}`];
|
|
720
1351
|
}
|
|
721
1352
|
|
|
722
1353
|
/**
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
* @param {string} agentId
|
|
728
|
-
* @returns {boolean}
|
|
1354
|
+
* Edit the in-flight thinking placeholder to show the agent's real
|
|
1355
|
+
* reply. Falls back to a fresh message if editing fails (e.g. the
|
|
1356
|
+
* placeholder was deleted by the user).
|
|
729
1357
|
*/
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1358
|
+
async _replaceThinkingWithReply(chatId, agentId, agentName, parsed) {
|
|
1359
|
+
const state = this.chats.get(String(chatId));
|
|
1360
|
+
const placeholderId = state?.thinkingByAgentId.get(agentId);
|
|
1361
|
+
state?.thinkingByAgentId.delete(agentId);
|
|
1362
|
+
|
|
1363
|
+
const header = `*${this._escapeMarkdown(agentName)}:*\n\n`;
|
|
1364
|
+
const formatted = this._formatAgentBody(parsed.text);
|
|
1365
|
+
const replyMarkup = this._buildActionKeyboard(agentId, parsed.actions);
|
|
1366
|
+
|
|
1367
|
+
if (formatted.kind === 'document') {
|
|
1368
|
+
// Code-heavy reply: drop the placeholder, send the document.
|
|
1369
|
+
if (placeholderId) {
|
|
1370
|
+
try { await this.bot.deleteMessage(chatId, placeholderId); } catch {}
|
|
1371
|
+
}
|
|
1372
|
+
await this._sendCodeDocument(chatId, agentName, formatted.body);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const fullText = header + formatted.body;
|
|
1377
|
+
const sendOpts = { parse_mode: formatted.parseMode };
|
|
1378
|
+
if (replyMarkup) sendOpts.reply_markup = replyMarkup;
|
|
734
1379
|
|
|
735
|
-
|
|
1380
|
+
if (placeholderId) {
|
|
1381
|
+
try {
|
|
1382
|
+
await this.bot.editMessageText(fullText, {
|
|
1383
|
+
chat_id: chatId,
|
|
1384
|
+
message_id: placeholderId,
|
|
1385
|
+
...sendOpts,
|
|
1386
|
+
});
|
|
1387
|
+
return;
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
this.logger?.debug?.('[TelegramService] edit placeholder failed, sending fresh', { error: err.message });
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
await this._sendRaw(chatId, fullText, sendOpts);
|
|
1393
|
+
}
|
|
736
1394
|
|
|
737
|
-
|
|
1395
|
+
/** Send a parsed payload (additional blocks after the first). */
|
|
1396
|
+
async _sendParsedPayload(chatId, agentName, parsed) {
|
|
738
1397
|
const header = `*${this._escapeMarkdown(agentName)}:*\n\n`;
|
|
1398
|
+
const formatted = this._formatAgentBody(parsed.text);
|
|
1399
|
+
if (formatted.kind === 'document') {
|
|
1400
|
+
await this._sendCodeDocument(chatId, agentName, formatted.body);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const replyMarkup = this._buildActionKeyboard(null, parsed.actions);
|
|
1404
|
+
const opts = { parse_mode: formatted.parseMode };
|
|
1405
|
+
if (replyMarkup) opts.reply_markup = replyMarkup;
|
|
1406
|
+
await this._sendRaw(chatId, header + formatted.body, opts);
|
|
1407
|
+
}
|
|
739
1408
|
|
|
740
|
-
|
|
741
|
-
|
|
1409
|
+
_buildActionKeyboard(agentId, actions) {
|
|
1410
|
+
if (!Array.isArray(actions) || actions.length === 0) return null;
|
|
1411
|
+
if (!agentId) return null; // can't dispatch without an agent
|
|
1412
|
+
return {
|
|
1413
|
+
inline_keyboard: actions.map(a => ([{
|
|
1414
|
+
text: a.label,
|
|
1415
|
+
// Telegram caps callback_data at 64 bytes. The shape is
|
|
1416
|
+
// `act:<agentId>:<value>` so the value is what's first to be
|
|
1417
|
+
// truncated. agentId is typically a UUID (36 chars) + 'act:'
|
|
1418
|
+
// prefix (4 chars) leaves 24 chars for the value.
|
|
1419
|
+
callback_data: `act:${agentId}:${a.value}`.slice(0, 64),
|
|
1420
|
+
}])),
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
742
1423
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1424
|
+
// ── Text formatting: HTML for code blocks, MarkdownV2 otherwise ────
|
|
1425
|
+
|
|
1426
|
+
_formatAgentBody(body) {
|
|
1427
|
+
if (!body || typeof body !== 'string' || body.trim() === '') {
|
|
1428
|
+
return { kind: 'text', parseMode: 'MarkdownV2', body: this._escapeMarkdown('(empty response)') };
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const fenceCount = (body.match(/```/g) || []).length;
|
|
1432
|
+
const hasFencedCode = fenceCount >= 2;
|
|
1433
|
+
const isCodeHeavy = hasFencedCode && body.length > CODE_AS_FILE_THRESHOLD;
|
|
1434
|
+
|
|
1435
|
+
if (isCodeHeavy) {
|
|
1436
|
+
// Send as document — guarantees no markdown mangling for big payloads.
|
|
1437
|
+
return { kind: 'document', body };
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (hasFencedCode) {
|
|
1441
|
+
// HTML mode is far more forgiving than MarkdownV2 for mixed-code text.
|
|
1442
|
+
// Convert fenced blocks to <pre><code>…</code></pre>.
|
|
1443
|
+
const htmlBody = body.replace(/```(\w+)?\n([\s\S]*?)```/g, (_full, _lang, code) => {
|
|
1444
|
+
const escaped = String(code)
|
|
1445
|
+
.replace(/&/g, '&')
|
|
1446
|
+
.replace(/</g, '<')
|
|
1447
|
+
.replace(/>/g, '>');
|
|
1448
|
+
return `<pre>${escaped}</pre>`;
|
|
1449
|
+
});
|
|
1450
|
+
// Inline-code backticks too.
|
|
1451
|
+
const withInline = htmlBody.replace(/`([^`\n]+)`/g, (_m, c) => {
|
|
1452
|
+
const esc = String(c).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1453
|
+
return `<code>${esc}</code>`;
|
|
1454
|
+
});
|
|
1455
|
+
// Escape everything else that HTML mode cares about (only <, >, &
|
|
1456
|
+
// outside the tags). Easiest: split by <pre>/<code> tags and
|
|
1457
|
+
// escape non-code parts.
|
|
1458
|
+
const safe = this._escapeHtmlPreservingTags(withInline);
|
|
1459
|
+
const truncated = safe.length > MAX_MESSAGE_LENGTH
|
|
1460
|
+
? safe.slice(0, MAX_MESSAGE_LENGTH - 100) + '\n\n… (truncated)'
|
|
1461
|
+
: safe;
|
|
1462
|
+
return { kind: 'text', parseMode: 'HTML', body: truncated };
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Plain text: full MarkdownV2 escape.
|
|
1466
|
+
const escaped = this._escapeMarkdown(body);
|
|
1467
|
+
const truncated = escaped.length > MAX_MESSAGE_LENGTH
|
|
1468
|
+
? escaped.slice(0, MAX_MESSAGE_LENGTH - 100) + '\n\n… (truncated)'
|
|
1469
|
+
: escaped;
|
|
1470
|
+
return { kind: 'text', parseMode: 'MarkdownV2', body: truncated };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
_escapeHtmlPreservingTags(text) {
|
|
1474
|
+
// Split into segments alternating between tag-segments and text.
|
|
1475
|
+
// We only allow <pre>/<code>; anything else gets escaped.
|
|
1476
|
+
const parts = text.split(/(<\/?(?:pre|code)>)/g);
|
|
1477
|
+
return parts.map((p) => {
|
|
1478
|
+
if (/^<\/?(pre|code)>$/i.test(p)) return p;
|
|
1479
|
+
return p.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1480
|
+
}).join('');
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
async _sendCodeDocument(chatId, agentName, body) {
|
|
1484
|
+
if (!this.bot) return;
|
|
1485
|
+
try {
|
|
1486
|
+
const filename = `${(agentName || 'reply').replace(/[^A-Za-z0-9-]/g, '_')}-${Date.now()}.txt`;
|
|
1487
|
+
// node-telegram-bot-api accepts a Buffer for sendDocument with
|
|
1488
|
+
// options.filename for the displayed name.
|
|
1489
|
+
await this.bot.sendDocument(
|
|
1490
|
+
chatId,
|
|
1491
|
+
Buffer.from(body, 'utf8'),
|
|
1492
|
+
{ caption: `*${this._escapeMarkdown(agentName)}* — full reply (sent as a file because it\\\'s code\\-heavy).`, parse_mode: 'MarkdownV2' },
|
|
1493
|
+
{ filename, contentType: 'text/plain' }
|
|
1494
|
+
);
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
this.logger?.warn?.('[TelegramService] code-document send failed, falling back to truncated text', { error: error.message });
|
|
1497
|
+
const fallback = body.slice(0, MAX_MESSAGE_LENGTH - 200) + '\n\n… (truncated; see local web UI for full reply)';
|
|
1498
|
+
await this._sendRaw(chatId, `*${this._escapeMarkdown(agentName)}:*\n\n` + this._escapeMarkdown(fallback), { parse_mode: 'MarkdownV2' });
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
async _sendMedia(chatId, media) {
|
|
1503
|
+
if (!this.bot) return;
|
|
1504
|
+
try {
|
|
1505
|
+
const captionMd = media.caption ? this._escapeMarkdown(media.caption) : undefined;
|
|
1506
|
+
if (media.kind === 'image') {
|
|
1507
|
+
await this.bot.sendPhoto(chatId, media.src, {
|
|
1508
|
+
caption: captionMd, parse_mode: 'MarkdownV2',
|
|
1509
|
+
});
|
|
1510
|
+
} else if (media.kind === 'document') {
|
|
1511
|
+
await this.bot.sendDocument(chatId, media.src, {
|
|
1512
|
+
caption: captionMd, parse_mode: 'MarkdownV2',
|
|
1513
|
+
});
|
|
749
1514
|
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
await this._send(chatId, header + this._escapeMarkdown(content.slice(0, MAX_MESSAGE_LENGTH - 100) + '\n\n… (truncated)'));
|
|
1515
|
+
} catch (error) {
|
|
1516
|
+
this.logger?.warn?.('[TelegramService] media send failed', { error: error.message, kind: media.kind });
|
|
753
1517
|
}
|
|
754
1518
|
}
|
|
755
1519
|
|
|
756
|
-
//
|
|
1520
|
+
// ── Prompt Relay ───────────────────────────────────────────────────
|
|
757
1521
|
|
|
758
1522
|
async _relayPromptRequest(message) {
|
|
759
|
-
if (
|
|
760
|
-
|
|
1523
|
+
if (this.chats.size === 0) return;
|
|
761
1524
|
const data = message.data || message;
|
|
762
1525
|
const requestId = data.requestId;
|
|
763
1526
|
const agentId = data.agentId;
|
|
764
1527
|
const questions = data.questions || [];
|
|
765
|
-
|
|
766
1528
|
if (!requestId || questions.length === 0) return;
|
|
767
1529
|
|
|
768
1530
|
const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
|
|
769
1531
|
const agentName = agent?.name || agentId;
|
|
770
1532
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1533
|
+
// Fan out to every chat that has this agent active.
|
|
1534
|
+
for (const [chatId, state] of this.chats.entries()) {
|
|
1535
|
+
if (!state.activeAgentIds.has(agentId)) continue;
|
|
1536
|
+
|
|
1537
|
+
this.pendingRelays.set(requestId, { type: 'user_prompt', agentId, chatId, questions, timestamp: Date.now() });
|
|
1538
|
+
|
|
1539
|
+
for (const q of questions) {
|
|
1540
|
+
let text = `🔔 *${this._escapeMarkdown(agentName)}* needs your input:\n\n`;
|
|
1541
|
+
text += this._escapeMarkdown(q.question) + '\n';
|
|
1542
|
+
const options = q.options || [];
|
|
1543
|
+
if (options.length > 0) {
|
|
1544
|
+
const buttons = options.map((opt, i) => ([{
|
|
1545
|
+
text: opt.label,
|
|
1546
|
+
callback_data: `prompt_reply:${requestId}:${i}`,
|
|
1547
|
+
}]));
|
|
1548
|
+
await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
|
|
1549
|
+
} else {
|
|
1550
|
+
text += '\n_Type your reply:_';
|
|
1551
|
+
this.replyContext.set(chatId, { type: 'user_prompt', requestId, agentId });
|
|
1552
|
+
await this._send(chatId, text);
|
|
1553
|
+
}
|
|
788
1554
|
}
|
|
789
|
-
}
|
|
790
1555
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1556
|
+
// Two timers per request:
|
|
1557
|
+
// - A 3-min reminder ("still waiting")
|
|
1558
|
+
// - A 10-min hard timeout ("expired — please re-trigger the action")
|
|
1559
|
+
const reminderId = setTimeout(() => {
|
|
1560
|
+
if (this.pendingRelays.has(requestId)) {
|
|
1561
|
+
this._send(chatId, this._escapeMarkdown(`⏰ Reminder: ${agentName} is still waiting for your input.`)).catch(() => {});
|
|
1562
|
+
}
|
|
1563
|
+
}, PROMPT_REMINDER_MS);
|
|
1564
|
+
const timeoutId = setTimeout(() => {
|
|
1565
|
+
if (this.pendingRelays.has(requestId)) {
|
|
1566
|
+
this.pendingRelays.delete(requestId);
|
|
1567
|
+
this.replyContext.delete(chatId);
|
|
1568
|
+
this._send(chatId, this._escapeMarkdown(`⏱ Prompt from ${agentName} expired without a reply. Re-run the action if you still need it.`)).catch(() => {});
|
|
1569
|
+
}
|
|
1570
|
+
}, PROMPT_TIMEOUT_MS);
|
|
1571
|
+
const existing = this.pendingRelays.get(requestId);
|
|
1572
|
+
if (existing) {
|
|
1573
|
+
existing.reminderId = reminderId;
|
|
1574
|
+
existing.timeoutId = timeoutId;
|
|
795
1575
|
}
|
|
796
|
-
}
|
|
1576
|
+
}
|
|
797
1577
|
}
|
|
798
1578
|
|
|
799
1579
|
async _relayCredentialRequest(message) {
|
|
800
|
-
if (
|
|
801
|
-
|
|
1580
|
+
if (this.chats.size === 0) return;
|
|
802
1581
|
const data = message.data || message;
|
|
803
1582
|
const requestId = data.requestId;
|
|
804
1583
|
const agentId = data.agentId;
|
|
805
|
-
|
|
806
1584
|
const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
|
|
807
1585
|
const agentName = agent?.name || agentId;
|
|
808
1586
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1587
|
+
for (const [chatId, state] of this.chats.entries()) {
|
|
1588
|
+
if (!state.activeAgentIds.has(agentId)) continue;
|
|
1589
|
+
this.pendingRelays.set(requestId, { type: 'credential', agentId, chatId, timestamp: Date.now() });
|
|
1590
|
+
this.replyContext.set(chatId, { type: 'credential', requestId, agentId });
|
|
1591
|
+
const text = `🔐 *${this._escapeMarkdown(agentName)}* needs credentials:\n\n` +
|
|
1592
|
+
this._escapeMarkdown(data.message || 'Please provide the requested credentials.') +
|
|
1593
|
+
'\n\n_Type your reply:_';
|
|
1594
|
+
await this._send(chatId, text);
|
|
1595
|
+
|
|
1596
|
+
// Timeout for credential requests too.
|
|
1597
|
+
const timeoutId = setTimeout(() => {
|
|
1598
|
+
if (this.pendingRelays.has(requestId)) {
|
|
1599
|
+
this.pendingRelays.delete(requestId);
|
|
1600
|
+
this.replyContext.delete(chatId);
|
|
1601
|
+
this._send(chatId, this._escapeMarkdown(`⏱ Credential request from ${agentName} expired. Re-trigger if needed.`)).catch(() => {});
|
|
1602
|
+
}
|
|
1603
|
+
}, PROMPT_TIMEOUT_MS);
|
|
1604
|
+
const existing = this.pendingRelays.get(requestId);
|
|
1605
|
+
if (existing) existing.timeoutId = timeoutId;
|
|
1606
|
+
}
|
|
817
1607
|
}
|
|
818
1608
|
|
|
819
1609
|
async _handlePromptReply(msg) {
|
|
820
|
-
|
|
1610
|
+
const chatId = String(msg.chat.id);
|
|
1611
|
+
const ctx = this.replyContext.get(chatId);
|
|
1612
|
+
if (!ctx) return;
|
|
821
1613
|
|
|
822
|
-
const { type, requestId
|
|
1614
|
+
const { type, requestId } = ctx;
|
|
823
1615
|
const text = msg.text?.trim();
|
|
824
1616
|
if (!text) return;
|
|
825
1617
|
|
|
826
|
-
this.replyContext
|
|
1618
|
+
this.replyContext.delete(chatId);
|
|
1619
|
+
const relay = this.pendingRelays.get(requestId);
|
|
1620
|
+
if (relay) {
|
|
1621
|
+
if (relay.reminderId) clearTimeout(relay.reminderId);
|
|
1622
|
+
if (relay.timeoutId) clearTimeout(relay.timeoutId);
|
|
1623
|
+
}
|
|
827
1624
|
this.pendingRelays.delete(requestId);
|
|
828
1625
|
|
|
829
1626
|
try {
|
|
830
1627
|
if (type === 'user_prompt' && this.webSocketManager) {
|
|
831
|
-
// Submit prompt response via the same path the web UI uses
|
|
832
1628
|
this.webSocketManager._handleUserPromptResult?.({
|
|
833
|
-
requestId,
|
|
834
|
-
answers: { default: text }
|
|
1629
|
+
requestId, answers: { default: text },
|
|
835
1630
|
});
|
|
836
1631
|
} else if (type === 'credential' && this.webSocketManager) {
|
|
837
1632
|
this.webSocketManager._handleCredentialResponse?.({
|
|
838
|
-
requestId,
|
|
839
|
-
credentials: { value: text },
|
|
840
|
-
saveForFuture: false
|
|
1633
|
+
requestId, credentials: { value: text }, saveForFuture: false,
|
|
841
1634
|
});
|
|
842
1635
|
}
|
|
843
|
-
|
|
844
1636
|
await this._send(msg.chat.id, this._escapeMarkdown('✅ Response submitted.'));
|
|
845
1637
|
} catch (error) {
|
|
846
1638
|
await this._send(msg.chat.id, this._escapeMarkdown(`❌ Failed to submit: ${error.message}`));
|
|
@@ -850,122 +1642,167 @@ class TelegramService {
|
|
|
850
1642
|
async _submitPromptReply(requestId, answerIndex) {
|
|
851
1643
|
const relay = this.pendingRelays.get(requestId);
|
|
852
1644
|
if (!relay) return;
|
|
853
|
-
|
|
1645
|
+
if (relay.reminderId) clearTimeout(relay.reminderId);
|
|
1646
|
+
if (relay.timeoutId) clearTimeout(relay.timeoutId);
|
|
854
1647
|
this.pendingRelays.delete(requestId);
|
|
855
|
-
this.replyContext
|
|
1648
|
+
this.replyContext.delete(String(relay.chatId));
|
|
856
1649
|
|
|
857
1650
|
try {
|
|
858
|
-
const question = relay.questions[0];
|
|
1651
|
+
const question = relay.questions?.[0];
|
|
859
1652
|
const option = question?.options?.[parseInt(answerIndex)];
|
|
860
1653
|
const answer = option?.label || String(answerIndex);
|
|
861
|
-
|
|
862
1654
|
if (this.webSocketManager?._handleUserPromptResult) {
|
|
863
1655
|
this.webSocketManager._handleUserPromptResult({
|
|
864
|
-
requestId,
|
|
865
|
-
answers: { [question.question]: answer }
|
|
1656
|
+
requestId, answers: { [question.question]: answer },
|
|
866
1657
|
});
|
|
867
1658
|
}
|
|
868
|
-
|
|
869
|
-
await this._send(this.chatId, this._escapeMarkdown(`✅ Selected: ${answer}`));
|
|
1659
|
+
await this._send(relay.chatId, this._escapeMarkdown(`✅ Selected: ${answer}`));
|
|
870
1660
|
} catch (error) {
|
|
871
|
-
await this._send(
|
|
1661
|
+
await this._send(relay.chatId, this._escapeMarkdown(`❌ Failed: ${error.message}`));
|
|
872
1662
|
}
|
|
873
1663
|
}
|
|
874
1664
|
|
|
875
|
-
//
|
|
1665
|
+
// ── Notification Batching ──────────────────────────────────────────
|
|
876
1666
|
|
|
877
|
-
_queueNotification(text) {
|
|
878
|
-
this.notificationQueue.
|
|
879
|
-
if (!this.
|
|
880
|
-
|
|
1667
|
+
_queueNotification(chatId, text) {
|
|
1668
|
+
let q = this.notificationQueue.get(chatId);
|
|
1669
|
+
if (!q) { q = []; this.notificationQueue.set(chatId, q); }
|
|
1670
|
+
q.push(text);
|
|
1671
|
+
if (!this.notificationTimers.has(chatId)) {
|
|
1672
|
+
this.notificationTimers.set(chatId, setTimeout(() => this._flushNotifications(chatId), NOTIFICATION_BATCH_INTERVAL_MS));
|
|
881
1673
|
}
|
|
882
1674
|
}
|
|
883
1675
|
|
|
884
|
-
async _flushNotifications() {
|
|
885
|
-
this.
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const combined =
|
|
890
|
-
|
|
1676
|
+
async _flushNotifications(chatId) {
|
|
1677
|
+
this.notificationTimers.delete(chatId);
|
|
1678
|
+
const q = this.notificationQueue.get(chatId) || [];
|
|
1679
|
+
this.notificationQueue.set(chatId, []);
|
|
1680
|
+
if (q.length === 0) return;
|
|
1681
|
+
const combined = q.join('\n\n');
|
|
891
1682
|
if (combined.length <= MAX_MESSAGE_LENGTH) {
|
|
892
|
-
await this._send(
|
|
1683
|
+
await this._send(chatId, combined);
|
|
893
1684
|
} else {
|
|
894
|
-
await this._send(
|
|
895
|
-
? `\n\n_…and ${messages.length - 1} more events_`
|
|
896
|
-
: ''));
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
_clearNotificationTimer() {
|
|
901
|
-
if (this.notificationTimer) {
|
|
902
|
-
clearTimeout(this.notificationTimer);
|
|
903
|
-
this.notificationTimer = null;
|
|
1685
|
+
await this._send(chatId, q[0] + `\n\n_…and ${q.length - 1} more events_`);
|
|
904
1686
|
}
|
|
905
|
-
this.notificationQueue = [];
|
|
906
1687
|
}
|
|
907
1688
|
|
|
908
|
-
//
|
|
1689
|
+
// ── Telegram API Helpers ───────────────────────────────────────────
|
|
909
1690
|
|
|
910
1691
|
async _send(chatId, text, options = {}) {
|
|
1692
|
+
return this._sendRaw(chatId, text, { parse_mode: 'MarkdownV2', ...options });
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
async _sendRaw(chatId, text, options) {
|
|
911
1696
|
if (!this.bot || !chatId) return;
|
|
912
1697
|
try {
|
|
913
|
-
return await this.bot.sendMessage(chatId, text,
|
|
914
|
-
parse_mode: 'MarkdownV2',
|
|
915
|
-
...options
|
|
916
|
-
});
|
|
1698
|
+
return await this.bot.sendMessage(chatId, text, options);
|
|
917
1699
|
} catch (error) {
|
|
918
|
-
// Fallback:
|
|
919
|
-
|
|
1700
|
+
// Fallback path: strip formatting and retry plain. This catches
|
|
1701
|
+
// MarkdownV2-escape gaps the regex doesn't cover.
|
|
1702
|
+
this.logger?.warn('[TelegramService] formatted send failed, retrying plain', { error: error.message });
|
|
920
1703
|
try {
|
|
921
|
-
|
|
1704
|
+
const plainOpts = { ...options };
|
|
1705
|
+
delete plainOpts.parse_mode;
|
|
1706
|
+
return await this.bot.sendMessage(
|
|
1707
|
+
chatId,
|
|
1708
|
+
String(text).replace(/[\\*_`\[\]()~>#+\-=|{}.!]/g, ''),
|
|
1709
|
+
plainOpts
|
|
1710
|
+
);
|
|
922
1711
|
} catch (e2) {
|
|
923
|
-
this.logger?.error('[TelegramService]
|
|
1712
|
+
this.logger?.error('[TelegramService] send failed', { error: e2.message });
|
|
924
1713
|
}
|
|
925
1714
|
}
|
|
926
1715
|
}
|
|
927
1716
|
|
|
928
1717
|
async sendPhoto(chatId, photoPath, caption = '') {
|
|
929
1718
|
if (!this.bot || !chatId) return;
|
|
930
|
-
try {
|
|
931
|
-
|
|
932
|
-
} catch (error) {
|
|
933
|
-
this.logger?.error('[TelegramService] Send photo failed', { error: error.message });
|
|
934
|
-
}
|
|
1719
|
+
try { return await this.bot.sendPhoto(chatId, photoPath, { caption }); }
|
|
1720
|
+
catch (error) { this.logger?.error('[TelegramService] sendPhoto failed', { error: error.message }); }
|
|
935
1721
|
}
|
|
936
1722
|
|
|
937
1723
|
async sendDocument(chatId, docPath, caption = '') {
|
|
938
1724
|
if (!this.bot || !chatId) return;
|
|
939
|
-
try {
|
|
940
|
-
|
|
941
|
-
} catch (error) {
|
|
942
|
-
this.logger?.error('[TelegramService] Send document failed', { error: error.message });
|
|
943
|
-
}
|
|
1725
|
+
try { return await this.bot.sendDocument(chatId, docPath, { caption }); }
|
|
1726
|
+
catch (error) { this.logger?.error('[TelegramService] sendDocument failed', { error: error.message }); }
|
|
944
1727
|
}
|
|
945
1728
|
|
|
946
1729
|
async sendTestMessage() {
|
|
947
|
-
if (
|
|
948
|
-
|
|
1730
|
+
if (this.chats.size === 0) {
|
|
1731
|
+
throw new Error('No chats registered. Send /start from Telegram first.');
|
|
1732
|
+
}
|
|
1733
|
+
for (const chatId of this.chats.keys()) {
|
|
1734
|
+
await this._send(chatId, this._escapeMarkdown('✅ Loxia Autopilot — test message received!'));
|
|
1735
|
+
}
|
|
949
1736
|
}
|
|
950
1737
|
|
|
951
|
-
//
|
|
1738
|
+
// ── Markdown Escaping ──────────────────────────────────────────────
|
|
952
1739
|
|
|
953
1740
|
_escapeMarkdown(text) {
|
|
954
1741
|
if (!text) return '';
|
|
955
|
-
// Escape MarkdownV2 special chars
|
|
956
1742
|
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, '\\$1');
|
|
957
1743
|
}
|
|
958
1744
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1745
|
+
/**
|
|
1746
|
+
* Tagged-template helper for MarkdownV2 messages with safe interpolation.
|
|
1747
|
+
*
|
|
1748
|
+
* The LITERAL parts of the template (the bits between `${...}`) are
|
|
1749
|
+
* passed through unchanged — author writes raw MarkdownV2 there
|
|
1750
|
+
* (e.g. `*bold*`, `` `code` ``, `_italic_`, with `\.` / `\!` etc.
|
|
1751
|
+
* spelled out where Telegram requires).
|
|
1752
|
+
*
|
|
1753
|
+
* The INTERPOLATED parts (`${value}`) are escaped automatically so a
|
|
1754
|
+
* variable containing `_` or `*` can't accidentally break formatting
|
|
1755
|
+
* or inject markup.
|
|
1756
|
+
*
|
|
1757
|
+
* The original `_escapeMarkdown(entireString)` pattern that wrapped
|
|
1758
|
+
* a hand-written MarkdownV2 string in escapes is the bug we keep
|
|
1759
|
+
* hitting — every intended `*bold*` ended up as literal `*bold*`.
|
|
1760
|
+
*
|
|
1761
|
+
* @example
|
|
1762
|
+
* const text = this._md`*Welcome* ${userName}\\. Use /help for commands\\.`;
|
|
1763
|
+
*
|
|
1764
|
+
* @returns {string} ready to pass to this._send() with MarkdownV2 parse_mode.
|
|
1765
|
+
*/
|
|
1766
|
+
_md(strings, ...values) {
|
|
1767
|
+
let out = '';
|
|
1768
|
+
for (let i = 0; i < strings.length; i++) {
|
|
1769
|
+
out += strings[i];
|
|
1770
|
+
if (i < values.length) out += this._escapeMarkdown(String(values[i] ?? ''));
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// ── Public bridge introspection ────────────────────────────────────
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
* Bridged channels for an agent: one alias per chat the agent has
|
|
1779
|
+
* been addressed from. The scheduler injects the `<external to="…">`
|
|
1780
|
+
* prompt guidance based on this list.
|
|
1781
|
+
*/
|
|
1782
|
+
getBridgedChannels(agentId) {
|
|
1783
|
+
const out = [];
|
|
1784
|
+
for (const [chatId, state] of this.chats.entries()) {
|
|
1785
|
+
if (state.activeAgentIds.has(agentId)) {
|
|
1786
|
+
out.push({
|
|
1787
|
+
alias: `telegram:chat-${chatId}`,
|
|
1788
|
+
label: state.title ? `Telegram > ${state.title}` : 'Telegram chat',
|
|
1789
|
+
});
|
|
966
1790
|
}
|
|
967
|
-
|
|
968
|
-
|
|
1791
|
+
}
|
|
1792
|
+
if (out.length > 0) {
|
|
1793
|
+
// Also expose the bare `telegram` alias for backward compat
|
|
1794
|
+
// with agents that don't know about per-chat addressing.
|
|
1795
|
+
out.push({ alias: 'telegram', label: 'Telegram (any chat)' });
|
|
1796
|
+
}
|
|
1797
|
+
return out;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
isAgentBridged(agentId) {
|
|
1801
|
+
if (!agentId || this.status !== TELEGRAM_STATUS.CONNECTED) return false;
|
|
1802
|
+
for (const state of this.chats.values()) {
|
|
1803
|
+
if (state.activeAgentIds.has(agentId)) return true;
|
|
1804
|
+
}
|
|
1805
|
+
return false;
|
|
969
1806
|
}
|
|
970
1807
|
}
|
|
971
1808
|
|
|
@@ -979,5 +1816,10 @@ export function getTelegramService(logger = null) {
|
|
|
979
1816
|
return instance;
|
|
980
1817
|
}
|
|
981
1818
|
|
|
1819
|
+
// Tests need to drop the singleton between cases.
|
|
1820
|
+
export function _resetTelegramSingletonForTests() {
|
|
1821
|
+
instance = null;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
982
1824
|
export { TelegramService, TELEGRAM_STATUS };
|
|
983
1825
|
export default TelegramService;
|