@adiholick/nvstudio-mcp 2.0.3 → 2.1.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.
- package/dist/index.js +53 -9
- package/dist/roblox-bridge.js +241 -64
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +287 -205
- package/studio-plugin/nvstudio_mcp.rbxmx +288 -206
|
@@ -378,153 +378,136 @@ end
|
|
|
378
378
|
<int64 name="SourceAssetId">-1</int64>
|
|
379
379
|
<BinaryString name="Tags"></BinaryString>
|
|
380
380
|
<token name="RunContext">0</token>
|
|
381
|
-
<ProtectedString name="Source"><![CDATA[
|
|
382
|
-
|
|
381
|
+
<ProtectedString name="Source"><![CDATA[-- ══════════════════════════════════════════════════════════════════════
|
|
382
|
+
-- NVStudio MCP Plugin v2.1.0
|
|
383
|
+
-- Arsitektur: SSE (Server-Sent Events) via WebStreamClient
|
|
384
|
+
-- Plugin membuka koneksi stream persisten ke server Node.js.
|
|
385
|
+
-- Server PUSH task langsung — tidak ada polling loop.
|
|
386
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
387
|
+
|
|
388
|
+
local HttpService = game:GetService("HttpService")
|
|
389
|
+
local LogService = game:GetService("LogService")
|
|
383
390
|
|
|
384
|
-
|
|
385
|
-
local
|
|
391
|
+
-- URL Endpoints
|
|
392
|
+
local BASE_URL = "http://localhost:3055"
|
|
393
|
+
local STREAM_URL = BASE_URL .. "/api/stream"
|
|
394
|
+
local RESPONSE_URL = BASE_URL .. "/api/response" -- /api/response/:taskId via POST
|
|
395
|
+
local PING_URL = BASE_URL .. "/api/ping"
|
|
386
396
|
|
|
387
|
-
--
|
|
388
|
-
local STUDIO_ID
|
|
397
|
+
-- ID unik per sesi Studio (jika dua Studio terbuka, keduanya didaftarkan terpisah)
|
|
398
|
+
local STUDIO_ID = HttpService:GenerateGUID(false)
|
|
389
399
|
|
|
390
|
-
--
|
|
400
|
+
-- ── UI Module ──────────────────────────────────────────────────────────────────
|
|
391
401
|
local uiModule = script.Parent:FindFirstChild("ui_manager")
|
|
392
402
|
local ui = nil
|
|
393
403
|
|
|
394
|
-
--
|
|
395
|
-
local toolbar
|
|
404
|
+
-- ── Toolbar ───────────────────────────────────────────────────────────────────
|
|
405
|
+
local toolbar = plugin:CreateToolbar("NVStudio MCP")
|
|
396
406
|
local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722", "NVStudio")
|
|
397
407
|
|
|
398
|
-
--
|
|
408
|
+
-- ── State ─────────────────────────────────────────────────────────────────────
|
|
399
409
|
local ctx = {
|
|
400
410
|
scriptHistory = {},
|
|
401
|
-
recentLogs
|
|
402
|
-
HttpService
|
|
403
|
-
studioId
|
|
411
|
+
recentLogs = {},
|
|
412
|
+
HttpService = HttpService,
|
|
413
|
+
studioId = STUDIO_ID,
|
|
404
414
|
}
|
|
405
|
-
local MAX_LOGS
|
|
406
|
-
local isConnected
|
|
407
|
-
local
|
|
408
|
-
local
|
|
409
|
-
local isClickDebounce
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
415
|
+
local MAX_LOGS = 50
|
|
416
|
+
local isConnected = false -- apakah SSE stream aktif
|
|
417
|
+
local mcpAgentActive = false -- apakah Antigravity/Cursor sedang konek ke MCP
|
|
418
|
+
local isManualDisconnect = false
|
|
419
|
+
local isClickDebounce = false
|
|
420
|
+
local currentStream = nil -- WebStreamClient aktif
|
|
421
|
+
local streamConnections = {} -- RBXScriptConnection SSE
|
|
422
|
+
|
|
423
|
+
-- Reconnect config (exponential backoff)
|
|
424
|
+
local RECONNECT_DELAY_INITIAL = 0.5
|
|
425
|
+
local RECONNECT_DELAY_MAX = 10
|
|
426
|
+
local reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
427
|
+
|
|
428
|
+
-- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
|
|
429
|
+
local skills = {}
|
|
430
|
+
for _, module in ipairs(script.Parent:GetChildren()) do
|
|
431
|
+
if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
|
|
432
|
+
local ok, result = pcall(require, module)
|
|
433
|
+
if ok then
|
|
434
|
+
skills[module.Name] = result
|
|
435
|
+
else
|
|
436
|
+
warn("[NVStudio] Gagal load module '" .. module.Name .. "': " .. tostring(result))
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
414
440
|
|
|
415
|
-
-- Inisialisasi UI
|
|
441
|
+
-- ── Inisialisasi UI ──────────────────────────────────────────────────────────
|
|
416
442
|
if uiModule then
|
|
417
443
|
local UIManager = require(uiModule)
|
|
418
444
|
ui = UIManager.init(plugin, STUDIO_ID)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
ui:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
-- Sync state tombol toolbar dengan status widget
|
|
445
|
+
|
|
446
|
+
ui:setAutoConnectState(true)
|
|
447
|
+
ui:setStatus("waiting")
|
|
448
|
+
|
|
425
449
|
if ui.widget then
|
|
426
450
|
toggleBtn:SetActive(ui.widget.Enabled)
|
|
427
451
|
ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
|
|
428
452
|
toggleBtn:SetActive(ui.widget.Enabled)
|
|
429
453
|
end)
|
|
430
454
|
end
|
|
431
|
-
|
|
432
|
-
-- Event Toggle GUI Panel
|
|
455
|
+
|
|
433
456
|
toggleBtn.Click:Connect(function()
|
|
434
457
|
ui:toggleVisibility()
|
|
435
458
|
end)
|
|
436
|
-
|
|
437
|
-
--
|
|
459
|
+
|
|
460
|
+
-- Tombol Connect / Disconnect
|
|
438
461
|
ui.connectBtn.MouseButton1Click:Connect(function()
|
|
439
462
|
if isClickDebounce then return end
|
|
440
463
|
isClickDebounce = true
|
|
441
|
-
|
|
464
|
+
|
|
442
465
|
if not isConnected then
|
|
443
|
-
-- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
|
|
444
466
|
isManualDisconnect = false
|
|
445
|
-
|
|
467
|
+
reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
446
468
|
ui:setStatus("waiting")
|
|
447
|
-
ui:addLog("[System]
|
|
448
|
-
|
|
469
|
+
ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
|
|
470
|
+
task.spawn(connectSSE)
|
|
449
471
|
else
|
|
450
|
-
|
|
451
|
-
|
|
472
|
+
isManualDisconnect = true
|
|
473
|
+
disconnectSSE()
|
|
452
474
|
ui:setStatus("disconnected")
|
|
453
475
|
ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
|
|
454
476
|
end
|
|
455
|
-
|
|
456
|
-
task.delay(0.3, function()
|
|
457
|
-
isClickDebounce = false
|
|
458
|
-
end)
|
|
459
|
-
end)
|
|
460
|
-
|
|
461
|
-
-- Event Custom Checkbox (Auto-Connect)
|
|
462
|
-
ui.autoConnectBtn.MouseButton1Click:Connect(function()
|
|
463
|
-
autoConnect = not autoConnect
|
|
464
|
-
plugin:SetSetting("AutoConnect", autoConnect)
|
|
465
|
-
ui:setAutoConnectState(autoConnect)
|
|
466
|
-
ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
|
|
477
|
+
|
|
478
|
+
task.delay(0.3, function() isClickDebounce = false end)
|
|
467
479
|
end)
|
|
468
480
|
else
|
|
469
|
-
-- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
|
|
470
481
|
toggleBtn.Click:Connect(function()
|
|
471
|
-
print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan
|
|
482
|
+
print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan. GUI gagal dimuat!")
|
|
472
483
|
end)
|
|
473
484
|
end
|
|
474
485
|
|
|
475
|
-
--
|
|
486
|
+
-- ── Log Capture ──────────────────────────────────────────────────────────────
|
|
476
487
|
local function addSystemLog(msg, msgType)
|
|
477
|
-
local color
|
|
488
|
+
local color = Color3.fromRGB(200, 200, 200)
|
|
478
489
|
local prefix = "[INFO] "
|
|
479
490
|
if msgType == Enum.MessageType.MessageError then
|
|
480
|
-
prefix = "[ERROR] "
|
|
481
|
-
color = Color3.fromRGB(255, 100, 100)
|
|
491
|
+
prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
|
|
482
492
|
elseif msgType == Enum.MessageType.MessageWarning then
|
|
483
|
-
prefix = "[WARN] "
|
|
484
|
-
color = Color3.fromRGB(255, 200, 100)
|
|
493
|
+
prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
|
|
485
494
|
end
|
|
486
|
-
|
|
487
495
|
local fullMsg = prefix .. msg
|
|
488
|
-
|
|
489
|
-
-- 1. Simpan di memori untuk disedot AI
|
|
490
496
|
table.insert(ctx.recentLogs, fullMsg)
|
|
491
|
-
if #ctx.recentLogs > MAX_LOGS then
|
|
492
|
-
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
-- 2. Cetak ke Terminal GUI Plugin
|
|
496
|
-
if ui then
|
|
497
|
-
ui:addLog(fullMsg, color)
|
|
498
|
-
end
|
|
497
|
+
if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
|
|
498
|
+
if ui then ui:addLog(fullMsg, color) end
|
|
499
499
|
end
|
|
500
|
-
|
|
501
|
-
-- Ikat listener output
|
|
502
500
|
LogService.MessageOut:Connect(addSystemLog)
|
|
503
501
|
|
|
504
|
-
--
|
|
505
|
-
local skills = {}
|
|
506
|
-
for _, module in ipairs(script.Parent:GetChildren()) do
|
|
507
|
-
if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
|
|
508
|
-
skills[module.Name] = require(module)
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
-- Fungsi Helper Pathing
|
|
502
|
+
-- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
|
|
513
503
|
local function getInstanceFromPath(pathString)
|
|
514
|
-
local parts
|
|
504
|
+
local parts = string.split(pathString, ".")
|
|
515
505
|
local current = game
|
|
516
|
-
|
|
517
506
|
if parts[1] == "game" then table.remove(parts, 1) end
|
|
518
|
-
|
|
519
|
-
-- Handle common Services directly to ensure reliability
|
|
520
507
|
pcall(function()
|
|
521
508
|
local svc = game:GetService(parts[1])
|
|
522
|
-
if svc then
|
|
523
|
-
current = svc
|
|
524
|
-
table.remove(parts, 1)
|
|
525
|
-
end
|
|
509
|
+
if svc then current = svc; table.remove(parts, 1) end
|
|
526
510
|
end)
|
|
527
|
-
|
|
528
511
|
for _, part in ipairs(parts) do
|
|
529
512
|
local found = current:FindFirstChild(part)
|
|
530
513
|
if not found then return nil end
|
|
@@ -533,160 +516,259 @@ local function getInstanceFromPath(pathString)
|
|
|
533
516
|
return current
|
|
534
517
|
end
|
|
535
518
|
|
|
536
|
-
--
|
|
519
|
+
-- ── Proses Task dari Server ───────────────────────────────────────────────────
|
|
537
520
|
local function processTask(taskData)
|
|
538
521
|
if not taskData or not taskData.id or not taskData.command then return nil end
|
|
539
|
-
|
|
540
|
-
if ui then
|
|
541
|
-
ui:addLog("[AI Command]
|
|
522
|
+
|
|
523
|
+
if ui then
|
|
524
|
+
ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
|
|
542
525
|
end
|
|
543
|
-
|
|
526
|
+
|
|
544
527
|
local targetInstance = getInstanceFromPath(taskData.target)
|
|
545
528
|
if not targetInstance then
|
|
546
|
-
return {
|
|
547
|
-
id = taskData.id,
|
|
548
|
-
status = "error",
|
|
549
|
-
error = "Target path tidak ditemukan: " .. taskData.target
|
|
550
|
-
}
|
|
529
|
+
return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
|
|
551
530
|
end
|
|
552
531
|
|
|
553
532
|
local skillFunc = skills[taskData.command]
|
|
554
533
|
if not skillFunc then
|
|
555
|
-
return {
|
|
556
|
-
id = taskData.id,
|
|
557
|
-
status = "error",
|
|
558
|
-
error = "Command tidak dikenali oleh Studio: " .. taskData.command
|
|
559
|
-
}
|
|
534
|
+
return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
|
|
560
535
|
end
|
|
561
536
|
|
|
562
|
-
-- Eksekusi skill AI
|
|
563
537
|
local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
|
|
564
|
-
result.id
|
|
565
|
-
|
|
566
|
-
-- Cetak hasil eksekusi ke Terminal GUI
|
|
538
|
+
result.id = taskData.id
|
|
539
|
+
|
|
567
540
|
if ui then
|
|
568
541
|
local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
|
|
569
542
|
ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
|
|
570
543
|
end
|
|
571
|
-
|
|
572
|
-
return result
|
|
573
|
-
end
|
|
574
544
|
|
|
575
|
-
|
|
576
|
-
local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
|
|
577
|
-
|
|
578
|
-
function stopPolling()
|
|
579
|
-
isConnected = false
|
|
580
|
-
isManualDisconnect = true
|
|
581
|
-
-- isPolling akan menjadi false secara otomatis saat loop keluar
|
|
545
|
+
return result
|
|
582
546
|
end
|
|
583
547
|
|
|
584
|
-
--
|
|
585
|
-
function
|
|
586
|
-
if isPolling then return end
|
|
587
|
-
isPolling = true
|
|
588
|
-
|
|
548
|
+
-- ── Kirim Respons ke Server ───────────────────────────────────────────────────
|
|
549
|
+
local function sendResponse(taskId, resultData)
|
|
589
550
|
task.spawn(function()
|
|
590
|
-
|
|
591
|
-
local
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
551
|
+
local ok, err = pcall(function()
|
|
552
|
+
local json = HttpService:JSONEncode(resultData)
|
|
553
|
+
-- Coba endpoint baru dulu (/api/response/:taskId), fallback ke lama
|
|
554
|
+
local success = pcall(function()
|
|
555
|
+
HttpService:PostAsync(
|
|
556
|
+
RESPONSE_URL .. "/" .. taskId,
|
|
557
|
+
json,
|
|
558
|
+
Enum.HttpContentType.ApplicationJson
|
|
559
|
+
)
|
|
595
560
|
end)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if not isConnected then
|
|
599
|
-
break
|
|
561
|
+
if not success then
|
|
562
|
+
HttpService:PostAsync(RESPONSE_URL, json, Enum.HttpContentType.ApplicationJson)
|
|
600
563
|
end
|
|
564
|
+
end)
|
|
565
|
+
if not ok and ui then
|
|
566
|
+
ui:addLog("[System] Gagal kirim response: " .. tostring(err), Color3.fromRGB(255, 100, 100))
|
|
567
|
+
end
|
|
568
|
+
end)
|
|
569
|
+
end
|
|
601
570
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
571
|
+
-- ── Proses satu baris data SSE ────────────────────────────────────────────────
|
|
572
|
+
local function handleSSEMessage(rawMessage)
|
|
573
|
+
-- Strip prefix "data: " jika ada (beberapa Studio version memberikannya)
|
|
574
|
+
local payload = rawMessage
|
|
575
|
+
local normalized = rawMessage:gsub("\r\n", "\n"):gsub("\r", "\n")
|
|
576
|
+
if normalized:sub(1, 5) == "data:" then
|
|
577
|
+
payload = normalized:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
|
|
578
|
+
end
|
|
579
|
+
if payload == "" then return end
|
|
605
580
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
581
|
+
local ok, decoded = pcall(function() return HttpService:JSONDecode(payload) end)
|
|
582
|
+
if not ok or type(decoded) ~= "table" then return end
|
|
583
|
+
|
|
584
|
+
local kind = decoded.kind
|
|
585
|
+
|
|
586
|
+
-- ── heartbeat ────────────────────────────────────────────────────
|
|
587
|
+
if kind == "heartbeat" then
|
|
588
|
+
-- Server masih hidup, tidak perlu tindakan khusus
|
|
589
|
+
return
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
-- ── status ───────────────────────────────────────────────────────
|
|
593
|
+
if kind == "status" then
|
|
594
|
+
local newMcpActive = decoded.mcpConnected == true
|
|
595
|
+
if newMcpActive ~= mcpAgentActive then
|
|
596
|
+
mcpAgentActive = newMcpActive
|
|
597
|
+
if ui then
|
|
598
|
+
if mcpAgentActive then
|
|
599
|
+
ui:setStatus("connected")
|
|
600
|
+
ui:addLog("[System] IDE Agent (Antigravity/Cursor) terhubung ke MCP.", Color3.fromRGB(100, 255, 100))
|
|
601
|
+
else
|
|
602
|
+
ui:setStatus("waiting")
|
|
603
|
+
ui:addLog("[System] IDE Agent terputus dari MCP.", Color3.fromRGB(255, 200, 100))
|
|
618
604
|
end
|
|
619
|
-
|
|
620
|
-
-- Fast-polling (long-polling yield di server Node.js)
|
|
621
|
-
-- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
|
|
622
|
-
task.wait(0.05)
|
|
623
|
-
else
|
|
624
|
-
-- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
|
|
625
|
-
if ui then ui:setStatus("waiting") end
|
|
626
|
-
task.wait(1)
|
|
627
605
|
end
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
606
|
+
end
|
|
607
|
+
return
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
-- ── request (task dari AI agent) ─────────────────────────────────
|
|
611
|
+
if kind == "request" then
|
|
612
|
+
local taskId = decoded.requestId
|
|
613
|
+
local command = decoded.command
|
|
614
|
+
local target = decoded.target
|
|
615
|
+
local data = decoded.data
|
|
616
|
+
|
|
617
|
+
if not taskId or not command then return end
|
|
618
|
+
|
|
619
|
+
-- Proses task di coroutine terpisah agar tidak memblokir stream
|
|
620
|
+
task.spawn(function()
|
|
621
|
+
local resultData = processTask({
|
|
622
|
+
id = taskId,
|
|
623
|
+
command = command,
|
|
624
|
+
target = target or "",
|
|
625
|
+
data = data,
|
|
626
|
+
})
|
|
627
|
+
if resultData then
|
|
628
|
+
sendResponse(taskId, resultData)
|
|
632
629
|
end
|
|
630
|
+
end)
|
|
631
|
+
return
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
-- ── Tutup SSE stream yang aktif ───────────────────────────────────────────────
|
|
636
|
+
function disconnectSSE()
|
|
637
|
+
isConnected = false
|
|
638
|
+
for _, conn in ipairs(streamConnections) do
|
|
639
|
+
pcall(function() conn:Disconnect() end)
|
|
640
|
+
end
|
|
641
|
+
streamConnections = {}
|
|
642
|
+
if currentStream then
|
|
643
|
+
pcall(function() currentStream:Close() end)
|
|
644
|
+
currentStream = nil
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
-- ── Buka SSE stream baru ──────────────────────────────────────────────────────
|
|
649
|
+
function connectSSE()
|
|
650
|
+
if isConnected then return end
|
|
651
|
+
disconnectSSE() -- Bersihkan sisa koneksi lama
|
|
652
|
+
|
|
653
|
+
local url = STREAM_URL .. "?studioId=" .. HttpService:UrlEncode(STUDIO_ID)
|
|
654
|
+
|
|
655
|
+
local ok, streamOrErr = pcall(function()
|
|
656
|
+
return HttpService:GetAsync(url, false) -- Gunakan non-cached request
|
|
657
|
+
end)
|
|
658
|
+
|
|
659
|
+
-- WebStreamClient hanya tersedia di Studio; GetAsync biasa tidak akan
|
|
660
|
+
-- mempertahankan koneksi. Kita butuh WebStreamClient khusus.
|
|
661
|
+
-- Coba via WebStreamClient API:
|
|
662
|
+
local streamOk, stream = pcall(function()
|
|
663
|
+
-- @ts-ignore – WebStreamClient mungkin belum ada di semua versi Studio
|
|
664
|
+
return (HttpService :: any):WebStream(url)
|
|
665
|
+
end)
|
|
666
|
+
|
|
667
|
+
if not streamOk or not stream then
|
|
668
|
+
-- Fallback: Studio tidak mendukung WebStreamClient → gunakan polling biasa
|
|
669
|
+
if ui then
|
|
670
|
+
ui:addLog("[System] WebStreamClient tidak tersedia. Menggunakan polling fallback.", Color3.fromRGB(255, 200, 100))
|
|
633
671
|
end
|
|
634
|
-
|
|
672
|
+
task.spawn(fallbackPolling)
|
|
673
|
+
return
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
currentStream = stream
|
|
677
|
+
isConnected = true
|
|
678
|
+
reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
679
|
+
|
|
680
|
+
if ui then
|
|
681
|
+
ui:setStatus("waiting") -- Menunggu status mcpConnected dari server
|
|
682
|
+
ui:addLog("[System] SSE stream terbuka. Menunggu IDE Agent...", Color3.fromRGB(100, 200, 255))
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
-- Event: pesan masuk dari SSE stream
|
|
686
|
+
local onMessage = stream.OnMessage:Connect(function(message)
|
|
687
|
+
handleSSEMessage(message)
|
|
635
688
|
end)
|
|
689
|
+
table.insert(streamConnections, onMessage)
|
|
690
|
+
|
|
691
|
+
-- Event: stream ditutup (server restart / network drop)
|
|
692
|
+
local onClose = stream.OnClose:Connect(function()
|
|
693
|
+
isConnected = false
|
|
694
|
+
mcpAgentActive = false
|
|
695
|
+
currentStream = nil
|
|
696
|
+
streamConnections = {}
|
|
697
|
+
if ui then
|
|
698
|
+
ui:setStatus("waiting")
|
|
699
|
+
ui:addLog("[System] SSE stream terputus. Reconnect dalam " .. reconnectDelay .. "s...", Color3.fromRGB(255, 200, 100))
|
|
700
|
+
end
|
|
701
|
+
if not isManualDisconnect then
|
|
702
|
+
task.delay(reconnectDelay, connectSSE)
|
|
703
|
+
reconnectDelay = math.min(reconnectDelay * 2, RECONNECT_DELAY_MAX)
|
|
704
|
+
end
|
|
705
|
+
end)
|
|
706
|
+
table.insert(streamConnections, onClose)
|
|
636
707
|
end
|
|
637
708
|
|
|
638
|
-
--
|
|
639
|
-
--
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
709
|
+
-- ── Fallback: HTTP Polling (jika WebStreamClient tidak tersedia) ──────────────
|
|
710
|
+
-- Digunakan pada versi Studio lama yang belum mendukung WebStreamClient.
|
|
711
|
+
local isPollingActive = false
|
|
712
|
+
function fallbackPolling()
|
|
713
|
+
if isPollingActive then return end
|
|
714
|
+
isPollingActive = true
|
|
715
|
+
isConnected = true
|
|
716
|
+
|
|
647
717
|
if ui then
|
|
648
|
-
ui:
|
|
718
|
+
ui:setStatus("waiting")
|
|
719
|
+
ui:addLog("[System] Mode Polling aktif (WebStreamClient tidak didukung).", Color3.fromRGB(200, 200, 100))
|
|
649
720
|
end
|
|
650
|
-
|
|
651
|
-
while
|
|
652
|
-
--
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
721
|
+
|
|
722
|
+
while isConnected and not isManualDisconnect do
|
|
723
|
+
-- Ping
|
|
724
|
+
local pingOk, _ = pcall(function()
|
|
725
|
+
return HttpService:GetAsync(PING_URL .. "?studioId=" .. STUDIO_ID .. "&t=" .. tostring(os.clock()))
|
|
726
|
+
end)
|
|
727
|
+
|
|
728
|
+
if pingOk then
|
|
729
|
+
if ui then ui:setStatus("connected") end
|
|
730
|
+
|
|
731
|
+
-- Long-poll tasks
|
|
732
|
+
local taskOk, taskResponse = pcall(function()
|
|
733
|
+
return HttpService:GetAsync(BASE_URL .. "/api/tasks?t=" .. tostring(os.clock()))
|
|
657
734
|
end)
|
|
658
|
-
|
|
659
|
-
if
|
|
660
|
-
|
|
661
|
-
if
|
|
662
|
-
|
|
663
|
-
if
|
|
664
|
-
|
|
665
|
-
ui:addLog("[System] Server MCP terdeteksi! Auto-connected.", Color3.fromRGB(100, 255, 100))
|
|
666
|
-
end
|
|
667
|
-
startPolling()
|
|
668
|
-
end
|
|
669
|
-
else
|
|
670
|
-
-- Server tidak aktif
|
|
671
|
-
if isConnected then
|
|
672
|
-
-- Server baru saja mati (sebelumnya connected) — polling loop juga akan exit
|
|
673
|
-
isConnected = false
|
|
674
|
-
if ui then
|
|
675
|
-
ui:setStatus("waiting")
|
|
676
|
-
ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
|
|
677
|
-
end
|
|
678
|
-
elseif not isManualDisconnect then
|
|
679
|
-
if ui then
|
|
680
|
-
ui:setStatus("waiting")
|
|
681
|
-
-- HANYA print error jika belum terkoneksi, untuk debugging
|
|
682
|
-
ui:addLog("[Debug] Deteksi gagal: " .. tostring(errorMsg), Color3.fromRGB(150, 150, 150))
|
|
735
|
+
|
|
736
|
+
if taskOk and taskResponse then
|
|
737
|
+
local decodeOk, taskData = pcall(function() return HttpService:JSONDecode(taskResponse) end)
|
|
738
|
+
if decodeOk and taskData and taskData.id then
|
|
739
|
+
local resultData = processTask(taskData)
|
|
740
|
+
if resultData then
|
|
741
|
+
sendResponse(taskData.id, resultData)
|
|
683
742
|
end
|
|
684
743
|
end
|
|
685
744
|
end
|
|
745
|
+
task.wait(0.1)
|
|
746
|
+
else
|
|
747
|
+
if ui then ui:setStatus("waiting") end
|
|
748
|
+
task.wait(1.5)
|
|
686
749
|
end
|
|
687
|
-
|
|
688
|
-
task.wait(2)
|
|
689
750
|
end
|
|
751
|
+
|
|
752
|
+
isPollingActive = false
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
-- ── Plugin Unloading ──────────────────────────────────────────────────────────
|
|
756
|
+
plugin.Unloading:Connect(function()
|
|
757
|
+
isManualDisconnect = true
|
|
758
|
+
disconnectSSE()
|
|
759
|
+
end)
|
|
760
|
+
|
|
761
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
762
|
+
-- STARTUP: Langsung buka SSE stream
|
|
763
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
764
|
+
task.spawn(function()
|
|
765
|
+
task.wait(0.5) -- Tunggu UI siap
|
|
766
|
+
|
|
767
|
+
if ui then
|
|
768
|
+
ui:addLog("[System] NVStudio MCP v2.1.0 termuat. Membuka SSE stream...", Color3.fromRGB(200, 200, 200))
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
connectSSE()
|
|
690
772
|
end)
|
|
691
773
|
]]></ProtectedString>
|
|
692
774
|
</Properties>
|