@mazees/mark 5.0.0

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.
Files changed (227) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +209 -0
  3. package/bin/mark.js +27 -0
  4. package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
  5. package/out/renderer/assets/index-G2fADWO-.js +4768 -0
  6. package/out/renderer/assets/index-f3mVse3c.css +1 -0
  7. package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
  8. package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  9. package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  10. package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
  11. package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
  12. package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
  13. package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
  14. package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
  15. package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
  16. package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  17. package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  18. package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  19. package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  20. package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  21. package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  22. package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  23. package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  24. package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  25. package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  26. package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
  27. package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
  28. package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
  29. package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
  30. package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
  31. package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
  32. package/out/renderer/favicon.ico +0 -0
  33. package/out/renderer/icon-256.png +0 -0
  34. package/out/renderer/icon.svg +1 -0
  35. package/out/renderer/index.html +25 -0
  36. package/out/renderer/manifest.json +25 -0
  37. package/out/renderer/robot-simulator.html +1640 -0
  38. package/package.json +106 -0
  39. package/src/cli/index.js +332 -0
  40. package/src/cli/monitor.js +413 -0
  41. package/src/cli/theme.js +347 -0
  42. package/src/launcher/MarkTray.cs +227 -0
  43. package/src/main/browser-agent.js +1132 -0
  44. package/src/main/git-service.js +90 -0
  45. package/src/main/google/google-calendar.js +69 -0
  46. package/src/main/google/google-drive.js +263 -0
  47. package/src/main/google/google-gmail.js +131 -0
  48. package/src/main/google/google-service.js +241 -0
  49. package/src/main/node-tools.js +23 -0
  50. package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
  51. package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
  52. package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
  53. package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
  54. package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
  55. package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
  56. package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
  57. package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
  58. package/src/main/plugins/plugin-loader.js +306 -0
  59. package/src/main/skills/skill-manager.js +265 -0
  60. package/src/main/syntax-validator.js +455 -0
  61. package/src/main/task-daemon.js +117 -0
  62. package/src/main/telegram/telegram-service.js +835 -0
  63. package/src/main/tools/browser-tools.js +516 -0
  64. package/src/main/tools/file-tools.js +1126 -0
  65. package/src/main/tools/git-tools.js +79 -0
  66. package/src/main/tools/google-tools.js +566 -0
  67. package/src/main/tools/index.js +36 -0
  68. package/src/main/tools/media-tools.js +42 -0
  69. package/src/main/tools/system-tools.js +295 -0
  70. package/src/main/tools/task-tools.js +73 -0
  71. package/src/main/tools/telegram-tools.js +101 -0
  72. package/src/renderer/index.html +24 -0
  73. package/src/renderer/public/favicon.ico +0 -0
  74. package/src/renderer/public/icon-256.png +0 -0
  75. package/src/renderer/public/icon.svg +1 -0
  76. package/src/renderer/public/manifest.json +25 -0
  77. package/src/renderer/public/robot-simulator.html +1640 -0
  78. package/src/renderer/src/App.jsx +304 -0
  79. package/src/renderer/src/api/ai/awareness.js +294 -0
  80. package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
  81. package/src/renderer/src/api/ai/contextCompactor.js +129 -0
  82. package/src/renderer/src/api/ai/contextManager.js +1130 -0
  83. package/src/renderer/src/api/ai/core.js +167 -0
  84. package/src/renderer/src/api/ai/index.js +4 -0
  85. package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
  86. package/src/renderer/src/api/ai/persona.js +115 -0
  87. package/src/renderer/src/api/ai/planning.js +317 -0
  88. package/src/renderer/src/api/ai/relationship.js +177 -0
  89. package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
  90. package/src/renderer/src/api/ai/tools.js +167 -0
  91. package/src/renderer/src/api/ai/utils.js +359 -0
  92. package/src/renderer/src/api/db.js +1013 -0
  93. package/src/renderer/src/api/oramaStore.js +592 -0
  94. package/src/renderer/src/api/ragPipeline.js +87 -0
  95. package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
  96. package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
  97. package/src/renderer/src/api/subagent/subagentStore.js +173 -0
  98. package/src/renderer/src/api/taskExecutor.js +18 -0
  99. package/src/renderer/src/api/taskStore.js +315 -0
  100. package/src/renderer/src/api/tools/core-tools.js +707 -0
  101. package/src/renderer/src/api/tools/index.js +151 -0
  102. package/src/renderer/src/api/turnPairMigrator.js +200 -0
  103. package/src/renderer/src/api/vectorMemory.js +136 -0
  104. package/src/renderer/src/api/wakeWord.js +101 -0
  105. package/src/renderer/src/api/web-bridge.js +885 -0
  106. package/src/renderer/src/api/webSpeech.js +178 -0
  107. package/src/renderer/src/api/workspaceRag.js +76 -0
  108. package/src/renderer/src/assets/electron.svg +10 -0
  109. package/src/renderer/src/assets/icon.svg +1 -0
  110. package/src/renderer/src/assets/main.css +589 -0
  111. package/src/renderer/src/assets/music-cover.png +0 -0
  112. package/src/renderer/src/assets/wavy-lines.svg +25 -0
  113. package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
  114. package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
  115. package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
  116. package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
  117. package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
  118. package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
  119. package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
  120. package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
  121. package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
  122. package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
  123. package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
  124. package/src/renderer/src/components/Chat/index.js +11 -0
  125. package/src/renderer/src/components/ChatList.jsx +434 -0
  126. package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
  127. package/src/renderer/src/components/Versions.jsx +15 -0
  128. package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
  129. package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
  130. package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
  131. package/src/renderer/src/components/config/CameraSection.jsx +100 -0
  132. package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
  133. package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
  134. package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
  135. package/src/renderer/src/components/config/index.js +5 -0
  136. package/src/renderer/src/components/core/Avatar.jsx +2197 -0
  137. package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
  138. package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
  139. package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
  140. package/src/renderer/src/components/core/HoloCard.jsx +64 -0
  141. package/src/renderer/src/components/core/InputBar.jsx +1346 -0
  142. package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
  143. package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
  144. package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
  145. package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
  146. package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
  147. package/src/renderer/src/components/core/native-skills.js +43 -0
  148. package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
  149. package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
  150. package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
  151. package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
  152. package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
  153. package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
  154. package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
  155. package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
  156. package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
  157. package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
  158. package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
  159. package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
  160. package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
  161. package/src/renderer/src/contexts/ChatContext.jsx +61 -0
  162. package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
  163. package/src/renderer/src/data/faqData.js +202 -0
  164. package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
  165. package/src/renderer/src/hooks/agent/index.js +4 -0
  166. package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
  167. package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
  168. package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
  169. package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
  170. package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
  171. package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
  172. package/src/renderer/src/hooks/useAwareness.js +289 -0
  173. package/src/renderer/src/hooks/useChatArchiver.js +52 -0
  174. package/src/renderer/src/hooks/useConfirm.jsx +68 -0
  175. package/src/renderer/src/hooks/useMarkAgent.js +454 -0
  176. package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
  177. package/src/renderer/src/hooks/useVAD.js +511 -0
  178. package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
  179. package/src/renderer/src/main.jsx +12 -0
  180. package/src/renderer/src/pages/ChatStudio.jsx +922 -0
  181. package/src/renderer/src/pages/Configuration.jsx +360 -0
  182. package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
  183. package/src/renderer/src/pages/Guidebook.jsx +917 -0
  184. package/src/renderer/src/pages/Knowledge.jsx +241 -0
  185. package/src/renderer/src/pages/MarkHome.jsx +491 -0
  186. package/src/renderer/src/pages/NeuralCore.jsx +786 -0
  187. package/src/renderer/src/pages/Plugins.jsx +630 -0
  188. package/src/renderer/src/pages/SkillEditor.jsx +380 -0
  189. package/src/renderer/src/pages/Skills.jsx +219 -0
  190. package/src/renderer/src/pages/Subagents.jsx +342 -0
  191. package/src/renderer/src/pages/TelegramBot.jsx +343 -0
  192. package/src/renderer/src/utils/chatContentParser.js +203 -0
  193. package/src/renderer/src/utils/toolClusters.js +201 -0
  194. package/src/server/bin/sha3_wasm_bg.wasm +0 -0
  195. package/src/server/config-manager.js +57 -0
  196. package/src/server/index.js +195 -0
  197. package/src/server/launcher.js +87 -0
  198. package/src/server/memory/db-store.js +758 -0
  199. package/src/server/memory/orama-store.js +177 -0
  200. package/src/server/memory/vector-engine.js +109 -0
  201. package/src/server/routes/ai.routes.js +280 -0
  202. package/src/server/routes/awareness.routes.js +62 -0
  203. package/src/server/routes/chat.routes.js +61 -0
  204. package/src/server/routes/config.routes.js +41 -0
  205. package/src/server/routes/index.js +35 -0
  206. package/src/server/routes/integrations.routes.js +116 -0
  207. package/src/server/routes/memory.routes.js +256 -0
  208. package/src/server/routes/plugins.routes.js +72 -0
  209. package/src/server/routes/skills.routes.js +139 -0
  210. package/src/server/routes/subagents.routes.js +70 -0
  211. package/src/server/routes/tasks.routes.js +175 -0
  212. package/src/server/routes/workspace.routes.js +127 -0
  213. package/src/server/services/ai/ai-utils.js +161 -0
  214. package/src/server/services/ai/openai-provider.js +757 -0
  215. package/src/server/services/ai/web-provider.js +729 -0
  216. package/src/server/services/ai-bridge.js +46 -0
  217. package/src/server/services/deepseek-web.js +956 -0
  218. package/src/server/services/gemini-web.js +170 -0
  219. package/src/server/services/notification-service.js +91 -0
  220. package/src/server/services/updater.js +125 -0
  221. package/src/server/tools/awareness-tracker.js +292 -0
  222. package/src/server/tools/group-tools.js +1164 -0
  223. package/src/server/tools/media-tools.js +244 -0
  224. package/src/server/tools/pc-agent.js +327 -0
  225. package/src/server/tools/screen-service.js +51 -0
  226. package/src/server/ws-hub.js +168 -0
  227. package/src/shared/parsers/mark-tag-parser.js +275 -0
