@adiholick/nvstudio-mcp 2.0.4 → 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 +239 -81
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +290 -231
- package/studio-plugin/nvstudio_mcp.rbxmx +290 -231
|
@@ -378,179 +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
|
|
386
|
-
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"
|
|
387
396
|
|
|
388
|
-
--
|
|
389
|
-
local STUDIO_ID
|
|
397
|
+
-- ID unik per sesi Studio (jika dua Studio terbuka, keduanya didaftarkan terpisah)
|
|
398
|
+
local STUDIO_ID = HttpService:GenerateGUID(false)
|
|
390
399
|
|
|
391
|
-
--
|
|
400
|
+
-- ── UI Module ──────────────────────────────────────────────────────────────────
|
|
392
401
|
local uiModule = script.Parent:FindFirstChild("ui_manager")
|
|
393
402
|
local ui = nil
|
|
394
403
|
|
|
395
|
-
--
|
|
396
|
-
local toolbar
|
|
404
|
+
-- ── Toolbar ───────────────────────────────────────────────────────────────────
|
|
405
|
+
local toolbar = plugin:CreateToolbar("NVStudio MCP")
|
|
397
406
|
local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722", "NVStudio")
|
|
398
407
|
|
|
399
|
-
--
|
|
408
|
+
-- ── State ─────────────────────────────────────────────────────────────────────
|
|
400
409
|
local ctx = {
|
|
401
410
|
scriptHistory = {},
|
|
402
|
-
recentLogs
|
|
403
|
-
HttpService
|
|
404
|
-
studioId
|
|
411
|
+
recentLogs = {},
|
|
412
|
+
HttpService = HttpService,
|
|
413
|
+
studioId = STUDIO_ID,
|
|
405
414
|
}
|
|
406
|
-
local MAX_LOGS
|
|
407
|
-
local isConnected
|
|
408
|
-
local
|
|
409
|
-
local
|
|
410
|
-
local
|
|
411
|
-
local
|
|
412
|
-
local
|
|
413
|
-
|
|
414
|
-
--
|
|
415
|
-
local
|
|
416
|
-
local
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
440
|
+
|
|
441
|
+
-- ── Inisialisasi UI ──────────────────────────────────────────────────────────
|
|
419
442
|
if uiModule then
|
|
420
443
|
local UIManager = require(uiModule)
|
|
421
444
|
ui = UIManager.init(plugin, STUDIO_ID)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
ui:
|
|
425
|
-
|
|
426
|
-
ui:setStatus("waiting")
|
|
427
|
-
else
|
|
428
|
-
ui:setStatus("disconnected")
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
-- Sync state tombol toolbar dengan status widget
|
|
445
|
+
|
|
446
|
+
ui:setAutoConnectState(true)
|
|
447
|
+
ui:setStatus("waiting")
|
|
448
|
+
|
|
432
449
|
if ui.widget then
|
|
433
450
|
toggleBtn:SetActive(ui.widget.Enabled)
|
|
434
451
|
ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
|
|
435
452
|
toggleBtn:SetActive(ui.widget.Enabled)
|
|
436
453
|
end)
|
|
437
454
|
end
|
|
438
|
-
|
|
439
|
-
-- Event Toggle GUI Panel
|
|
455
|
+
|
|
440
456
|
toggleBtn.Click:Connect(function()
|
|
441
457
|
ui:toggleVisibility()
|
|
442
458
|
end)
|
|
443
|
-
|
|
444
|
-
--
|
|
459
|
+
|
|
460
|
+
-- Tombol Connect / Disconnect
|
|
445
461
|
ui.connectBtn.MouseButton1Click:Connect(function()
|
|
446
462
|
if isClickDebounce then return end
|
|
447
463
|
isClickDebounce = true
|
|
448
|
-
|
|
464
|
+
|
|
449
465
|
if not isConnected then
|
|
450
|
-
-- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
|
|
451
466
|
isManualDisconnect = false
|
|
467
|
+
reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
452
468
|
ui:setStatus("waiting")
|
|
453
|
-
ui:addLog("[System] Menghubungkan ke Server MCP...", Color3.fromRGB(255, 220, 100))
|
|
454
|
-
|
|
455
|
-
task.spawn(function()
|
|
456
|
-
local cacheBuster = "?t=" .. tostring(os.clock())
|
|
457
|
-
local success, _ = pcall(function()
|
|
458
|
-
return HttpService:GetAsync(PING_URL .. cacheBuster)
|
|
459
|
-
end)
|
|
460
|
-
|
|
461
|
-
isConnected = true
|
|
462
|
-
if success then
|
|
463
|
-
if ui then
|
|
464
|
-
ui:setStatus("connected")
|
|
465
|
-
ui:addLog("[System] Berhasil terhubung ke Server MCP!", Color3.fromRGB(100, 255, 100))
|
|
466
|
-
end
|
|
467
|
-
else
|
|
468
|
-
if ui then
|
|
469
|
-
ui:setStatus("waiting")
|
|
470
|
-
ui:addLog("[System] Server MCP belum aktif di port 3055. Menunggu koneksi...", Color3.fromRGB(255, 200, 100))
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
startPolling()
|
|
474
|
-
end)
|
|
469
|
+
ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
|
|
470
|
+
task.spawn(connectSSE)
|
|
475
471
|
else
|
|
476
|
-
|
|
477
|
-
|
|
472
|
+
isManualDisconnect = true
|
|
473
|
+
disconnectSSE()
|
|
478
474
|
ui:setStatus("disconnected")
|
|
479
475
|
ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
|
|
480
476
|
end
|
|
481
|
-
|
|
482
|
-
task.delay(0.3, function()
|
|
483
|
-
isClickDebounce = false
|
|
484
|
-
end)
|
|
485
|
-
end)
|
|
486
|
-
|
|
487
|
-
-- Event Custom Checkbox (Auto-Connect)
|
|
488
|
-
ui.autoConnectBtn.MouseButton1Click:Connect(function()
|
|
489
|
-
autoConnect = not autoConnect
|
|
490
|
-
plugin:SetSetting("AutoConnect", autoConnect)
|
|
491
|
-
ui:setAutoConnectState(autoConnect)
|
|
492
|
-
ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
|
|
477
|
+
|
|
478
|
+
task.delay(0.3, function() isClickDebounce = false end)
|
|
493
479
|
end)
|
|
494
480
|
else
|
|
495
|
-
-- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
|
|
496
481
|
toggleBtn.Click:Connect(function()
|
|
497
|
-
print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan
|
|
482
|
+
print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan. GUI gagal dimuat!")
|
|
498
483
|
end)
|
|
499
484
|
end
|
|
500
485
|
|
|
501
|
-
--
|
|
486
|
+
-- ── Log Capture ──────────────────────────────────────────────────────────────
|
|
502
487
|
local function addSystemLog(msg, msgType)
|
|
503
|
-
local color
|
|
488
|
+
local color = Color3.fromRGB(200, 200, 200)
|
|
504
489
|
local prefix = "[INFO] "
|
|
505
490
|
if msgType == Enum.MessageType.MessageError then
|
|
506
|
-
prefix = "[ERROR] "
|
|
507
|
-
color = Color3.fromRGB(255, 100, 100)
|
|
491
|
+
prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
|
|
508
492
|
elseif msgType == Enum.MessageType.MessageWarning then
|
|
509
|
-
prefix = "[WARN] "
|
|
510
|
-
color = Color3.fromRGB(255, 200, 100)
|
|
493
|
+
prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
|
|
511
494
|
end
|
|
512
|
-
|
|
513
495
|
local fullMsg = prefix .. msg
|
|
514
|
-
|
|
515
|
-
-- 1. Simpan di memori untuk disedot AI
|
|
516
496
|
table.insert(ctx.recentLogs, fullMsg)
|
|
517
|
-
if #ctx.recentLogs > MAX_LOGS then
|
|
518
|
-
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
-- 2. Cetak ke Terminal GUI Plugin
|
|
522
|
-
if ui then
|
|
523
|
-
ui:addLog(fullMsg, color)
|
|
524
|
-
end
|
|
497
|
+
if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
|
|
498
|
+
if ui then ui:addLog(fullMsg, color) end
|
|
525
499
|
end
|
|
526
|
-
|
|
527
|
-
-- Ikat listener output
|
|
528
500
|
LogService.MessageOut:Connect(addSystemLog)
|
|
529
501
|
|
|
530
|
-
--
|
|
531
|
-
local skills = {}
|
|
532
|
-
for _, module in ipairs(script.Parent:GetChildren()) do
|
|
533
|
-
if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
|
|
534
|
-
skills[module.Name] = require(module)
|
|
535
|
-
end
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
-- Fungsi Helper Pathing
|
|
502
|
+
-- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
|
|
539
503
|
local function getInstanceFromPath(pathString)
|
|
540
|
-
local parts
|
|
504
|
+
local parts = string.split(pathString, ".")
|
|
541
505
|
local current = game
|
|
542
|
-
|
|
543
506
|
if parts[1] == "game" then table.remove(parts, 1) end
|
|
544
|
-
|
|
545
|
-
-- Handle common Services directly to ensure reliability
|
|
546
507
|
pcall(function()
|
|
547
508
|
local svc = game:GetService(parts[1])
|
|
548
|
-
if svc then
|
|
549
|
-
current = svc
|
|
550
|
-
table.remove(parts, 1)
|
|
551
|
-
end
|
|
509
|
+
if svc then current = svc; table.remove(parts, 1) end
|
|
552
510
|
end)
|
|
553
|
-
|
|
554
511
|
for _, part in ipairs(parts) do
|
|
555
512
|
local found = current:FindFirstChild(part)
|
|
556
513
|
if not found then return nil end
|
|
@@ -559,157 +516,259 @@ local function getInstanceFromPath(pathString)
|
|
|
559
516
|
return current
|
|
560
517
|
end
|
|
561
518
|
|
|
562
|
-
--
|
|
519
|
+
-- ── Proses Task dari Server ───────────────────────────────────────────────────
|
|
563
520
|
local function processTask(taskData)
|
|
564
521
|
if not taskData or not taskData.id or not taskData.command then return nil end
|
|
565
|
-
|
|
566
|
-
if ui then
|
|
567
|
-
ui:addLog("[AI Command]
|
|
522
|
+
|
|
523
|
+
if ui then
|
|
524
|
+
ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
|
|
568
525
|
end
|
|
569
|
-
|
|
526
|
+
|
|
570
527
|
local targetInstance = getInstanceFromPath(taskData.target)
|
|
571
528
|
if not targetInstance then
|
|
572
|
-
return {
|
|
573
|
-
id = taskData.id,
|
|
574
|
-
status = "error",
|
|
575
|
-
error = "Target path tidak ditemukan: " .. taskData.target
|
|
576
|
-
}
|
|
529
|
+
return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
|
|
577
530
|
end
|
|
578
531
|
|
|
579
532
|
local skillFunc = skills[taskData.command]
|
|
580
533
|
if not skillFunc then
|
|
581
|
-
return {
|
|
582
|
-
id = taskData.id,
|
|
583
|
-
status = "error",
|
|
584
|
-
error = "Command tidak dikenali oleh Studio: " .. taskData.command
|
|
585
|
-
}
|
|
534
|
+
return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
|
|
586
535
|
end
|
|
587
536
|
|
|
588
|
-
-- Eksekusi skill AI
|
|
589
537
|
local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
|
|
590
|
-
result.id
|
|
591
|
-
|
|
592
|
-
-- Cetak hasil eksekusi ke Terminal GUI
|
|
538
|
+
result.id = taskData.id
|
|
539
|
+
|
|
593
540
|
if ui then
|
|
594
541
|
local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
|
|
595
542
|
ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
|
|
596
543
|
end
|
|
597
|
-
|
|
598
|
-
return result
|
|
599
|
-
end
|
|
600
544
|
|
|
601
|
-
|
|
602
|
-
function stopPolling()
|
|
603
|
-
isConnected = false
|
|
604
|
-
isManualDisconnect = true
|
|
605
|
-
-- isPolling akan menjadi false secara otomatis saat loop keluar
|
|
545
|
+
return result
|
|
606
546
|
end
|
|
607
547
|
|
|
608
|
-
--
|
|
609
|
-
function
|
|
610
|
-
if isPolling then return end
|
|
611
|
-
isPolling = true
|
|
612
|
-
if ui and isConnected then ui:setStatus("connected") end
|
|
613
|
-
|
|
548
|
+
-- ── Kirim Respons ke Server ───────────────────────────────────────────────────
|
|
549
|
+
local function sendResponse(taskId, resultData)
|
|
614
550
|
task.spawn(function()
|
|
615
|
-
|
|
616
|
-
local
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
)
|
|
620
560
|
end)
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if not isConnected then
|
|
624
|
-
break
|
|
561
|
+
if not success then
|
|
562
|
+
HttpService:PostAsync(RESPONSE_URL, json, Enum.HttpContentType.ApplicationJson)
|
|
625
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
|
|
570
|
+
|
|
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
|
|
626
580
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if ui then ui:setStatus("connected") end
|
|
581
|
+
local ok, decoded = pcall(function() return HttpService:JSONDecode(payload) end)
|
|
582
|
+
if not ok or type(decoded) ~= "table" then return end
|
|
630
583
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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))
|
|
643
604
|
end
|
|
644
|
-
|
|
645
|
-
-- Fast-polling (long-polling yield di server Node.js)
|
|
646
|
-
-- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
|
|
647
|
-
task.wait(0.05)
|
|
648
|
-
else
|
|
649
|
-
-- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
|
|
650
|
-
if ui then ui:setStatus("waiting") end
|
|
651
|
-
task.wait(1.5)
|
|
652
605
|
end
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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)
|
|
657
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))
|
|
671
|
+
end
|
|
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)
|
|
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)
|
|
658
704
|
end
|
|
659
|
-
isPolling = false
|
|
660
705
|
end)
|
|
706
|
+
table.insert(streamConnections, onClose)
|
|
661
707
|
end
|
|
662
708
|
|
|
663
|
-
--
|
|
664
|
-
--
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
+
|
|
671
717
|
if ui then
|
|
672
|
-
ui:
|
|
718
|
+
ui:setStatus("waiting")
|
|
719
|
+
ui:addLog("[System] Mode Polling aktif (WebStreamClient tidak didukung).", Color3.fromRGB(200, 200, 100))
|
|
673
720
|
end
|
|
674
|
-
|
|
675
|
-
while
|
|
676
|
-
--
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
end
|
|
696
|
-
else
|
|
697
|
-
-- Server tidak aktif / belum menyala
|
|
698
|
-
if isConnected then
|
|
699
|
-
isConnected = false
|
|
700
|
-
if ui then
|
|
701
|
-
ui:setStatus("waiting")
|
|
702
|
-
ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
|
|
703
|
-
end
|
|
704
|
-
elseif ui and ui.status ~= "waiting" and not isManualDisconnect then
|
|
705
|
-
ui:setStatus("waiting")
|
|
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()))
|
|
734
|
+
end)
|
|
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)
|
|
706
742
|
end
|
|
707
743
|
end
|
|
708
744
|
end
|
|
745
|
+
task.wait(0.1)
|
|
746
|
+
else
|
|
747
|
+
if ui then ui:setStatus("waiting") end
|
|
748
|
+
task.wait(1.5)
|
|
709
749
|
end
|
|
710
|
-
|
|
711
|
-
task.wait(1.5)
|
|
712
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()
|
|
713
772
|
end)
|
|
714
773
|
]]></ProtectedString>
|
|
715
774
|
</Properties>
|