@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,956 @@
1
+ /**
2
+ * DeepSeek Web RPC Engine for MARK Node.js Core
3
+ * Reverse-engineered chat.deepseek.com client with native WASM PoW Solver
4
+ */
5
+ import https from 'https'
6
+ import fs from 'fs'
7
+ import path from 'path'
8
+ import zlib from 'zlib'
9
+ import crypto from 'crypto'
10
+ import { fileURLToPath } from 'url'
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = path.dirname(__filename)
14
+
15
+ export const DEEPSEEK_WEB_MODELS = {
16
+ 'deepseek-chat': {
17
+ modelType: 'default',
18
+ thinking: false,
19
+ search: false,
20
+ name: 'deepseek-chat',
21
+ desc: 'DeepSeek-V3/Chat model cepat & responsif'
22
+ }
23
+ }
24
+
25
+ const BASE_HOST = 'chat.deepseek.com'
26
+
27
+ let wasmInstanceCache = null
28
+ const activeSessionCache = new Map()
29
+ const utilitySessionCache = new Map()
30
+ const sessionStateMap = new Map() // sessionId -> { lastMessageId, turnCount, createdAt, lastUsedAt }
31
+ let lastUsedToken = null
32
+
33
+ export function getSessionCacheKey(token, markSessionId = '1', isSmallTask = false) {
34
+ const prefix = isSmallTask ? 'util_' : 'sess_'
35
+ return `${token}:${prefix}${markSessionId || '1'}`
36
+ }
37
+
38
+ function ensureTokenSession(token) {
39
+ // Jika token berubah, clear session cache lama
40
+ if (lastUsedToken !== token) {
41
+ if (lastUsedToken) {
42
+ clearDeepSeekSession(lastUsedToken)
43
+ }
44
+ lastUsedToken = token
45
+ }
46
+ }
47
+
48
+ export function clearDeepSeekSession(token = null, markSessionId = null) {
49
+ if (token) {
50
+ if (markSessionId) {
51
+ for (const isSmall of [false, true]) {
52
+ const cache = isSmall ? utilitySessionCache : activeSessionCache
53
+ const key = getSessionCacheKey(token, markSessionId, isSmall)
54
+ const sId = cache.get(key)
55
+ if (sId) sessionStateMap.delete(sId)
56
+ cache.delete(key)
57
+ }
58
+ } else {
59
+ for (const [key, sId] of [...activeSessionCache.entries()]) {
60
+ if (key.startsWith(`${token}:`)) {
61
+ sessionStateMap.delete(sId)
62
+ activeSessionCache.delete(key)
63
+ }
64
+ }
65
+ for (const [key, sId] of [...utilitySessionCache.entries()]) {
66
+ if (key.startsWith(`${token}:`)) {
67
+ sessionStateMap.delete(sId)
68
+ utilitySessionCache.delete(key)
69
+ }
70
+ }
71
+ }
72
+ } else {
73
+ activeSessionCache.clear()
74
+ utilitySessionCache.clear()
75
+ sessionStateMap.clear()
76
+ }
77
+ }
78
+
79
+ export function getSessionState(token, isSmallTask = false, markSessionId = '1') {
80
+ const cache = isSmallTask ? utilitySessionCache : activeSessionCache
81
+ const key = getSessionCacheKey(token, markSessionId, isSmallTask)
82
+ const sessionId = cache.get(key)
83
+ if (!sessionId) return null
84
+ return sessionStateMap.get(sessionId) || null
85
+ }
86
+
87
+ /**
88
+ * Inisialisasi WebAssembly PoW Solver bawaan DeepSeek
89
+ */
90
+ async function getWasmSolver(wasmBuffer = null) {
91
+ if (wasmInstanceCache) return wasmInstanceCache
92
+
93
+ let buf = wasmBuffer
94
+ if (!buf) {
95
+ const localWasmPaths = [
96
+ path.resolve(__dirname, '../bin/sha3_wasm_bg.wasm'),
97
+ path.resolve(__dirname, '../assets/sha3_wasm_bg.wasm'),
98
+ path.resolve(__dirname, '../../../resources/sha3_wasm_bg.wasm')
99
+ ]
100
+ for (const p of localWasmPaths) {
101
+ if (fs.existsSync(p)) {
102
+ try {
103
+ const fileBuf = fs.readFileSync(p)
104
+ buf = fileBuf.buffer.slice(fileBuf.byteOffset, fileBuf.byteOffset + fileBuf.byteLength)
105
+ break
106
+ } catch {
107
+ // ignore error reading local wasm
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ if (!buf) {
114
+ throw new Error(
115
+ "Berkas WASM DeepSeek ('src/server/bin/sha3_wasm_bg.wasm') tidak ditemukan di disk lokal."
116
+ )
117
+ }
118
+
119
+ const wasmModule = await WebAssembly.instantiate(buf, {})
120
+ const exp = wasmModule.instance.exports
121
+
122
+ function writeStr(text) {
123
+ const encoder = new TextEncoder()
124
+ const data = encoder.encode(text)
125
+ const ptr = exp.__wbindgen_export_0(data.length, 1)
126
+ const mem = new Uint8Array(exp.memory.buffer)
127
+ mem.set(data, ptr)
128
+ return [ptr, data.length]
129
+ }
130
+
131
+ function solve(challenge, prefix, difficulty) {
132
+ const retptr = exp.__wbindgen_add_to_stack_pointer(-16)
133
+ try {
134
+ const [cPtr, cLen] = writeStr(challenge)
135
+ const [pPtr, pLen] = writeStr(prefix)
136
+ exp.wasm_solve(retptr, cPtr, cLen, pPtr, pLen, Number(difficulty))
137
+
138
+ const view = new DataView(exp.memory.buffer)
139
+ const status = view.getInt32(retptr, true)
140
+ const value = view.getFloat64(retptr + 8, true)
141
+ if (status === 0) return null
142
+ return Math.floor(value)
143
+ } finally {
144
+ exp.__wbindgen_add_to_stack_pointer(16)
145
+ }
146
+ }
147
+
148
+ wasmInstanceCache = { solve }
149
+ return wasmInstanceCache
150
+ }
151
+
152
+ /**
153
+ * Utility HTTP POST JSON request
154
+ */
155
+ function httpPostJson(path, headers, bodyObj) {
156
+ return new Promise((resolve, reject) => {
157
+ const postData = JSON.stringify(bodyObj)
158
+ const options = {
159
+ hostname: BASE_HOST,
160
+ port: 443,
161
+ path: path,
162
+ method: 'POST',
163
+ headers: {
164
+ 'content-type': 'application/json',
165
+ 'content-length': Buffer.byteLength(postData),
166
+ ...headers
167
+ }
168
+ }
169
+
170
+ const req = https.request(options, (res) => {
171
+ let data = ''
172
+ res.on('data', (chunk) => (data += chunk.toString()))
173
+ res.on('end', () => {
174
+ try {
175
+ const json = JSON.parse(data)
176
+ resolve(json)
177
+ } catch {
178
+ resolve(data)
179
+ }
180
+ })
181
+ })
182
+
183
+ req.on('error', reject)
184
+ req.write(postData)
185
+ req.end()
186
+ })
187
+ }
188
+
189
+ /**
190
+ * Utility HTTP GET JSON request
191
+ */
192
+ function httpGetJson(path, headers) {
193
+ return new Promise((resolve, reject) => {
194
+ const options = {
195
+ hostname: BASE_HOST,
196
+ port: 443,
197
+ path: path,
198
+ method: 'GET',
199
+ headers: {
200
+ ...headers
201
+ }
202
+ }
203
+
204
+ const req = https.request(options, (res) => {
205
+ let data = ''
206
+ res.on('data', (chunk) => (data += chunk.toString()))
207
+ res.on('end', () => {
208
+ try {
209
+ const json = JSON.parse(data)
210
+ resolve(json)
211
+ } catch {
212
+ resolve(data)
213
+ }
214
+ })
215
+ })
216
+
217
+ req.on('error', reject)
218
+ req.end()
219
+ })
220
+ }
221
+
222
+ /**
223
+ * Header standar untuk menyerupai peramban web asli
224
+ */
225
+ function getBaseHeaders(token, userAgent = null) {
226
+ return {
227
+ authorization: `Bearer ${token}`,
228
+ accept: '*/*',
229
+ 'accept-language': 'en-US,en;q=0.9,id;q=0.8',
230
+ 'user-agent':
231
+ userAgent ||
232
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
233
+ origin: `https://${BASE_HOST}`,
234
+ referer: `https://${BASE_HOST}/`,
235
+ 'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
236
+ 'sec-ch-ua-mobile': '?0',
237
+ 'sec-ch-ua-platform': '"Windows"',
238
+ 'sec-fetch-dest': 'empty',
239
+ 'sec-fetch-mode': 'cors',
240
+ 'sec-fetch-site': 'same-origin',
241
+ 'x-app-version': '2.0.2',
242
+ 'x-client-version': '2.0.2',
243
+ 'x-client-platform': 'web',
244
+ 'x-client-locale': 'en_US',
245
+ 'x-client-bundle-id': 'com.deepseek.chat'
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Ekstraksi envelope biz_data DeepSeek
251
+ */
252
+ function unwrapBizData(resJson) {
253
+ if (!resJson || resJson.code !== 0) {
254
+ throw new Error(`DeepSeek API Error: ${resJson?.msg || JSON.stringify(resJson)}`)
255
+ }
256
+ const biz = resJson.data?.biz_data
257
+ if (!biz) throw new Error(`Envelope biz_data tidak ditemukan: ${JSON.stringify(resJson)}`)
258
+ return biz
259
+ }
260
+
261
+ /**
262
+ * Utility HTTP POST multipart/form-data untuk upload berkas mentah.
263
+ */
264
+ function httpPostMultipart(reqPath, headers, fields, fileField) {
265
+ return new Promise((resolve, reject) => {
266
+ const boundary = '----MarkAgentBoundary' + crypto.randomBytes(16).toString('hex')
267
+ const CRLF = '\r\n'
268
+ const chunks = []
269
+
270
+ for (const [key, value] of Object.entries(fields || {})) {
271
+ chunks.push(
272
+ Buffer.from(
273
+ `--${boundary}${CRLF}` +
274
+ `Content-Disposition: form-data; name="${key}"${CRLF}${CRLF}` +
275
+ `${value}${CRLF}`
276
+ )
277
+ )
278
+ }
279
+
280
+ chunks.push(
281
+ Buffer.from(
282
+ `--${boundary}${CRLF}` +
283
+ `Content-Disposition: form-data; name="${fileField.fieldName}"; filename="${fileField.filename}"${CRLF}` +
284
+ `Content-Type: ${fileField.mimeType}${CRLF}${CRLF}`
285
+ )
286
+ )
287
+ chunks.push(fileField.buffer)
288
+ chunks.push(Buffer.from(CRLF))
289
+ chunks.push(Buffer.from(`--${boundary}--${CRLF}`))
290
+
291
+ const body = Buffer.concat(chunks)
292
+
293
+ const options = {
294
+ hostname: BASE_HOST,
295
+ port: 443,
296
+ path: reqPath,
297
+ method: 'POST',
298
+ headers: {
299
+ ...headers,
300
+ 'content-type': `multipart/form-data; boundary=${boundary}`,
301
+ 'content-length': body.length
302
+ }
303
+ }
304
+
305
+ const req = https.request(options, (res) => {
306
+ let data = ''
307
+ res.on('data', (chunk) => (data += chunk.toString()))
308
+ res.on('end', () => {
309
+ try {
310
+ resolve({ statusCode: res.statusCode, json: JSON.parse(data) })
311
+ } catch {
312
+ resolve({ statusCode: res.statusCode, json: null, raw: data })
313
+ }
314
+ })
315
+ })
316
+
317
+ req.on('error', reject)
318
+ req.write(body)
319
+ req.end()
320
+ })
321
+ }
322
+
323
+ /**
324
+ * Upload gambar mentah ke DeepSeek Web RPC.
325
+ * Mendukung Buffer, Base64 data URL, atau path file lokal.
326
+ */
327
+ export async function uploadImageFile(token, imagePathOrBuffer, filename = 'image.png') {
328
+ let buffer
329
+ if (Buffer.isBuffer(imagePathOrBuffer)) {
330
+ buffer = imagePathOrBuffer
331
+ } else if (typeof imagePathOrBuffer === 'string') {
332
+ if (imagePathOrBuffer.startsWith('data:')) {
333
+ const commaIdx = imagePathOrBuffer.indexOf(',')
334
+ const headerPart = imagePathOrBuffer.slice(0, commaIdx)
335
+ const base64Data = imagePathOrBuffer.slice(commaIdx + 1)
336
+ buffer = Buffer.from(base64Data, 'base64')
337
+ const matchMime = headerPart.match(/data:([^;]+)/)
338
+ if (matchMime) {
339
+ const ext = matchMime[1].split('/')[1] || 'png'
340
+ filename = `upload_${Date.now()}.${ext}`
341
+ }
342
+ } else {
343
+ buffer = fs.readFileSync(imagePathOrBuffer)
344
+ filename = path.basename(imagePathOrBuffer)
345
+ }
346
+ } else {
347
+ throw new Error(
348
+ 'Format berkas gambar tidak valid (harus Buffer, Base64 data URL, atau file path).'
349
+ )
350
+ }
351
+
352
+ const ext = path.extname(filename).toLowerCase()
353
+ const mimeMap = {
354
+ '.png': 'image/png',
355
+ '.jpg': 'image/jpeg',
356
+ '.jpeg': 'image/jpeg',
357
+ '.webp': 'image/webp',
358
+ '.gif': 'image/gif'
359
+ }
360
+ const mimeType = mimeMap[ext] || 'image/png'
361
+ const powHeader = await generatePowHeader(token, '/api/v0/file/upload_file')
362
+ const headers = {
363
+ ...getBaseHeaders(token),
364
+ 'x-ds-pow-response': powHeader
365
+ }
366
+
367
+ const { statusCode, json, raw } = await httpPostMultipart(
368
+ '/api/v0/file/upload_file',
369
+ headers,
370
+ {},
371
+ { fieldName: 'file', filename, mimeType, buffer }
372
+ )
373
+
374
+ if (statusCode !== 200 || !json || json.code !== 0) {
375
+ throw new Error(
376
+ `Upload gambar ke DeepSeek gagal (${statusCode}): ${JSON.stringify(json || raw)}`
377
+ )
378
+ }
379
+
380
+ const biz = unwrapBizData(json)
381
+ const fileId = biz.file_id || biz.id || biz.file?.id
382
+ if (!fileId) {
383
+ throw new Error(`Tidak menemukan file_id pada respon upload: ${JSON.stringify(biz)}`)
384
+ }
385
+ return { fileId, raw: biz }
386
+ }
387
+
388
+ /**
389
+ * Fork file ke target task 'vision' jika diperlukan oleh DeepSeek.
390
+ * Menggunakan fallback ke file_id awal bila endpoint fork gagal/tidak dibutuhkan.
391
+ */
392
+ export async function forkFileToVision(token, fileId) {
393
+ const headers = getBaseHeaders(token)
394
+ try {
395
+ const res = await httpPostJson('/api/v0/file/fork_file_task', headers, {
396
+ file_id: fileId,
397
+ target_type: 'vision'
398
+ })
399
+ if (res?.code === 0 && res.data?.biz_data) {
400
+ const biz = res.data.biz_data
401
+ return biz.file_id || biz.id || fileId
402
+ }
403
+ return fileId
404
+ } catch (err) {
405
+ console.warn('[DeepSeek-Web] forkFileToVision fallback ke fileId awal:', err?.message || err)
406
+ return fileId
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Polling status parsing/readiness file di DeepSeek Web via /api/v0/file/fetch_files.
412
+ */
413
+ export async function waitForFileReady(
414
+ token,
415
+ fileId,
416
+ { maxAttempts = 30, intervalMs = 1000, settleMs = 1200 } = {}
417
+ ) {
418
+ const headers = getBaseHeaders(token)
419
+ for (let i = 0; i < maxAttempts; i++) {
420
+ try {
421
+ const res = await httpGetJson(
422
+ `/api/v0/file/fetch_files?file_ids=${encodeURIComponent(fileId)}`,
423
+ headers
424
+ )
425
+ const biz = res?.data?.biz_data
426
+ let file = null
427
+ if (Array.isArray(biz)) {
428
+ file = biz.find((f) => f.id === fileId || f.file_id === fileId) || biz[0]
429
+ } else if (Array.isArray(biz?.files)) {
430
+ file = biz.files.find((f) => f.id === fileId || f.file_id === fileId) || biz.files[0]
431
+ } else {
432
+ file = biz
433
+ }
434
+
435
+ const status = String(file?.status || '').toUpperCase()
436
+ if (['SUCCESS', 'READY', 'DONE', 'COMPLETED', 'FINISHED', 'OK'].includes(status)) {
437
+ if (settleMs > 0) {
438
+ await new Promise((r) => setTimeout(r, settleMs))
439
+ }
440
+ return file
441
+ }
442
+
443
+ if (['FAILED', 'ERROR', 'REJECTED', 'CONTENT_EMPTY'].includes(status)) {
444
+ throw new Error(`Pemrosesan berkas DeepSeek gagal (status: ${status})`)
445
+ }
446
+ } catch (err) {
447
+ if (err.message?.includes('Pemrosesan berkas DeepSeek gagal')) {
448
+ throw err
449
+ }
450
+ }
451
+ await new Promise((r) => setTimeout(r, intervalMs))
452
+ }
453
+ return { id: fileId, status: 'ready_assumed' }
454
+ }
455
+
456
+ /**
457
+ * Generate Header Proof-of-Work (x-ds-pow-response)
458
+ */
459
+ async function generatePowHeader(token, targetPath = '/api/v0/chat/completion', wasmBuf = null) {
460
+ const solver = await getWasmSolver(wasmBuf)
461
+ const challengeRes = await httpPostJson(
462
+ '/api/v0/chat/create_pow_challenge',
463
+ getBaseHeaders(token),
464
+ { target_path: targetPath }
465
+ )
466
+
467
+ const challenge = unwrapBizData(challengeRes).challenge
468
+ const prefix = `${challenge.salt}_${challenge.expire_at}_`
469
+ const answer = solver.solve(challenge.challenge, prefix, challenge.difficulty)
470
+
471
+ if (answer === null) {
472
+ throw new Error('Gagal menyelesaikan Proof-of-Work DeepSeek (Challenge kadaluarsa/gagal)')
473
+ }
474
+
475
+ const payload = {
476
+ algorithm: challenge.algorithm,
477
+ challenge: challenge.challenge,
478
+ salt: challenge.salt,
479
+ answer: answer,
480
+ signature: challenge.signature,
481
+ target_path: challenge.target_path
482
+ }
483
+
484
+ return Buffer.from(JSON.stringify(payload)).toString('base64')
485
+ }
486
+
487
+ /**
488
+ * Membuat chat session ID baru
489
+ */
490
+ export async function createChatSession(token) {
491
+ const res = await httpPostJson('/api/v0/chat_session/create', getBaseHeaders(token), {})
492
+ const biz = unwrapBizData(res)
493
+ return biz.chat_session.id
494
+ }
495
+
496
+ /**
497
+ * Eksekusi tunggal request ke DeepSeek Web RPC (langsung instan tanpa delay jika normal)
498
+ */
499
+ async function _executeSingleDeepSeekCall(
500
+ prompt,
501
+ modelName = 'deepseek-chat',
502
+ token = '',
503
+ options = {}
504
+ ) {
505
+ // Auto-clear session cache jika token berubah
506
+ ensureTokenSession(token)
507
+
508
+ const isSmallTask = !!options.isSmallTask
509
+ const targetCache = isSmallTask ? utilitySessionCache : activeSessionCache
510
+ const markSessionId = options.sessionId || '1'
511
+ const cacheKey = getSessionCacheKey(token, markSessionId, isSmallTask)
512
+
513
+ let sessionId = targetCache.get(cacheKey)
514
+ let sessionState = sessionId ? sessionStateMap.get(sessionId) : null
515
+
516
+ const now = Date.now()
517
+ const MAX_TURNS_PER_SESSION = 10
518
+ const MAX_IDLE_MS = 2 * 60 * 60 * 1000 // 2 jam
519
+
520
+ // Rotasi otomatis jika melebihi batas turn atau idle > 2 jam
521
+ if (
522
+ sessionId &&
523
+ sessionState &&
524
+ (sessionState.turnCount >= MAX_TURNS_PER_SESSION || now - sessionState.lastUsedAt > MAX_IDLE_MS)
525
+ ) {
526
+ console.log(
527
+ `[DeepSeek-Web] Merotasi sesi ${isSmallTask ? '(utility)' : `(${markSessionId})`} (turnCount: ${sessionState.turnCount}). Membuat chat session baru...`
528
+ )
529
+ targetCache.delete(cacheKey)
530
+ sessionStateMap.delete(sessionId)
531
+ sessionId = null
532
+ sessionState = null
533
+ }
534
+
535
+ // Buat session baru jika tidak ada
536
+ if (!sessionId) {
537
+ sessionId = await createChatSession(token)
538
+ targetCache.set(cacheKey, sessionId)
539
+ sessionState = { lastMessageId: null, turnCount: 0, createdAt: now, lastUsedAt: now }
540
+ sessionStateMap.set(sessionId, sessionState)
541
+ } else if (!sessionState) {
542
+ sessionState = { lastMessageId: null, turnCount: 0, createdAt: now, lastUsedAt: now }
543
+ sessionStateMap.set(sessionId, sessionState)
544
+ }
545
+
546
+ // Tentukan parentMessageId: prioritaskan options, jika undefined ambil dari state chaining
547
+ let parentMessageId = null
548
+ if (options.parentMessageId !== undefined) {
549
+ parentMessageId = options.parentMessageId
550
+ } else if (sessionState?.lastMessageId) {
551
+ parentMessageId = sessionState.lastMessageId
552
+ }
553
+
554
+ const { onDelta = null, wasmBuffer = null, refFileIds = [] } = options
555
+
556
+ // Jeda acak manusiawi (jitter delay 350-800ms) untuk menghindari deteksi burst
557
+ const jitterMs = Math.floor(Math.random() * (800 - 350 + 1)) + 350
558
+ await new Promise((resolve) => setTimeout(resolve, jitterMs))
559
+
560
+ const reqModel = (modelName || 'deepseek-chat').toLowerCase()
561
+ let selected = DEEPSEEK_WEB_MODELS[reqModel] || DEEPSEEK_WEB_MODELS['deepseek-chat']
562
+ const powHeader = await generatePowHeader(token, '/api/v0/chat/completion', wasmBuffer)
563
+
564
+ const bodyData = {
565
+ chat_session_id: sessionId,
566
+ parent_message_id: parentMessageId,
567
+ prompt: prompt,
568
+ ref_file_ids: Array.isArray(refFileIds) ? refFileIds : [],
569
+ thinking_enabled: selected.thinking,
570
+ search_enabled: selected.search,
571
+ action: null,
572
+ preempt: false
573
+ }
574
+
575
+ if (!parentMessageId) {
576
+ bodyData.model_type = selected.modelType
577
+ }
578
+
579
+ const payloadStr = JSON.stringify(bodyData)
580
+ const headers = {
581
+ ...getBaseHeaders(token),
582
+ accept: 'text/event-stream, */*',
583
+ 'cache-control': 'no-cache',
584
+ 'content-type': 'application/json',
585
+ 'content-length': Buffer.byteLength(payloadStr),
586
+ 'x-ds-pow-response': powHeader
587
+ }
588
+
589
+ return new Promise((resolve, reject) => {
590
+ const reqOptions = {
591
+ hostname: BASE_HOST,
592
+ port: 443,
593
+ path: '/api/v0/chat/completion',
594
+ method: 'POST',
595
+ headers: headers
596
+ }
597
+
598
+ const req = https.request(reqOptions, (res) => {
599
+ const encoding = (res.headers['content-encoding'] || '').toLowerCase()
600
+ console.log(
601
+ `[DeepSeek-Web] Status: ${res.statusCode}, encoding: "${encoding}", type: "${res.headers['content-type']}"`
602
+ )
603
+
604
+ let stream = res
605
+ if (encoding === 'gzip') {
606
+ stream = res.pipe(zlib.createGunzip())
607
+ } else if (encoding === 'br') {
608
+ stream = res.pipe(zlib.createBrotliDecompress())
609
+ } else if (encoding === 'deflate') {
610
+ stream = res.pipe(zlib.createInflate())
611
+ }
612
+
613
+ if (res.statusCode !== 200) {
614
+ let errBody = ''
615
+ stream.on('data', (d) => (errBody += d.toString()))
616
+ stream.on('end', () => {
617
+ reject(new Error(`DeepSeek Server menolak permintaan (${res.statusCode}): ${errBody}`))
618
+ })
619
+ return
620
+ }
621
+
622
+ let buffer = ''
623
+ let fullContent = ''
624
+ let reasoningContent = ''
625
+ let activePath = null
626
+ let lastReceivedPayload = ''
627
+ let responseMessageId = null
628
+
629
+ const processLine = (line) => {
630
+ const trimmed = line.trim()
631
+ if (!trimmed) return
632
+
633
+ // Skip SSE event lines that are not "data"
634
+ if (trimmed.startsWith('event:')) return
635
+
636
+ let payload = trimmed
637
+ if (trimmed.startsWith('data:')) {
638
+ payload = trimmed.slice(5).trim()
639
+ }
640
+ if (!payload || payload === '[DONE]') return
641
+ lastReceivedPayload = payload
642
+
643
+ try {
644
+ const obj = JSON.parse(payload)
645
+
646
+ // Tangkap message_id respon assistant
647
+ if (obj.v?.response?.message_id) {
648
+ responseMessageId = obj.v.response.message_id
649
+ }
650
+
651
+ // Abaikan frame status seperti response/status: "FINISHED" atau "WIP"
652
+ if (
653
+ obj.p === 'response/status' ||
654
+ obj.p?.includes('status') ||
655
+ obj.v === 'FINISHED' ||
656
+ obj.v === 'WIP'
657
+ ) {
658
+ return
659
+ }
660
+
661
+ // 0. Deteksi error resmi dari server DeepSeek
662
+ if (obj.click_behavior !== undefined || obj.auto_resume !== undefined) {
663
+ // Jika sudah ada content, ini bukan error - akhir stream normal
664
+ if (fullContent) return
665
+ // Content kosong + click_behavior = session expired
666
+ targetCache.delete(token)
667
+ targetCache.delete(cacheKey)
668
+ if (sessionId) sessionStateMap.delete(sessionId)
669
+ reject(
670
+ new Error(
671
+ 'DeepSeek Web session expired atau tidak valid. Session di-clear, silakan coba lagi.'
672
+ )
673
+ )
674
+ return
675
+ }
676
+ if (obj.code !== undefined && obj.code !== 0) {
677
+ const errMsg = obj.msg || obj.message || JSON.stringify(obj)
678
+ const isFrequent =
679
+ errMsg.toLowerCase().includes('frequent') ||
680
+ errMsg.toLowerCase().includes('too many') ||
681
+ errMsg.toLowerCase().includes('terlalu sering')
682
+ if (!isFrequent) {
683
+ targetCache.delete(token)
684
+ targetCache.delete(cacheKey)
685
+ if (sessionId) sessionStateMap.delete(sessionId)
686
+ }
687
+ reject(new Error(`DeepSeek Server Error (${obj.code}): ${errMsg}`))
688
+ return
689
+ }
690
+ if (obj.data?.biz_code !== undefined && obj.data.biz_code !== 0) {
691
+ const muteUntil = obj.data.biz_data?.mute_until
692
+ ? ` (sampai ${new Date(obj.data.biz_data.mute_until * 1000).toLocaleTimeString('id-ID')})`
693
+ : ''
694
+ const errMsg = obj.data.biz_msg || `Kode bisnis ${obj.data.biz_code}`
695
+ const isFrequent =
696
+ errMsg.toLowerCase().includes('frequent') ||
697
+ errMsg.toLowerCase().includes('too many') ||
698
+ errMsg.toLowerCase().includes('terlalu sering')
699
+ if (!isFrequent) {
700
+ targetCache.delete(token)
701
+ targetCache.delete(cacheKey)
702
+ if (sessionId) sessionStateMap.delete(sessionId)
703
+ }
704
+ reject(
705
+ new Error(
706
+ `Akun DeepSeek Web kamu sedang dibatasi sementara oleh DeepSeek: "${errMsg}"${muteUntil}. Tunggu beberapa saat atau ganti token akun baru.`
707
+ )
708
+ )
709
+ return
710
+ }
711
+ if (obj.error || obj.error_msg) {
712
+ const errMsg = obj.error?.message || obj.error_msg || JSON.stringify(obj)
713
+ reject(new Error(`DeepSeek Server Error: ${errMsg}`))
714
+ return
715
+ }
716
+
717
+ const v = obj.v
718
+
719
+ // 1. Snapshot Response Frame
720
+ if (v && typeof v === 'object') {
721
+ if (v.response) {
722
+ for (const frag of v.response.fragments || []) {
723
+ if (frag.content) {
724
+ if (frag.type === 'THINKING') {
725
+ reasoningContent = frag.content
726
+ onDelta?.({ type: 'thinking', delta: frag.content, full: reasoningContent })
727
+ } else {
728
+ activePath = 'response/fragments/-1/content'
729
+ // FIX: Gabungkan fragment, jangan overwrite
730
+ if (fullContent && fullContent !== frag.content) {
731
+ fullContent += frag.content
732
+ } else {
733
+ fullContent = frag.content
734
+ }
735
+ onDelta?.({ type: 'content', delta: frag.content, full: fullContent })
736
+ }
737
+ }
738
+ }
739
+ return
740
+ }
741
+ if (v.content && typeof v.content === 'string') {
742
+ fullContent += v.content
743
+ onDelta?.({ type: 'content', delta: v.content, full: fullContent })
744
+ return
745
+ }
746
+ if (v.thinking_content && typeof v.thinking_content === 'string') {
747
+ reasoningContent += v.thinking_content
748
+ onDelta?.({ type: 'thinking', delta: v.thinking_content, full: reasoningContent })
749
+ return
750
+ }
751
+ }
752
+
753
+ // 2. Path-Setting Frame
754
+ if (obj.p) {
755
+ activePath = obj.p
756
+ if (typeof v === 'string') {
757
+ if (activePath.includes('thinking')) {
758
+ reasoningContent += v
759
+ onDelta?.({ type: 'thinking', delta: v, full: reasoningContent })
760
+ } else {
761
+ fullContent += v
762
+ onDelta?.({ type: 'content', delta: v, full: fullContent })
763
+ }
764
+ }
765
+ return
766
+ }
767
+
768
+ // 3. Continuous Append Frame
769
+ if (typeof v === 'string') {
770
+ if (activePath && activePath.includes('thinking')) {
771
+ reasoningContent += v
772
+ onDelta?.({ type: 'thinking', delta: v, full: reasoningContent })
773
+ } else {
774
+ fullContent += v
775
+ onDelta?.({ type: 'content', delta: v, full: fullContent })
776
+ }
777
+ }
778
+ } catch {
779
+ // Abaikan chunk malformed
780
+ }
781
+ }
782
+
783
+ stream.on('data', (chunk) => {
784
+ buffer += chunk.toString()
785
+ const lines = buffer.split('\n')
786
+ buffer = lines.pop()
787
+
788
+ for (const line of lines) {
789
+ processLine(line)
790
+ }
791
+ })
792
+
793
+ stream.on('end', () => {
794
+ if (buffer && buffer.trim()) {
795
+ processLine(buffer.trim())
796
+ }
797
+
798
+ // Sanitasi trailing status jika ada yang lolos
799
+ if (fullContent) {
800
+ fullContent = fullContent.replace(/\s*(?:FINISHED|FINISH|DONE)\b.*$/i, '').trim()
801
+ }
802
+
803
+ // Fallback: Jika content kosong tapi thinking terisi, gunakan thinking sebagai jawaban
804
+ if (!fullContent && reasoningContent) {
805
+ fullContent = reasoningContent
806
+ reasoningContent = null
807
+ }
808
+
809
+ if (!fullContent) {
810
+ console.warn(
811
+ `[DeepSeek-Web] Empty response on end. Last payload:`,
812
+ lastReceivedPayload,
813
+ `Buffer:`,
814
+ buffer
815
+ )
816
+ reject(
817
+ new Error(
818
+ `Gagal mengekstrak teks balasan dari streaming DeepSeek Web. Status: ${res.statusCode}, Tipe: ${res.headers['content-type']}, Respons server: ${lastReceivedPayload || buffer || 'tidak ada data streaming'}`
819
+ )
820
+ )
821
+ return
822
+ }
823
+
824
+ // Simpan progress chaining sesi jika berhasil
825
+ if (sessionId && sessionStateMap.has(sessionId)) {
826
+ const state = sessionStateMap.get(sessionId)
827
+ if (responseMessageId) {
828
+ state.lastMessageId = responseMessageId
829
+ }
830
+ state.turnCount = (state.turnCount || 0) + 1
831
+ state.lastUsedAt = Date.now()
832
+ }
833
+
834
+ resolve({
835
+ text: fullContent,
836
+ thinking: reasoningContent || null,
837
+ sessionId: sessionId,
838
+ messageId: responseMessageId || null
839
+ })
840
+ })
841
+ })
842
+
843
+ req.on('error', reject)
844
+ req.write(payloadStr)
845
+ req.end()
846
+ })
847
+ }
848
+
849
+ /**
850
+ * Fungsi Utama: Generate respons dari DeepSeek Web RPC dengan auto-retry hingga 50x dan jeda proteksi rate limit
851
+ *
852
+ * @param {string} prompt - Pertanyaan/pesan pengguna
853
+ * @param {string} modelName - 'deepseek-chat' | 'deepseek-reasoner' | 'deepseek-search'
854
+ * @param {string} token - DeepSeek Bearer token (dari localStorage userToken)
855
+ * @param {object} options - Opsi tambahan: sessionId, parentMessageId, onDelta, onStatus, wasmBuffer, maxAttempts, retryDelayMs
856
+ */
857
+ export async function generateDeepSeekResponse(
858
+ prompt,
859
+ modelName = 'deepseek-chat',
860
+ token = '',
861
+ options = {}
862
+ ) {
863
+ if (!token) {
864
+ throw new Error(
865
+ 'DeepSeek User Token (Bearer) dibutuhkan. Silakan ambil dari localStorage.userToken.'
866
+ )
867
+ }
868
+
869
+ const maxAttempts = options.maxAttempts ?? 50
870
+ const retryDelayMs = options.retryDelayMs ?? 5000
871
+
872
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
873
+ try {
874
+ return await _executeSingleDeepSeekCall(prompt, modelName, token, options)
875
+ } catch (err) {
876
+ const errMsg = err?.message || ''
877
+ const isRetryable =
878
+ errMsg.toLowerCase().includes('session expired') ||
879
+ errMsg.toLowerCase().includes('tidak valid') ||
880
+ errMsg.toLowerCase().includes('invalid ref file id') ||
881
+ errMsg.toLowerCase().includes('frequent') ||
882
+ errMsg.toLowerCase().includes('too many') ||
883
+ errMsg.toLowerCase().includes('terlalu sering') ||
884
+ errMsg.toLowerCase().includes('rate limit') ||
885
+ errMsg.toLowerCase().includes('dibatasi') ||
886
+ errMsg.toLowerCase().includes('menolak permintaan') ||
887
+ errMsg.toLowerCase().includes('tidak ada data streaming') ||
888
+ errMsg.toLowerCase().includes('503') ||
889
+ errMsg.toLowerCase().includes('502') ||
890
+ errMsg.toLowerCase().includes('429')
891
+
892
+ if (isRetryable && attempt < maxAttempts) {
893
+ // Setiap 5 kali kegagalan berulang, reset session cache agar membuat session baru
894
+ if (attempt % 5 === 0) {
895
+ activeSessionCache.delete(token)
896
+ utilitySessionCache.delete(token)
897
+ const markSessionId = options.sessionId || '1'
898
+ const key = getSessionCacheKey(token, markSessionId, !!options.isSmallTask)
899
+ const targetCache = options.isSmallTask ? utilitySessionCache : activeSessionCache
900
+ const sId = targetCache.get(key)
901
+ if (sId) sessionStateMap.delete(sId)
902
+ targetCache.delete(key)
903
+ }
904
+
905
+ console.warn(
906
+ `[DeepSeek-Web] Kendala DeepSeek: "${errMsg}". Mencoba ulang (${attempt}/${maxAttempts}) dalam ${retryDelayMs / 1000}s...`
907
+ )
908
+ if (typeof options.onStatus === 'function') {
909
+ options.onStatus(
910
+ `DeepSeek sibuk (${errMsg.substring(0, 35)}...). Jeda ${retryDelayMs / 1000}s (Percobaan ${attempt}/${maxAttempts})...`
911
+ )
912
+ }
913
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs))
914
+ continue
915
+ }
916
+ throw err
917
+ }
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Fungsi tingkat tinggi Vision: upload gambar -> wait ready -> generate respons chat DeepSeek.
923
+ *
924
+ * @param {string} prompt - Pertanyaan tentang gambar
925
+ * @param {string|Buffer} imagePathOrBuffer - Path file gambar atau Buffer
926
+ * @param {string} token - DeepSeek Bearer token
927
+ * @param {object} options - Opsi tambahan (filename, modelName, onDelta, onStatus, dll)
928
+ */
929
+ export async function generateDeepSeekVisionResponse(
930
+ prompt,
931
+ imagePathOrBuffer,
932
+ token,
933
+ options = {}
934
+ ) {
935
+ if (!token) {
936
+ throw new Error('DeepSeek User Token (Bearer) dibutuhkan.')
937
+ }
938
+
939
+ if (typeof options.onStatus === 'function') {
940
+ options.onStatus('Mengupload gambar ke DeepSeek...')
941
+ }
942
+ const { fileId } = await uploadImageFile(token, imagePathOrBuffer, options.filename)
943
+
944
+ if (typeof options.onStatus === 'function') {
945
+ options.onStatus('Menunggu DeepSeek selesai memproses gambar...')
946
+ }
947
+ await waitForFileReady(token, fileId)
948
+
949
+ const modelName = options.modelName || 'deepseek-chat'
950
+ const existingRefs = Array.isArray(options.refFileIds) ? options.refFileIds : []
951
+
952
+ return generateDeepSeekResponse(prompt, modelName, token, {
953
+ ...options,
954
+ refFileIds: [...new Set([...existingRefs, fileId])]
955
+ })
956
+ }