@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,757 @@
1
+ import { jsonrepair } from 'jsonrepair'
2
+ import {
3
+ getSystemSignature,
4
+ activeAbortControllers,
5
+ createMoodStreamFilter,
6
+ createLMStudioOfflineError,
7
+ isLMStudioOfflineError,
8
+ parseMarkTag,
9
+ stripMarkTags
10
+ } from './ai-utils.js'
11
+
12
+ export async function executeOpenAIProvider({
13
+ messages,
14
+ tools = null,
15
+ stream = false,
16
+ config = {},
17
+ isSmallTask = false,
18
+ jsonSchema = null,
19
+ signal = null,
20
+ onToken = null,
21
+ onReasoning = null,
22
+ onMood = null,
23
+ onMeta = null,
24
+ onToolCall = null,
25
+ onStatus = null
26
+ }) {
27
+ const conf = config || {}
28
+
29
+ let formattedMessages = (messages || []).map((m, index) => {
30
+ let sanitizedContent = m.content
31
+ if (Array.isArray(m.content)) {
32
+ if (index < messages.length - 1) {
33
+ sanitizedContent = m.content.find((c) => c.type === 'text')?.text || '[Gambar terlampir]'
34
+ } else {
35
+ sanitizedContent = m.content
36
+ }
37
+ }
38
+
39
+ if (
40
+ (m.role === 'assistant' || m.role === 'model') &&
41
+ typeof sanitizedContent === 'string' &&
42
+ m.mood &&
43
+ !/<mark\b/i.test(sanitizedContent.trim())
44
+ ) {
45
+ sanitizedContent = `<mark mood="${m.mood}" done="true" /> ${sanitizedContent}`
46
+ }
47
+
48
+ if (
49
+ (m.role === 'assistant' || m.role === 'model') &&
50
+ !sanitizedContent &&
51
+ (!m.tool_calls || m.tool_calls.length === 0)
52
+ ) {
53
+ return {
54
+ ...m,
55
+ role: 'user',
56
+ content: stream
57
+ ? '[Catatan Sistem]: Lanjutkan analisis dan eksekusi tugas berikutnya.'
58
+ : '[Catatan Sistem]: Lanjutkan analisis dan langkah kerja berikutnya.'
59
+ }
60
+ }
61
+
62
+ return { ...m, content: sanitizedContent }
63
+ })
64
+
65
+ // Sanitasi trailing assistant/model message untuk mencegah error 400 di endpoint OpenAI-compat
66
+ while (
67
+ formattedMessages.length > 0 &&
68
+ (formattedMessages[formattedMessages.length - 1].role === 'assistant' ||
69
+ formattedMessages[formattedMessages.length - 1].role === 'model') &&
70
+ !formattedMessages[formattedMessages.length - 1].content &&
71
+ (!formattedMessages[formattedMessages.length - 1].tool_calls ||
72
+ formattedMessages[formattedMessages.length - 1].tool_calls.length === 0)
73
+ ) {
74
+ formattedMessages.pop()
75
+ }
76
+
77
+ if (
78
+ formattedMessages.length > 0 &&
79
+ (formattedMessages[formattedMessages.length - 1].role === 'assistant' ||
80
+ formattedMessages[formattedMessages.length - 1].role === 'model')
81
+ ) {
82
+ formattedMessages.push({
83
+ role: 'user',
84
+ content: stream
85
+ ? '[Instruksi Lanjutan]: Lanjutkan analisis dan eksekusi tugas berikutnya.'
86
+ : 'Lanjutkan analisis dan langkah kerja berikutnya.'
87
+ })
88
+ }
89
+
90
+ if (!isSmallTask) {
91
+ const pld = getSystemSignature()
92
+ const sysIdx = formattedMessages.findIndex((m) => m.role === 'system')
93
+ if (sysIdx >= 0) {
94
+ formattedMessages[sysIdx].content += `\n\n${pld}`
95
+ } else {
96
+ formattedMessages.unshift({ role: 'system', content: pld })
97
+ }
98
+ }
99
+
100
+ let endpoint = 'http://localhost:1234/v1/chat/completions'
101
+ const headers = {
102
+ 'Content-Type': 'application/json'
103
+ }
104
+
105
+ if (conf.aiProvider === 'custom') {
106
+ endpoint = conf.customEndpoint || 'http://localhost:1234/v1/chat/completions'
107
+ if (conf.customApiKey) {
108
+ headers['Authorization'] = `Bearer ${conf.customApiKey}`
109
+ }
110
+ }
111
+
112
+ const model =
113
+ conf.aiProvider === 'custom' ? conf.customModel || 'default-model' : conf.model || 'local-model'
114
+
115
+ // ==========================================
116
+ // 1. STREAMING MODE (Agent ReAct Gateway)
117
+ // ==========================================
118
+ if (stream) {
119
+ const body = {
120
+ stream: true,
121
+ stream_options: { include_usage: true },
122
+ model,
123
+ temperature: Number(conf.temperature) || 0,
124
+ messages: formattedMessages
125
+ }
126
+
127
+ if (tools && Array.isArray(tools) && tools.length > 0) {
128
+ body.tools = tools
129
+ }
130
+
131
+ const abortController = new AbortController()
132
+ activeAbortControllers.add(abortController)
133
+
134
+ if (signal) {
135
+ if (signal.aborted) {
136
+ abortController.abort()
137
+ } else {
138
+ signal.addEventListener('abort', () => abortController.abort())
139
+ }
140
+ }
141
+
142
+ let moodExtracted = false
143
+ const extractMood = (text) => {
144
+ if (!moodExtracted && text && onMood) {
145
+ const parsed = parseMarkTag(text)
146
+ if (parsed.meta?.mood && parsed.meta.mood !== 'neutral') {
147
+ onMood(parsed.meta.mood)
148
+ moodExtracted = true
149
+ }
150
+ }
151
+ }
152
+
153
+ let accumulatedContent = ''
154
+ let accumulatedReasoning = ''
155
+ const accumulatedToolCalls = {}
156
+ let finishReason = 'stop'
157
+ let serverUsage = null
158
+
159
+ try {
160
+ let response = await fetch(endpoint, {
161
+ method: 'POST',
162
+ headers,
163
+ body: JSON.stringify(body),
164
+ signal: abortController.signal
165
+ })
166
+
167
+ if (!response.ok && body.stream_options && response.status === 400) {
168
+ const errCloned = response.clone()
169
+ const textData = await errCloned.text().catch(() => '')
170
+ if (
171
+ textData.toLowerCase().includes('stream_options') ||
172
+ textData.toLowerCase().includes('unknown')
173
+ ) {
174
+ const fallbackBody = { ...body }
175
+ delete fallbackBody.stream_options
176
+ const retryRes = await fetch(endpoint, {
177
+ method: 'POST',
178
+ headers,
179
+ body: JSON.stringify(fallbackBody),
180
+ signal: abortController.signal
181
+ })
182
+ if (retryRes.ok) {
183
+ response = retryRes
184
+ }
185
+ }
186
+ }
187
+
188
+ if (!response.ok) {
189
+ const textData = await response.text()
190
+ let errorMsg = textData
191
+ try {
192
+ const json = JSON.parse(textData)
193
+ errorMsg = json.error?.message || json.message || textData
194
+ } catch (_) {}
195
+ throw new Error(`API Error (${response.status}): ${errorMsg}`)
196
+ }
197
+
198
+ const sseMoodFilter = createMoodStreamFilter(
199
+ onToken,
200
+ (mood) => {
201
+ onMood?.(mood)
202
+ moodExtracted = true
203
+ },
204
+ (meta) => {
205
+ onMeta?.(meta)
206
+ }
207
+ )
208
+
209
+ const processContentToken = (token) => {
210
+ accumulatedContent += token
211
+ sseMoodFilter(token)
212
+ }
213
+
214
+ const handleChunkText = (jsonStr) => {
215
+ if (!jsonStr || jsonStr === '[DONE]') return
216
+ try {
217
+ const parsed = JSON.parse(jsonStr)
218
+ if (parsed.usage) {
219
+ serverUsage = parsed.usage
220
+ }
221
+ const choice = parsed.choices?.[0]
222
+ if (!choice) return
223
+
224
+ if (choice.finish_reason) {
225
+ finishReason = choice.finish_reason
226
+ }
227
+
228
+ const delta = choice.delta || {}
229
+
230
+ if (delta.reasoning_content || delta.reasoning) {
231
+ const rToken = delta.reasoning_content || delta.reasoning
232
+ accumulatedReasoning += rToken
233
+ extractMood(accumulatedReasoning)
234
+ onReasoning?.(rToken)
235
+ }
236
+
237
+ if (delta.content) {
238
+ processContentToken(delta.content)
239
+ }
240
+
241
+ if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
242
+ for (const tc of delta.tool_calls) {
243
+ const idx = tc.index ?? 0
244
+ if (!accumulatedToolCalls[idx]) {
245
+ accumulatedToolCalls[idx] = {
246
+ id: tc.id || `call_${Date.now()}_${idx}`,
247
+ type: 'function',
248
+ function: {
249
+ name: tc.function?.name || '',
250
+ arguments: ''
251
+ }
252
+ }
253
+ }
254
+ if (tc.function?.name && !accumulatedToolCalls[idx].function.name) {
255
+ accumulatedToolCalls[idx].function.name = tc.function.name
256
+ }
257
+ if (tc.function?.arguments) {
258
+ accumulatedToolCalls[idx].function.arguments += tc.function.arguments
259
+ }
260
+ }
261
+ }
262
+ } catch (_) {}
263
+ }
264
+
265
+ if (response.body && (response.body.getReader || response.body[Symbol.asyncIterator])) {
266
+ if (response.body[Symbol.asyncIterator]) {
267
+ const decoder = new TextDecoder()
268
+ let lineBuffer = ''
269
+ for await (const chunk of response.body) {
270
+ lineBuffer +=
271
+ typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true })
272
+ const lines = lineBuffer.split('\n')
273
+ lineBuffer = lines.pop() || ''
274
+ for (const line of lines) {
275
+ const trimmed = line.trim()
276
+ if (trimmed.startsWith('data:')) {
277
+ handleChunkText(trimmed.slice(5).trim())
278
+ }
279
+ }
280
+ }
281
+ if (lineBuffer.trim().startsWith('data:')) {
282
+ handleChunkText(lineBuffer.trim().slice(5).trim())
283
+ }
284
+ } else {
285
+ const reader = response.body.getReader()
286
+ const decoder = new TextDecoder()
287
+ let lineBuffer = ''
288
+ while (true) {
289
+ const { done, value } = await reader.read()
290
+ if (done) break
291
+ lineBuffer +=
292
+ typeof value === 'string' ? value : decoder.decode(value, { stream: true })
293
+ const lines = lineBuffer.split('\n')
294
+ lineBuffer = lines.pop() || ''
295
+ for (const line of lines) {
296
+ const trimmed = line.trim()
297
+ if (trimmed.startsWith('data:')) {
298
+ handleChunkText(trimmed.slice(5).trim())
299
+ }
300
+ }
301
+ }
302
+ if (lineBuffer.trim().startsWith('data:')) {
303
+ handleChunkText(lineBuffer.trim().slice(5).trim())
304
+ }
305
+ sseMoodFilter.flush()
306
+ }
307
+ } else {
308
+ const raw = await response.text()
309
+ const parsed = JSON.parse(raw)
310
+ if (parsed.usage) {
311
+ serverUsage = parsed.usage
312
+ }
313
+ const choice = parsed.choices?.[0]
314
+ if (choice) {
315
+ accumulatedContent = choice.message?.content || ''
316
+ accumulatedReasoning =
317
+ choice.message?.reasoning || choice.message?.reasoning_content || ''
318
+ if (choice.message?.tool_calls) {
319
+ choice.message.tool_calls.forEach((tc, i) => {
320
+ accumulatedToolCalls[i] = tc
321
+ })
322
+ finishReason = 'tool_calls'
323
+ }
324
+ if (accumulatedContent) onToken?.(accumulatedContent)
325
+ if (accumulatedReasoning) onReasoning?.(accumulatedReasoning)
326
+ }
327
+ }
328
+
329
+ const toolCallsList = Object.values(accumulatedToolCalls)
330
+ if (toolCallsList.length > 0) {
331
+ finishReason = 'tool_calls'
332
+ onToolCall?.(toolCallsList)
333
+ }
334
+
335
+ let finalMood = null
336
+ const fullText = (accumulatedReasoning + '\n' + accumulatedContent).trim()
337
+ const parsedFull = parseMarkTag(fullText)
338
+ if (parsedFull.meta?.mood) {
339
+ finalMood = parsedFull.meta.mood
340
+ if (!moodExtracted) {
341
+ onMood?.(finalMood)
342
+ moodExtracted = true
343
+ }
344
+ }
345
+
346
+ return {
347
+ content: accumulatedContent,
348
+ reasoning: accumulatedReasoning,
349
+ mood: finalMood,
350
+ toolCalls: toolCallsList.length > 0 ? toolCallsList : null,
351
+ finishReason,
352
+ usage: serverUsage
353
+ }
354
+ } catch (error) {
355
+ if (conf.aiProvider !== 'custom' && isLMStudioOfflineError(error)) {
356
+ throw createLMStudioOfflineError(error)
357
+ }
358
+ throw error
359
+ } finally {
360
+ activeAbortControllers.delete(abortController)
361
+ }
362
+ }
363
+
364
+ // ==========================================
365
+ // 2. NON-STREAMING MODE (Standard fetchAI)
366
+ // ==========================================
367
+ const body = {
368
+ stream: false,
369
+ model,
370
+ temperature: Number(conf.temperature) || 0,
371
+ messages: formattedMessages
372
+ }
373
+
374
+ if (jsonSchema) {
375
+ if (conf.aiProvider === 'custom') {
376
+ body.messages = body.messages.map((m) => ({ ...m }))
377
+ const sysIdx = body.messages.findIndex((m) => m.role === 'system')
378
+ const instruction = `\n\n[CRITICAL] YOU MUST RETURN ONLY VALID JSON THAT STRICTLY MATCHES THIS EXACT SCHEMA:\n${JSON.stringify(jsonSchema)}\n`
379
+ if (sysIdx >= 0) {
380
+ body.messages[sysIdx].content += instruction
381
+ } else {
382
+ body.messages.unshift({ role: 'system', content: instruction })
383
+ }
384
+ } else {
385
+ body.response_format = {
386
+ type: 'json_schema',
387
+ json_schema: {
388
+ name: 'mark_schema',
389
+ strict: true,
390
+ schema: jsonSchema
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ const parentAbortController = new AbortController()
397
+ activeAbortControllers.add(parentAbortController)
398
+
399
+ if (signal) {
400
+ if (signal.aborted) parentAbortController.abort()
401
+ else signal.addEventListener('abort', () => parentAbortController.abort())
402
+ }
403
+
404
+ const executeFetch = async (currentBody, isRetry = false, trafficRetryCount = 0) => {
405
+ if (parentAbortController.signal.aborted) {
406
+ throw new Error('AbortError')
407
+ }
408
+
409
+ const timeoutMs = 300000
410
+ const abortController = new AbortController()
411
+ activeAbortControllers.add(abortController)
412
+ const timeoutId = setTimeout(
413
+ () => abortController.abort(new Error('Request Timeout (Tidak ada respon dari server)')),
414
+ timeoutMs
415
+ )
416
+
417
+ let response
418
+ try {
419
+ response = await fetch(endpoint, {
420
+ method: 'POST',
421
+ headers,
422
+ body: JSON.stringify(currentBody),
423
+ signal: abortController.signal
424
+ })
425
+ clearTimeout(timeoutId)
426
+ } catch (err) {
427
+ clearTimeout(timeoutId)
428
+ if (parentAbortController.signal.aborted) {
429
+ throw new Error('AbortError')
430
+ }
431
+ if (
432
+ abortController.signal.reason?.message === 'Request Timeout (Tidak ada respon dari server)'
433
+ ) {
434
+ throw new Error('Request Timeout: AI memakan waktu terlalu lama untuk membalas.')
435
+ }
436
+ if (err.name === 'AbortError' || (err.message && err.message.includes('Timeout'))) {
437
+ throw new Error(`Koneksi Timeout: Server API (${endpoint}) nge-gantung lebih dari 5 menit.`)
438
+ }
439
+ const causeStr = err.cause ? ` (${err.cause.message || err.cause.code || err.cause})` : ''
440
+ const enrichedError = new Error(
441
+ `Gagal menghubungi server AI di ${endpoint}: ${err.message}${causeStr}`
442
+ )
443
+ enrichedError.code = err.code || err.cause?.code || 'FETCH_FAILED'
444
+ enrichedError.cause = err
445
+ throw enrichedError
446
+ } finally {
447
+ clearTimeout(timeoutId)
448
+ parentAbortController.signal.removeEventListener(
449
+ 'abort',
450
+ abortController.abort.bind(abortController)
451
+ )
452
+ activeAbortControllers.delete(abortController)
453
+ }
454
+
455
+ if (!response.ok) {
456
+ const textData = await response.text()
457
+ let errorData = null
458
+ try {
459
+ errorData = JSON.parse(textData)
460
+ } catch (_) {}
461
+
462
+ const errorMsg =
463
+ errorData?.error?.message || errorData?.message || response.statusText || textData
464
+
465
+ if (
466
+ !isRetry &&
467
+ currentBody.response_format?.type === 'json_schema' &&
468
+ (String(errorMsg).toLowerCase().includes('schema') ||
469
+ String(errorMsg).toLowerCase().includes('json') ||
470
+ response.status === 400 ||
471
+ response.status === 422)
472
+ ) {
473
+ const fallbackBody = { ...currentBody }
474
+ fallbackBody.response_format = { type: 'json_object' }
475
+
476
+ const fallbackMessages = fallbackBody.messages.map((m) => ({ ...m }))
477
+ const sysIdx = fallbackMessages.findIndex((m) => m.role === 'system')
478
+ const instruction = `\n\n[CRITICAL] YOU MUST RETURN ONLY VALID JSON THAT STRICTLY MATCHES THIS EXACT SCHEMA:\n${JSON.stringify(jsonSchema)}\n`
479
+
480
+ if (sysIdx >= 0) {
481
+ fallbackMessages[sysIdx].content += instruction
482
+ } else {
483
+ fallbackMessages.unshift({ role: 'system', content: instruction })
484
+ }
485
+ fallbackBody.messages = fallbackMessages
486
+
487
+ return executeFetch(fallbackBody, true, trafficRetryCount)
488
+ }
489
+
490
+ const errorProvider = conf.aiProvider === 'custom' ? 'Custom API' : 'LM Studio'
491
+ const finalErrorMessage = typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg)
492
+
493
+ const err = new Error(`Gagal memuat AI (${errorProvider}): ${finalErrorMessage}`)
494
+ err.status = response.status
495
+ throw err
496
+ }
497
+
498
+ const rawText = await response.text()
499
+ const cleanText = rawText.trim()
500
+
501
+ // 1. Tangani jika response berupa JSON Array chunk
502
+ if (cleanText.startsWith('[') && cleanText.endsWith(']')) {
503
+ try {
504
+ const parsedArray = JSON.parse(cleanText)
505
+ if (Array.isArray(parsedArray) && parsedArray.length > 0) {
506
+ let combinedContent = ''
507
+ let reasoning = ''
508
+ const lastId = parsedArray[0]?.id || 'chatcmpl-array-stream'
509
+ const modelName = parsedArray[0]?.model || model
510
+ let isChunkArray = false
511
+
512
+ for (const item of parsedArray) {
513
+ if (item?.object === 'chat.completion.chunk' || item?.choices?.[0]?.delta) {
514
+ isChunkArray = true
515
+ const delta = item.choices?.[0]?.delta
516
+ if (delta) {
517
+ if (delta.content) combinedContent += delta.content
518
+ if (delta.reasoning_content) reasoning += delta.reasoning_content
519
+ else if (delta.reasoning) reasoning += delta.reasoning
520
+ }
521
+ } else if (item?.choices?.[0]?.message?.content) {
522
+ isChunkArray = true
523
+ combinedContent += item.choices[0].message.content
524
+ }
525
+ }
526
+
527
+ if (isChunkArray && combinedContent) {
528
+ return {
529
+ id: lastId,
530
+ model: modelName,
531
+ choices: [
532
+ {
533
+ message: {
534
+ role: 'assistant',
535
+ content: combinedContent,
536
+ reasoning: reasoning || null
537
+ }
538
+ }
539
+ ]
540
+ }
541
+ }
542
+ }
543
+ } catch (_) {}
544
+ }
545
+
546
+ // 2. Tangani jika response berupa SSE Stream
547
+ if (cleanText.includes('data:') || cleanText.includes('[DONE]')) {
548
+ const lines = cleanText.split('\n')
549
+ let combinedContent = ''
550
+ let reasoning = ''
551
+ let lastId = 'chatcmpl-stream'
552
+ let modelName = model
553
+
554
+ for (const line of lines) {
555
+ const trimmed = line.trim()
556
+ if (!trimmed || !trimmed.startsWith('data:')) continue
557
+ const dataStr = trimmed.slice(5).trim()
558
+ if (!dataStr || dataStr === '[DONE]') continue
559
+
560
+ try {
561
+ const chunk = JSON.parse(dataStr)
562
+ if (chunk.id) lastId = chunk.id
563
+ if (chunk.model) modelName = chunk.model
564
+ const delta = chunk.choices?.[0]?.delta
565
+ if (delta) {
566
+ if (delta.content) combinedContent += delta.content
567
+ if (delta.reasoning_content) reasoning += delta.reasoning_content
568
+ else if (delta.reasoning) reasoning += delta.reasoning
569
+ } else if (chunk.choices?.[0]?.message?.content) {
570
+ combinedContent += chunk.choices[0].message.content
571
+ }
572
+ } catch (_) {}
573
+ }
574
+
575
+ if (combinedContent) {
576
+ return {
577
+ id: lastId,
578
+ model: modelName,
579
+ choices: [
580
+ {
581
+ message: {
582
+ role: 'assistant',
583
+ content: combinedContent,
584
+ reasoning: reasoning || null
585
+ }
586
+ }
587
+ ]
588
+ }
589
+ }
590
+ }
591
+
592
+ // 3. Tangani jika response berupa NDJSON (JSON Lines)
593
+ if (cleanText.includes('\n{')) {
594
+ const lines = cleanText
595
+ .split('\n')
596
+ .map((l) => l.trim())
597
+ .filter(Boolean)
598
+ let combinedContent = ''
599
+ let isValidNdjson = false
600
+ for (const line of lines) {
601
+ try {
602
+ const parsed = JSON.parse(line)
603
+ if (parsed.choices?.[0]?.delta?.content) {
604
+ combinedContent += parsed.choices[0].delta.content
605
+ isValidNdjson = true
606
+ } else if (parsed.choices?.[0]?.message?.content) {
607
+ combinedContent += parsed.choices[0].message.content
608
+ isValidNdjson = true
609
+ }
610
+ } catch (_) {}
611
+ }
612
+ if (isValidNdjson && combinedContent) {
613
+ return {
614
+ choices: [{ message: { role: 'assistant', content: combinedContent } }]
615
+ }
616
+ }
617
+ }
618
+
619
+ // 4. Tangani JSON standar atau dengan jsonrepair
620
+ try {
621
+ return JSON.parse(rawText)
622
+ } catch (_) {
623
+ const firstBrace = cleanText.indexOf('{')
624
+ const lastBrace = cleanText.lastIndexOf('}')
625
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace >= firstBrace) {
626
+ const sub = cleanText.substring(firstBrace, lastBrace + 1)
627
+ try {
628
+ return JSON.parse(sub)
629
+ } catch (_) {
630
+ try {
631
+ return JSON.parse(jsonrepair(sub))
632
+ } catch (_) {}
633
+ }
634
+ }
635
+
636
+ try {
637
+ return JSON.parse(jsonrepair(rawText))
638
+ } catch (parseError) {
639
+ throw new Error(`API mengembalikan format respon tidak valid: ${parseError.message}`)
640
+ }
641
+ }
642
+ }
643
+
644
+ try {
645
+ const data = await executeFetch(body)
646
+
647
+ if (!data) {
648
+ throw new Error('API tidak mengembalikan data respon.')
649
+ }
650
+
651
+ let processedData = data
652
+ if (Array.isArray(processedData)) {
653
+ let combinedContent = ''
654
+ let combinedReasoning = ''
655
+ for (const item of processedData) {
656
+ const delta = item.choices?.[0]?.delta || item.choices?.[0]?.message
657
+ if (delta) {
658
+ if (delta.content) combinedContent += delta.content
659
+ if (delta.reasoning_content) combinedReasoning += delta.reasoning_content
660
+ else if (delta.reasoning) combinedReasoning += delta.reasoning
661
+ }
662
+ }
663
+ processedData = {
664
+ choices: [
665
+ {
666
+ message: {
667
+ role: 'assistant',
668
+ content: combinedContent,
669
+ reasoning: combinedReasoning || null
670
+ }
671
+ }
672
+ ]
673
+ }
674
+ }
675
+
676
+ const choice =
677
+ Array.isArray(processedData.choices) && processedData.choices.length > 0
678
+ ? processedData.choices[0]
679
+ : null
680
+ const message =
681
+ choice?.message ||
682
+ choice?.delta ||
683
+ (processedData.message
684
+ ? processedData.message
685
+ : processedData.response
686
+ ? { content: processedData.response }
687
+ : processedData.result
688
+ ? { content: processedData.result }
689
+ : null)
690
+
691
+ let content = ''
692
+ let reasoning = null
693
+
694
+ if (message) {
695
+ content = typeof message === 'string' ? message : message.content || message.text || ''
696
+ reasoning = message.reasoning || message.reasoning_content || null
697
+ } else if (typeof processedData === 'string') {
698
+ content = processedData
699
+ } else if (
700
+ processedData.content ||
701
+ processedData.text ||
702
+ processedData.response ||
703
+ processedData.result
704
+ ) {
705
+ content =
706
+ processedData.content ||
707
+ processedData.text ||
708
+ processedData.response ||
709
+ processedData.result ||
710
+ ''
711
+ reasoning = processedData.reasoning || processedData.reasoning_content || null
712
+ } else if (processedData.error) {
713
+ const errMsg =
714
+ typeof processedData.error === 'object'
715
+ ? processedData.error.message || JSON.stringify(processedData.error)
716
+ : processedData.error
717
+ throw new Error(`API mengembalikan error: ${errMsg}`)
718
+ } else {
719
+ throw new Error(
720
+ `Format data API tidak dikenali atau kosong. Balasan mentah: ${JSON.stringify(processedData).slice(0, 150)}`
721
+ )
722
+ }
723
+
724
+ if (!reasoning && content.includes('<think>')) {
725
+ const match = content.match(/<think>([\s\S]*?)<\/think>/)
726
+ if (match) {
727
+ reasoning = match[1].trim()
728
+ content = content.replace(/<think>[\s\S]*?<\/think>/, '').trim()
729
+ } else {
730
+ const openIdx = content.indexOf('<think>')
731
+ if (openIdx !== -1) {
732
+ reasoning = content.substring(openIdx + 7).trim()
733
+ content = content.substring(0, openIdx).trim()
734
+ }
735
+ }
736
+ }
737
+
738
+ if (!content && reasoning) {
739
+ const firstBrace = reasoning.indexOf('{')
740
+ const lastBrace = reasoning.lastIndexOf('}')
741
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
742
+ content = reasoning.substring(firstBrace, lastBrace + 1)
743
+ reasoning =
744
+ (reasoning.substring(0, firstBrace) + reasoning.substring(lastBrace + 1)).trim() || null
745
+ }
746
+ }
747
+
748
+ return { content, reasoning, usage: processedData?.usage || null }
749
+ } catch (error) {
750
+ if (conf.aiProvider !== 'custom' && isLMStudioOfflineError(error)) {
751
+ throw createLMStudioOfflineError(error)
752
+ }
753
+ throw error
754
+ } finally {
755
+ activeAbortControllers.delete(parentAbortController)
756
+ }
757
+ }