@@ -0,0 +1,835 @@
1
+ import { Telegraf } from 'telegraf'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import os from 'os'
5
+ import yts from 'yt-search'
6
+ import { execFile } from 'child_process'
7
+ import ffmpeg from 'ffmpeg-static'
8
+ import https from 'https'
9
+ import { abortAllFetches, activeAbortControllers } from '../../server/services/ai-bridge.js'
10
+ import { dbStore } from '../../server/memory/db-store.js'
11
+ import { wsHub } from '../../server/ws-hub.js'
12
+
13
+ let bot = null
14
+ let currentStatus = 'disconnected'
15
+ export const uiMessageHistory = []
16
+ const MAX_UI_HISTORY = 100
17
+ const pendingRequestsMap = new Map()
18
+ const pendingBroadcastQueue = []
19
+
20
+ const CHAT_IDS_FILE = path.join(os.homedir(), '.config', 'mark-agent', 'telegram-chats.json')
21
+ const adminChatIdsSet = new Set()
22
+ const usernameToChatIdMap = new Map()
23
+
24
+ const agent = new https.Agent({
25
+ keepAlive: true,
26
+ keepAliveMsecs: 10000,
27
+ rejectUnauthorized: false
28
+ })
29
+
30
+ function getTelegramConfig() {
31
+ try {
32
+ const cfgs = dbStore.config.getAll()
33
+ const cfg = cfgs[0] || {}
34
+ // SQLite config table stores settings inside row.data object
35
+ if (cfg.data && typeof cfg.data === 'object') {
36
+ return { ...cfg, ...cfg.data }
37
+ }
38
+ return cfg
39
+ } catch (_) {
40
+ return {}
41
+ }
42
+ }
43
+
44
+ export const getConnectionStatus = () => {
45
+ return { status: currentStatus }
46
+ }
47
+
48
+ const updateStatus = (status) => {
49
+ currentStatus = status
50
+ wsHub.broadcast('tg:connection', status)
51
+ if (status === 'connected') {
52
+ setTimeout(flushPendingBroadcasts, 500)
53
+ }
54
+ }
55
+
56
+ const loadSavedChatIds = () => {
57
+ try {
58
+ if (fs.existsSync(CHAT_IDS_FILE)) {
59
+ const data = JSON.parse(fs.readFileSync(CHAT_IDS_FILE, 'utf8'))
60
+ if (Array.isArray(data.chatIds)) {
61
+ data.chatIds.forEach((id) => adminChatIdsSet.add(String(id)))
62
+ }
63
+ if (data.usernameMap && typeof data.usernameMap === 'object') {
64
+ Object.entries(data.usernameMap).forEach(([user, id]) => {
65
+ usernameToChatIdMap.set(user, String(id))
66
+ })
67
+ }
68
+ }
69
+ } catch (e) {
70
+ console.error('[Telegram] Gagal memuat saved chat IDs:', e)
71
+ }
72
+ }
73
+
74
+ const saveChatIdsToFile = () => {
75
+ try {
76
+ const dir = path.dirname(CHAT_IDS_FILE)
77
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
78
+ const data = {
79
+ chatIds: Array.from(adminChatIdsSet),
80
+ usernameMap: Object.fromEntries(usernameToChatIdMap)
81
+ }
82
+ fs.writeFileSync(CHAT_IDS_FILE, JSON.stringify(data, null, 2), 'utf8')
83
+ } catch (e) {
84
+ console.error('[Telegram] Gagal menyimpan chat IDs ke file:', e)
85
+ }
86
+ }
87
+
88
+ loadSavedChatIds()
89
+
90
+ const formatMarkdownToTelegramHTML = (text) => {
91
+ if (!text) return ''
92
+ let html = String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
93
+
94
+ html = html.replace(/```([a-z0-9-]*)\n([\s\S]*?)```/gi, (_match, lang, code) => {
95
+ return lang
96
+ ? `<pre><code class="language-${lang}">${code}</code></pre>`
97
+ : `<pre><code>${code}</code></pre>`
98
+ })
99
+
100
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
101
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
102
+ html = html.replace(/\*([^*]+)\*/g, '<i>$1</i>')
103
+ html = html.replace(/~~([^~]+)~~/g, '<s>$1</s>')
104
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
105
+ html = html.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>')
106
+
107
+ return html
108
+ }
109
+
110
+ export const stopTelegramBot = () => {
111
+ if (bot) {
112
+ try {
113
+ bot.stop('BOT_STOPPED')
114
+ } catch (e) {
115
+ console.error('[Telegram] Error stopping bot:', e)
116
+ }
117
+ bot = null
118
+ }
119
+ updateStatus('disconnected')
120
+ }
121
+
122
+ export const startTelegramBot = async (token) => {
123
+ if (!token || !token.trim()) {
124
+ console.error('[Telegram] Token kosong')
125
+ updateStatus('disconnected')
126
+ return
127
+ }
128
+
129
+ if (bot) {
130
+ stopTelegramBot()
131
+ }
132
+
133
+ updateStatus('connecting')
134
+
135
+ try {
136
+ const config = getTelegramConfig()
137
+ const telegramOpts = { agent }
138
+ if (config.tgApiRoot && config.tgApiRoot.trim()) {
139
+ telegramOpts.apiRoot = config.tgApiRoot.trim()
140
+ }
141
+
142
+ bot = new Telegraf(token.trim(), { telegram: telegramOpts })
143
+
144
+ if (config.tgAdminIds) {
145
+ const ids = config.tgAdminIds
146
+ .split(',')
147
+ .map((s) => s.trim())
148
+ .filter(Boolean)
149
+ ids.forEach((id) => {
150
+ const cleanId = id.replace(/^@/, '')
151
+ if (/^\d+$/.test(cleanId)) {
152
+ adminChatIdsSet.add(cleanId)
153
+ }
154
+ })
155
+ saveChatIdsToFile()
156
+ }
157
+
158
+ bot.command('start', (ctx) => {
159
+ const chatId = String(ctx.chat?.id || ctx.from?.id || '')
160
+ const senderUsername = (ctx.from?.username || '').toLowerCase()
161
+ if (chatId) {
162
+ adminChatIdsSet.add(chatId)
163
+ if (senderUsername) usernameToChatIdMap.set(senderUsername, chatId)
164
+ saveChatIdsToFile()
165
+ }
166
+ ctx.reply('Halo! Saya Mark (AI OS Companion). Bot Telegram ini telah terhubung.')
167
+ })
168
+
169
+ bot.command('info', (ctx) => {
170
+ ctx.reply(
171
+ 'Daftar Perintah MARK:\n\n' +
172
+ '/start - Memulai bot\n' +
173
+ '/info - Menampilkan daftar perintah\n' +
174
+ '/abort - Menghentikan proses AI yang sedang berjalan\n' +
175
+ '/accept - Mengizinkan persetujuan sekali saja\n' +
176
+ '/always - Mengizinkan selamanya untuk path folder ini\n' +
177
+ '/reject - Menolak prompt persetujuan'
178
+ )
179
+ })
180
+
181
+ bot.command('abort', (ctx) => {
182
+ if (activeAbortControllers.size > 0) {
183
+ abortAllFetches()
184
+ ctx.reply('[INFO]: Membatalkan proses AI saat ini...')
185
+ } else {
186
+ ctx.reply('[INFO]: Tidak ada proses AI yang sedang berjalan.')
187
+ }
188
+ })
189
+
190
+ const isApprovalCommand = (cmdText) => {
191
+ const clean = (cmdText || '').trim().toLowerCase()
192
+ return (
193
+ clean === '/accept' ||
194
+ clean === 'accept' ||
195
+ clean === '/izinkan' ||
196
+ clean === 'izinkan' ||
197
+ clean.startsWith('/accept@') ||
198
+ clean === '/always' ||
199
+ clean === 'always' ||
200
+ clean === '/selamanya' ||
201
+ clean === 'selamanya' ||
202
+ clean.startsWith('/always@') ||
203
+ clean === '/reject' ||
204
+ clean === 'reject' ||
205
+ clean === '/tolak' ||
206
+ clean === 'tolak' ||
207
+ clean.startsWith('/reject@') ||
208
+ clean === '/session' ||
209
+ clean === 'session' ||
210
+ clean === '/sesi' ||
211
+ clean === 'sesi' ||
212
+ clean.startsWith('/session@') ||
213
+ clean.startsWith('/sesi@')
214
+ )
215
+ }
216
+
217
+ bot.command('accept', (ctx) => {
218
+ const chatId = String(ctx.chat?.id || ctx.from?.id || '')
219
+ wsHub.broadcast('tg:command-accept', { chatId })
220
+ })
221
+
222
+ bot.command('always', (ctx) => {
223
+ const chatId = String(ctx.chat?.id || ctx.from?.id || '')
224
+ wsHub.broadcast('tg:command-always', { chatId })
225
+ })
226
+
227
+ bot.command('reject', (ctx) => {
228
+ const chatId = String(ctx.chat?.id || ctx.from?.id || '')
229
+ wsHub.broadcast('tg:command-reject', { chatId })
230
+ })
231
+
232
+ bot.command(['session', 'sesi'], (ctx) => {
233
+ const chatId = String(ctx.chat?.id || ctx.from?.id || '')
234
+ wsHub.broadcast('tg:command-session', { chatId })
235
+ })
236
+
237
+ bot.command(['auto', 'automode'], async (ctx) => {
238
+ const arg = (ctx.message?.text || '')
239
+ .replace(/^\/(auto|automode)(@\w+)?/i, '')
240
+ .trim()
241
+ .toLowerCase()
242
+ try {
243
+ const session1 = dbStore.sessions.getById('1') || { id: '1' }
244
+ if (arg === 'on' || arg === '1' || arg === 'enable') {
245
+ session1.is_auto_mode = 1
246
+ session1.updated_at = Date.now()
247
+ dbStore.sessions.insert(session1)
248
+ wsHub.broadcast('session-auto-mode-updated', { sessionId: '1', isAutoMode: true })
249
+ await ctx.reply(
250
+ '[INFO]: Auto Mode untuk Sesi Utama telah DIAKTIFKAN (ON). Semua persetujuan tool akan disetujui otomatis.'
251
+ )
252
+ } else if (arg === 'off' || arg === '0' || arg === 'disable') {
253
+ session1.is_auto_mode = 0
254
+ session1.updated_at = Date.now()
255
+ dbStore.sessions.insert(session1)
256
+ wsHub.broadcast('session-auto-mode-updated', { sessionId: '1', isAutoMode: false })
257
+ await ctx.reply(
258
+ '[INFO]: Auto Mode untuk Sesi Utama telah DINONAKTIFKAN (OFF). Persetujuan tool aktif kembali.'
259
+ )
260
+ } else {
261
+ const current = Boolean(session1.is_auto_mode)
262
+ await ctx.reply(
263
+ `[INFO]: Status Auto Mode Sesi Utama saat ini: ${current ? 'AKTIF (ON)' : 'NONAKTIF (OFF)'}.\nKetik \`/auto on\` untuk mengaktifkan atau \`/auto off\` untuk mematikan.`,
264
+ { parse_mode: 'Markdown' }
265
+ )
266
+ }
267
+ } catch (err) {
268
+ await ctx.reply(`[ERROR]: Gagal memperbarui Auto Mode: ${err.message}`)
269
+ }
270
+ })
271
+
272
+ bot.on('text', async (ctx) => {
273
+ const senderId = String(ctx.from?.id || '')
274
+ const senderName = ctx.from?.first_name
275
+ ? `${ctx.from.first_name} ${ctx.from.last_name || ''}`.trim()
276
+ : ctx.from?.username || senderId
277
+ const chatId = String(ctx.chat?.id || senderId)
278
+ const text = ctx.message?.text || ''
279
+
280
+ const senderUsername = (ctx.from?.username || '').toLowerCase()
281
+ const config = getTelegramConfig()
282
+ const adminList = (config.tgAdminIds || '')
283
+ .split(',')
284
+ .map((item) => item.trim().toLowerCase().replace(/^@/, ''))
285
+ .filter(Boolean)
286
+
287
+ const isAdmin =
288
+ adminList.includes(senderId.toLowerCase()) ||
289
+ (senderUsername && adminList.includes(senderUsername))
290
+
291
+ if (!isAdmin) {
292
+ console.log(`[Telegram] Access denied for user ${senderId} (@${senderUsername})`)
293
+ await ctx.reply('Maaf, kamu belum punya akses ke MARK.')
294
+ return
295
+ }
296
+
297
+ adminChatIdsSet.add(chatId)
298
+ if (senderUsername) usernameToChatIdMap.set(senderUsername, chatId)
299
+ saveChatIdsToFile()
300
+
301
+ // Jika teks adalah perintah approval (/accept, /always, /reject), jangan picu loop planning agent baru
302
+ if (isApprovalCommand(text)) {
303
+ const clean = text.trim().toLowerCase()
304
+ if (
305
+ clean === '/accept' ||
306
+ clean === 'accept' ||
307
+ clean === '/izinkan' ||
308
+ clean === 'izinkan' ||
309
+ clean.startsWith('/accept@')
310
+ ) {
311
+ wsHub.broadcast('tg:command-accept', { chatId })
312
+ } else if (
313
+ clean === '/always' ||
314
+ clean === 'always' ||
315
+ clean === '/selamanya' ||
316
+ clean === 'selamanya' ||
317
+ clean.startsWith('/always@')
318
+ ) {
319
+ wsHub.broadcast('tg:command-always', { chatId })
320
+ } else if (
321
+ clean === '/reject' ||
322
+ clean === 'reject' ||
323
+ clean === '/tolak' ||
324
+ clean === 'tolak' ||
325
+ clean.startsWith('/reject@')
326
+ ) {
327
+ wsHub.broadcast('tg:command-reject', { chatId })
328
+ }
329
+ return
330
+ }
331
+
332
+ const msgId = `${chatId}-${ctx.message.message_id}`
333
+
334
+ const uiMsgPayload = {
335
+ id: msgId,
336
+ chatId: chatId,
337
+ sender: senderName,
338
+ text: text,
339
+ isGroup: ctx.chat?.type !== 'private',
340
+ chatTitle: ctx.chat?.title || senderName,
341
+ time: new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }),
342
+ type: 'incoming'
343
+ }
344
+
345
+ uiMessageHistory.push(uiMsgPayload)
346
+ if (uiMessageHistory.length > MAX_UI_HISTORY) uiMessageHistory.shift()
347
+
348
+ wsHub.broadcast('tg:message', uiMsgPayload)
349
+ wsHub.broadcast('tg:thinking', { sender: senderName, chatId })
350
+
351
+ let loadingMsgId = null
352
+ try {
353
+ const loadingMsg = await ctx.reply('[LOADING]: Sedang diproses...', {
354
+ disable_notification: true
355
+ })
356
+ loadingMsgId = loadingMsg.message_id
357
+ } catch (_) {}
358
+
359
+ pendingRequestsMap.set(msgId, { ctx, chatId, text, loadingMsgId })
360
+ setTimeout(() => pendingRequestsMap.delete(msgId), 300000)
361
+
362
+ const recentHistory = uiMessageHistory
363
+ .filter((m) => m.chatId === chatId)
364
+ .slice(-10)
365
+ .map((m) => ({
366
+ role: m.type === 'incoming' ? 'user' : 'assistant',
367
+ content: m.type === 'incoming' ? m.text : m.reply
368
+ }))
369
+
370
+ wsHub.broadcast('tg:request-agent-execution', {
371
+ text: `[Telegram from ${chatId} - ${senderName}]:\n${text}`,
372
+ isAdmin: true,
373
+ senderName,
374
+ msgId,
375
+ chatId,
376
+ isGroup: ctx.chat?.type !== 'private',
377
+ chatSession: recentHistory
378
+ })
379
+ })
380
+
381
+ bot.on(['document', 'photo'], async (ctx) => {
382
+ const senderId = String(ctx.from?.id || '')
383
+ const senderName = ctx.from?.first_name
384
+ ? `${ctx.from.first_name} ${ctx.from.last_name || ''}`.trim()
385
+ : ctx.from?.username || senderId
386
+ const chatId = String(ctx.chat?.id || senderId)
387
+
388
+ const senderUsername = (ctx.from?.username || '').toLowerCase()
389
+ const config = getTelegramConfig()
390
+ const adminList = (config.tgAdminIds || '')
391
+ .split(',')
392
+ .map((item) => item.trim().toLowerCase().replace(/^@/, ''))
393
+ .filter(Boolean)
394
+ const isAdmin =
395
+ adminList.includes(senderId.toLowerCase()) ||
396
+ (senderUsername && adminList.includes(senderUsername))
397
+
398
+ if (!isAdmin) {
399
+ await ctx.reply('Maaf, kamu belum punya akses ke MARK.')
400
+ return
401
+ }
402
+
403
+ try {
404
+ let fileId = ''
405
+ let originalName = ''
406
+
407
+ if (ctx.message.document) {
408
+ fileId = ctx.message.document.file_id
409
+ originalName = ctx.message.document.file_name || `document_${Date.now()}`
410
+ } else if (ctx.message.photo) {
411
+ const photo = ctx.message.photo[ctx.message.photo.length - 1]
412
+ fileId = photo.file_id
413
+ originalName = `photo_${Date.now()}.jpg`
414
+ }
415
+
416
+ const statusMsg = await ctx.reply(`[INFO]: Sedang mengunduh file ${originalName}...`)
417
+
418
+ const fileUrl = await ctx.telegram.getFileLink(fileId)
419
+ const saveDir = path.join(os.homedir(), 'Documents', 'Mark Workspace', 'Telegram')
420
+ if (!fs.existsSync(saveDir)) fs.mkdirSync(saveDir, { recursive: true })
421
+
422
+ const savePath = path.join(saveDir, originalName)
423
+
424
+ const response = await fetch(fileUrl)
425
+ const buffer = await response.arrayBuffer()
426
+ fs.writeFileSync(savePath, Buffer.from(buffer))
427
+
428
+ await ctx.telegram.editMessageText(
429
+ chatId,
430
+ statusMsg.message_id,
431
+ undefined,
432
+ `[INFO]: Berhasil mengunduh: ${originalName}\n[LOADING]: Sedang diproses...`
433
+ )
434
+
435
+ const caption = ctx.message.caption || ''
436
+ const text = `[FILE TERLAMPIR]: "${savePath}"\n${caption ? `Caption dari user: ${caption}` : 'Silakan baca/analisa file gambar atau dokumen ini jika perlu.'}`
437
+
438
+ const msgId = `${chatId}-${ctx.message.message_id}`
439
+ const uiMsgPayload = {
440
+ id: msgId,
441
+ chatId: chatId,
442
+ sender: senderName,
443
+ text: `[FILE]: Mengirim file: ${originalName}\n${caption}`,
444
+ isGroup: ctx.chat?.type !== 'private',
445
+ chatTitle: ctx.chat?.title || senderName,
446
+ time: new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }),
447
+ type: 'incoming'
448
+ }
449
+
450
+ uiMessageHistory.push(uiMsgPayload)
451
+ if (uiMessageHistory.length > MAX_UI_HISTORY) uiMessageHistory.shift()
452
+
453
+ wsHub.broadcast('tg:message', uiMsgPayload)
454
+ wsHub.broadcast('tg:thinking', { sender: senderName, chatId })
455
+
456
+ pendingRequestsMap.set(msgId, { ctx, chatId, text, loadingMsgId: statusMsg.message_id })
457
+ setTimeout(() => pendingRequestsMap.delete(msgId), 300000)
458
+
459
+ const recentHistory = uiMessageHistory
460
+ .filter((m) => m.chatId === chatId)
461
+ .slice(-10)
462
+ .map((m) => ({
463
+ role: m.type === 'incoming' ? 'user' : 'assistant',
464
+ content: m.type === 'incoming' ? m.text : m.reply
465
+ }))
466
+
467
+ wsHub.broadcast('tg:request-agent-execution', {
468
+ text: `[Telegram from ${chatId} - ${senderName}]:\n${text}`,
469
+ isAdmin: true,
470
+ senderName,
471
+ msgId,
472
+ chatId,
473
+ isGroup: ctx.chat?.type !== 'private',
474
+ chatSession: recentHistory
475
+ })
476
+ } catch (e) {
477
+ console.error('Failed to download file from Telegram:', e)
478
+ ctx.reply(`Gagal mengunduh file: ${e.message}`)
479
+ }
480
+ })
481
+
482
+ bot.catch((err, ctx) => {
483
+ console.error(`[Telegram] Error for ${ctx.updateType}:`, err)
484
+ })
485
+
486
+ bot.launch({ allowedUpdates: ['message'] }).catch((err) => {
487
+ console.error('[Telegram] Polling error:', err)
488
+ bot = null
489
+ updateStatus('disconnected')
490
+ })
491
+ updateStatus('connected')
492
+ console.log('[Telegram] Bot berhasil dijalankan dan mendengarkan pesan.')
493
+ } catch (err) {
494
+ console.error('[Telegram] Gagal menjalankan bot:', err)
495
+ bot = null
496
+ updateStatus('disconnected')
497
+ }
498
+ }
499
+
500
+ export const resolveTelegramChatIds = (chatId = null) => {
501
+ const cleanId = String(chatId || '').trim()
502
+ if (cleanId && /^-?\d+$/.test(cleanId)) {
503
+ return [cleanId]
504
+ }
505
+
506
+ const cleanUser = cleanId.replace(/^@/, '').toLowerCase()
507
+ if (cleanUser && usernameToChatIdMap.has(cleanUser)) {
508
+ return [usernameToChatIdMap.get(cleanUser)]
509
+ }
510
+
511
+ const config = getTelegramConfig()
512
+ const adminInputs = (config.tgAdminIds || '')
513
+ .split(',')
514
+ .map((id) => id.trim().toLowerCase().replace(/^@/, ''))
515
+ .filter(Boolean)
516
+
517
+ const targetChatIds = new Set(adminChatIdsSet)
518
+ for (const input of adminInputs) {
519
+ if (/^-?\d+$/.test(input)) {
520
+ targetChatIds.add(input)
521
+ } else if (usernameToChatIdMap.has(input)) {
522
+ targetChatIds.add(usernameToChatIdMap.get(input))
523
+ }
524
+ }
525
+
526
+ return Array.from(targetChatIds)
527
+ }
528
+
529
+ export const sendTelegramMessage = async (chatId, text) => {
530
+ if (!bot || currentStatus !== 'connected') {
531
+ return { success: false, error: 'Telegram Bot belum terhubung.' }
532
+ }
533
+ const targets = resolveTelegramChatIds(chatId)
534
+ if (targets.length === 0) {
535
+ return { success: false, error: 'Tidak ada ID chat tujuan atau admin Telegram yang terdaftar.' }
536
+ }
537
+
538
+ let lastError = null
539
+ let successCount = 0
540
+ const htmlText = formatMarkdownToTelegramHTML(text)
541
+
542
+ for (const target of targets) {
543
+ try {
544
+ await bot.telegram.sendMessage(target, htmlText, { parse_mode: 'HTML' })
545
+ successCount++
546
+ } catch {
547
+ try {
548
+ await bot.telegram.sendMessage(target, text)
549
+ successCount++
550
+ } catch (fallbackErr) {
551
+ lastError = fallbackErr.message
552
+ }
553
+ }
554
+ }
555
+
556
+ return successCount > 0
557
+ ? { success: true, targets }
558
+ : { success: false, error: lastError || 'Gagal mengirim pesan Telegram.' }
559
+ }
560
+
561
+ export const sendTelegramFile = async (chatId, filePathOrData, caption = '', options = {}) => {
562
+ if (!bot || currentStatus !== 'connected') {
563
+ return { success: false, error: 'Telegram Bot belum terhubung.' }
564
+ }
565
+
566
+ const targets = resolveTelegramChatIds(chatId)
567
+ if (targets.length === 0) {
568
+ return { success: false, error: 'Tidak ada target Telegram yang terdaftar atau valid.' }
569
+ }
570
+
571
+ let fileBuffer = null
572
+ let fileName = 'file'
573
+ let isImage = false
574
+
575
+ const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp']
576
+
577
+ if (
578
+ typeof filePathOrData === 'string' &&
579
+ (filePathOrData.startsWith('data:') ||
580
+ (filePathOrData.length > 500 && !fs.existsSync(filePathOrData)))
581
+ ) {
582
+ const cleanBase64 = filePathOrData.replace(/^data:[^;]+;base64,/, '')
583
+ fileBuffer = Buffer.from(cleanBase64, 'base64')
584
+ fileName = options.filename || `image-${Date.now()}.png`
585
+ isImage = true
586
+ } else if (typeof filePathOrData === 'string' && fs.existsSync(filePathOrData)) {
587
+ const rawPath = filePathOrData.trim()
588
+ fileName = path.basename(rawPath)
589
+ const ext = path.extname(rawPath).toLowerCase()
590
+ isImage = IMAGE_EXTS.includes(ext)
591
+
592
+ const rawBuf = fs.readFileSync(rawPath)
593
+ const previewStr = rawBuf.slice(0, 100).toString('utf8')
594
+ if (previewStr.startsWith('data:image/') || previewStr.startsWith('data:application/')) {
595
+ const cleanBase64 = rawBuf
596
+ .toString('utf8')
597
+ .replace(/^data:[^;]+;base64,/, '')
598
+ .trim()
599
+ fileBuffer = Buffer.from(cleanBase64, 'base64')
600
+ } else {
601
+ fileBuffer = rawBuf
602
+ }
603
+ } else if (Buffer.isBuffer(filePathOrData)) {
604
+ fileBuffer = filePathOrData
605
+ fileName = options.filename || `file-${Date.now()}.png`
606
+ isImage =
607
+ options.type === 'photo' || IMAGE_EXTS.some((ext) => fileName.toLowerCase().endsWith(ext))
608
+ } else {
609
+ return { success: false, error: `Berkas atau data tidak ditemukan: ${filePathOrData}` }
610
+ }
611
+
612
+ if (options.type === 'photo' || options.type === 'image') {
613
+ isImage = true
614
+ } else if (options.type === 'file' || options.type === 'document') {
615
+ isImage = false
616
+ }
617
+
618
+ let successCount = 0
619
+ let lastError = null
620
+
621
+ for (const target of targets) {
622
+ try {
623
+ if (isImage) {
624
+ try {
625
+ await bot.telegram.sendPhoto(
626
+ target,
627
+ { source: fileBuffer, filename: fileName },
628
+ caption ? { caption } : undefined
629
+ )
630
+ successCount++
631
+ continue
632
+ } catch (photoErr) {
633
+ console.warn(
634
+ `[Telegram] sendPhoto gagal ke ${target}, fallback ke sendDocument:`,
635
+ photoErr.message
636
+ )
637
+ }
638
+ }
639
+
640
+ await bot.telegram.sendDocument(
641
+ target,
642
+ { source: fileBuffer, filename: fileName },
643
+ caption ? { caption } : undefined
644
+ )
645
+ successCount++
646
+ } catch (err) {
647
+ console.error(`[Telegram] Gagal kirim berkas ke ${target}:`, err.message)
648
+ lastError = err.message
649
+ }
650
+ }
651
+
652
+ return successCount > 0
653
+ ? { success: true, targets }
654
+ : { success: false, error: lastError || 'Gagal mengirim berkas ke Telegram.' }
655
+ }
656
+
657
+ export const sendTelegramScreenshot = async (chatId = null) => {
658
+ if (!bot || currentStatus !== 'connected') {
659
+ return { success: false, error: 'Telegram Bot belum terhubung.' }
660
+ }
661
+ try {
662
+ const { captureDesktopScreenshotsBase64 } = await import('../../server/tools/screen-service.js')
663
+ const base64List = await captureDesktopScreenshotsBase64()
664
+
665
+ const targets = resolveTelegramChatIds(chatId)
666
+ if (targets.length === 0) {
667
+ return { success: false, error: 'Tidak ada target admin Telegram yang terdaftar.' }
668
+ }
669
+
670
+ for (const target of targets) {
671
+ try {
672
+ if (base64List.length === 1) {
673
+ const cleanBase64 = base64List[0].replace(/^data:image\/\w+;base64,/, '')
674
+ const buffer = Buffer.from(cleanBase64, 'base64')
675
+ await bot.telegram.sendPhoto(
676
+ target,
677
+ { source: buffer },
678
+ { caption: 'Screenshot Layar PC Mark' }
679
+ )
680
+ } else {
681
+ for (let i = 0; i < base64List.length; i++) {
682
+ const cleanBase64 = base64List[i].replace(/^data:image\/\w+;base64,/, '')
683
+ const buffer = Buffer.from(cleanBase64, 'base64')
684
+ await bot.telegram.sendPhoto(
685
+ target,
686
+ { source: buffer },
687
+ { caption: `Screenshot Layar PC Mark - Monitor ${i + 1} dari ${base64List.length}` }
688
+ )
689
+ }
690
+ }
691
+ } catch (err) {
692
+ console.error(`[Telegram] Gagal kirim screenshot ke ${target}:`, err.message)
693
+ }
694
+ }
695
+ return { success: true }
696
+ } catch (e) {
697
+ console.error('[Telegram] Gagal proses screenshot:', e)
698
+ return { success: false, error: e.message }
699
+ }
700
+ }
701
+
702
+ export const sendTelegramToAdmins = async (text) => {
703
+ if (!bot || currentStatus !== 'connected') {
704
+ pendingBroadcastQueue.push(text)
705
+ return
706
+ }
707
+ const config = getTelegramConfig()
708
+ const adminInputs = (config.tgAdminIds || '')
709
+ .split(',')
710
+ .map((id) => id.trim().toLowerCase().replace(/^@/, ''))
711
+ .filter(Boolean)
712
+
713
+ const targetChatIds = new Set(adminChatIdsSet)
714
+ for (const input of adminInputs) {
715
+ if (/^\d+$/.test(input)) {
716
+ targetChatIds.add(input)
717
+ } else if (usernameToChatIdMap.has(input)) {
718
+ targetChatIds.add(usernameToChatIdMap.get(input))
719
+ }
720
+ }
721
+
722
+ if (targetChatIds.size === 0) return
723
+
724
+ for (const chatId of targetChatIds) {
725
+ try {
726
+ const htmlText = formatMarkdownToTelegramHTML(text)
727
+ await bot.telegram.sendMessage(chatId, htmlText, { parse_mode: 'HTML' })
728
+ } catch (_) {
729
+ try {
730
+ await bot.telegram.sendMessage(chatId, text)
731
+ } catch (e) {
732
+ console.error(`[Telegram] Gagal broadcast ke ${chatId}:`, e.message)
733
+ }
734
+ }
735
+ }
736
+ }
737
+
738
+ const flushPendingBroadcasts = async () => {
739
+ if (currentStatus !== 'connected' || pendingBroadcastQueue.length === 0) return
740
+ const queue = [...pendingBroadcastQueue]
741
+ pendingBroadcastQueue.length = 0
742
+ for (const text of queue) {
743
+ await sendTelegramToAdmins(text)
744
+ }
745
+ }
746
+
747
+ export const finishAgentExecution = async ({ chatId, result, msgId }) => {
748
+ const reqObj = pendingRequestsMap.get(msgId)
749
+ if (msgId && pendingRequestsMap.has(msgId)) {
750
+ pendingRequestsMap.delete(msgId)
751
+ }
752
+ const replyText = result?.answer || 'Selesai diproses.'
753
+
754
+ const uiReplyPayload = {
755
+ id: Date.now(),
756
+ chatId: chatId,
757
+ sender: 'Mark',
758
+ text: reqObj?.text || '',
759
+ reply: replyText,
760
+ toolsUsed: result?.toolsUsed || [],
761
+ time: new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }),
762
+ type: 'outgoing'
763
+ }
764
+
765
+ uiMessageHistory.push(uiReplyPayload)
766
+ if (uiMessageHistory.length > MAX_UI_HISTORY) uiMessageHistory.shift()
767
+
768
+ wsHub.broadcast('tg:reply-sent', uiReplyPayload)
769
+
770
+ if (bot && chatId) {
771
+ if (reqObj?.loadingMsgId) {
772
+ try {
773
+ await bot.telegram.deleteMessage(chatId, reqObj.loadingMsgId)
774
+ } catch (_) {}
775
+ }
776
+ try {
777
+ const htmlText = formatMarkdownToTelegramHTML(replyText)
778
+ await bot.telegram.sendMessage(chatId, htmlText, { parse_mode: 'HTML' })
779
+ } catch (_) {
780
+ try {
781
+ await bot.telegram.sendMessage(chatId, replyText, { parse_mode: 'Markdown' })
782
+ } catch (fallbackErr) {
783
+ await bot.telegram.sendMessage(chatId, replyText).catch(() => {})
784
+ }
785
+ }
786
+ }
787
+ }
788
+
789
+ export const triggerTelegramMusicDownload = async ({ chatId, query }) => {
790
+ if (!bot || !chatId || !query) return
791
+ try {
792
+ const searchResult = await yts(query)
793
+ const video = searchResult.videos[0]
794
+ if (!video) {
795
+ await bot.telegram.sendMessage(chatId, `Lagu "${query}" tidak ditemukan di YouTube.`)
796
+ return
797
+ }
798
+ const tempPath = path.join(os.tmpdir(), `tg-audio-${Date.now()}.mp3`)
799
+ const unpackFfmpeg = ffmpeg ? ffmpeg.replace('app.asar', 'app.asar.unpacked') : 'ffmpeg'
800
+ const unpackYtdl = unpackFfmpeg.replace(
801
+ /ffmpeg-static[\\/]ffmpeg\.exe/i,
802
+ 'youtube-dl-exec\\bin\\yt-dlp.exe'
803
+ )
804
+
805
+ await new Promise((resolve, reject) => {
806
+ execFile(
807
+ unpackYtdl,
808
+ [
809
+ video.url,
810
+ '--extract-audio',
811
+ '--audio-format',
812
+ 'mp3',
813
+ '--ffmpeg-location',
814
+ unpackFfmpeg,
815
+ '--output',
816
+ tempPath
817
+ ],
818
+ (err) => {
819
+ if (err) reject(err)
820
+ else resolve()
821
+ }
822
+ )
823
+ })
824
+
825
+ await bot.telegram.sendAudio(
826
+ chatId,
827
+ { source: tempPath },
828
+ { title: video.title, performer: video.author?.name || 'YouTube' }
829
+ )
830
+ fs.unlink(tempPath, () => {})
831
+ } catch (err) {
832
+ console.error('[Telegram] Error music download:', err)
833
+ await bot.telegram.sendMessage(chatId, `Gagal download lagu: ${err.message}`)
834
+ }
835
+ }