refactor: 拆分 claude-dev-stack 为 windows-dev-stack 和 wsl-dev-stack
将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
@tool
|
||||
extends Node
|
||||
|
||||
## Multi-connection WebSocket client.
|
||||
## Connects to multiple Node.js MCP server instances on ports 6505-6514.
|
||||
## Each Claude Code session gets its own port; Godot talks to all of them.
|
||||
## Ports 6505-6509: MCP servers (stdio), 6510-6514: CLI tool connections.
|
||||
|
||||
signal client_connected()
|
||||
signal client_disconnected()
|
||||
signal message_received(text: String)
|
||||
signal command_executed(method: String, success: bool)
|
||||
signal command_completed(method: String, success: bool, response: String, source_port: int)
|
||||
|
||||
var command_router: Node
|
||||
|
||||
const BASE_PORT := 6505
|
||||
const MAX_PORT := 6514
|
||||
const RECONNECT_INTERVAL := 3.0
|
||||
const BUFFER_SIZE := 16 * 1024 * 1024 # 16MB
|
||||
const PING_INTERVAL := 5.0 # send ping every N seconds while connected
|
||||
const INACTIVITY_TIMEOUT := 30.0 # force-close if no message received for N seconds
|
||||
|
||||
# Per-port connection state
|
||||
var _peers: Dictionary = {} # port -> WebSocketPeer
|
||||
var _connected: Dictionary = {} # port -> bool
|
||||
var _timers: Dictionary = {} # port -> float (reconnect countdown)
|
||||
var _connect_times: Dictionary = {} # port -> float (elapsed seconds since connect)
|
||||
var _last_activity: Dictionary = {} # port -> float (seconds since last received message)
|
||||
var _ping_timers: Dictionary = {} # port -> float (seconds since last sent ping)
|
||||
var _stale_ports: Dictionary = {} # port -> bool (heartbeat timeout flag, exposed to UI)
|
||||
var _running: bool = false
|
||||
|
||||
|
||||
func start_server() -> void:
|
||||
_running = true
|
||||
for p in range(BASE_PORT, MAX_PORT + 1):
|
||||
_connected[p] = false
|
||||
_timers[p] = 0.0
|
||||
_try_connect(p)
|
||||
print("[MCP] Connecting to ports %d-%d" % [BASE_PORT, MAX_PORT])
|
||||
|
||||
|
||||
func stop_server() -> void:
|
||||
_running = false
|
||||
for p in _peers:
|
||||
var ws: WebSocketPeer = _peers[p]
|
||||
if ws:
|
||||
ws.close(1000, "Plugin shutting down")
|
||||
_peers.clear()
|
||||
_connected.clear()
|
||||
_timers.clear()
|
||||
_last_activity.clear()
|
||||
_ping_timers.clear()
|
||||
_stale_ports.clear()
|
||||
print("[MCP] WebSocket client stopped")
|
||||
|
||||
|
||||
func get_client_count() -> int:
|
||||
var count: int = 0
|
||||
for p in _connected:
|
||||
if _connected[p]:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func get_connected_ports() -> Array[int]:
|
||||
var ports: Array[int] = []
|
||||
for p: int in _connected:
|
||||
if _connected[p]:
|
||||
ports.append(p)
|
||||
return ports
|
||||
|
||||
|
||||
func get_port_connect_time(port: int) -> float:
|
||||
return _connect_times.get(port, -1.0)
|
||||
|
||||
|
||||
func get_port_idle_time(port: int) -> float:
|
||||
return _last_activity.get(port, -1.0)
|
||||
|
||||
|
||||
func is_port_stale(port: int) -> bool:
|
||||
return _stale_ports.get(port, false)
|
||||
|
||||
|
||||
func _try_connect(p: int) -> void:
|
||||
var ws := WebSocketPeer.new()
|
||||
ws.outbound_buffer_size = BUFFER_SIZE
|
||||
ws.inbound_buffer_size = BUFFER_SIZE
|
||||
var err := ws.connect_to_url("ws://127.0.0.1:%d" % p)
|
||||
if err == OK:
|
||||
_peers[p] = ws
|
||||
else:
|
||||
_peers[p] = null
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _running:
|
||||
return
|
||||
|
||||
for p in range(BASE_PORT, MAX_PORT + 1):
|
||||
var ws: WebSocketPeer = _peers.get(p)
|
||||
|
||||
# No peer - try reconnect on timer
|
||||
if ws == null:
|
||||
_timers[p] = _timers.get(p, 0.0) + delta
|
||||
if _timers[p] >= RECONNECT_INTERVAL:
|
||||
_timers[p] = 0.0
|
||||
_try_connect(p)
|
||||
continue
|
||||
|
||||
ws.poll()
|
||||
var state := ws.get_ready_state()
|
||||
|
||||
match state:
|
||||
WebSocketPeer.STATE_OPEN:
|
||||
if not _connected.get(p, false):
|
||||
_connected[p] = true
|
||||
_connect_times[p] = 0.0
|
||||
_last_activity[p] = 0.0
|
||||
_ping_timers[p] = 0.0
|
||||
_stale_ports[p] = false
|
||||
_timers[p] = 0.0
|
||||
print_verbose("[MCP] Connected on port %d" % p)
|
||||
client_connected.emit()
|
||||
else:
|
||||
_connect_times[p] = _connect_times.get(p, 0.0) + delta
|
||||
_last_activity[p] = _last_activity.get(p, 0.0) + delta
|
||||
_ping_timers[p] = _ping_timers.get(p, 0.0) + delta
|
||||
|
||||
var received_any := false
|
||||
while ws.get_available_packet_count() > 0:
|
||||
var packet := ws.get_packet()
|
||||
var text := packet.get_string_from_utf8()
|
||||
received_any = true
|
||||
_dispatch_message(text, p)
|
||||
|
||||
if received_any:
|
||||
_last_activity[p] = 0.0
|
||||
if _stale_ports.get(p, false):
|
||||
_stale_ports[p] = false
|
||||
print("[MCP] Port %d recovered from stale state" % p)
|
||||
|
||||
# Force-close if no message received for INACTIVITY_TIMEOUT.
|
||||
# The MCP server pings every 10s, so 30s of silence means the
|
||||
# connection is half-open and reconnect is the only way out.
|
||||
if _last_activity.get(p, 0.0) > INACTIVITY_TIMEOUT:
|
||||
push_warning("[MCP] Port %d silent for %.1fs — forcing reconnect" % [p, _last_activity[p]])
|
||||
_stale_ports[p] = true
|
||||
ws.close(4000, "Heartbeat timeout")
|
||||
_connected[p] = false
|
||||
_peers[p] = null
|
||||
_timers[p] = 0.0
|
||||
client_disconnected.emit()
|
||||
continue
|
||||
|
||||
# Send periodic ping so the server can detect our death too,
|
||||
# and so any reply resets our own inactivity timer.
|
||||
if _ping_timers.get(p, 0.0) >= PING_INTERVAL:
|
||||
_ping_timers[p] = 0.0
|
||||
ws.send_text(JSON.stringify({"jsonrpc": "2.0", "method": "ping", "params": {}}))
|
||||
|
||||
WebSocketPeer.STATE_CLOSING:
|
||||
pass
|
||||
|
||||
WebSocketPeer.STATE_CLOSED:
|
||||
if _connected.get(p, false):
|
||||
_connected[p] = false
|
||||
print_verbose("[MCP] Disconnected from port %d" % p)
|
||||
client_disconnected.emit()
|
||||
_peers[p] = null
|
||||
_timers[p] = 0.0
|
||||
_last_activity[p] = 0.0
|
||||
_ping_timers[p] = 0.0
|
||||
|
||||
WebSocketPeer.STATE_CONNECTING:
|
||||
pass
|
||||
|
||||
|
||||
func _send_to_port(p: int, text: String) -> void:
|
||||
var ws: WebSocketPeer = _peers.get(p)
|
||||
if ws and _connected.get(p, false):
|
||||
ws.send_text(text)
|
||||
|
||||
|
||||
func send_message(text: String) -> void:
|
||||
# Broadcast to all connected peers
|
||||
for p in _peers:
|
||||
_send_to_port(p, text)
|
||||
|
||||
|
||||
## Synchronous dispatch - parse JSON, handle ping/pong, queue command execution
|
||||
func _dispatch_message(text: String, source_port: int) -> void:
|
||||
message_received.emit(text)
|
||||
|
||||
var json := JSON.new()
|
||||
var err := json.parse(text)
|
||||
if err != OK:
|
||||
_send_response(source_port, null, null, {"code": -32700, "message": "Parse error"})
|
||||
return
|
||||
|
||||
var msg: Variant = json.data
|
||||
if not msg is Dictionary:
|
||||
_send_response(source_port, null, null, {"code": -32600, "message": "Invalid request"})
|
||||
return
|
||||
|
||||
var msg_dict: Dictionary = msg
|
||||
|
||||
if msg_dict.get("method") == "ping":
|
||||
_send_to_port(source_port, JSON.stringify({"jsonrpc": "2.0", "method": "pong", "params": {}}))
|
||||
return
|
||||
|
||||
if msg_dict.get("method") == "pong":
|
||||
return
|
||||
|
||||
var id: Variant = msg_dict.get("id")
|
||||
var method: String = msg_dict.get("method", "")
|
||||
var params: Dictionary = msg_dict.get("params", {})
|
||||
|
||||
if method.is_empty():
|
||||
_send_response(source_port, id, null, {"code": -32600, "message": "Missing method"})
|
||||
return
|
||||
|
||||
if not command_router:
|
||||
_send_response(source_port, id, null, {"code": -32603, "message": "No command router"})
|
||||
return
|
||||
|
||||
_execute_command.call_deferred(source_port, id, method, params)
|
||||
|
||||
|
||||
func _execute_command(source_port: int, id: Variant, method: String, params: Dictionary) -> void:
|
||||
var cmd_result: Dictionary = await command_router.execute(method, params)
|
||||
if cmd_result.has("error"):
|
||||
var err_data: Variant = cmd_result["error"]
|
||||
_send_response(source_port, id, null, err_data)
|
||||
var response_text := JSON.stringify(err_data)
|
||||
command_executed.emit(method, false)
|
||||
command_completed.emit(method, false, response_text, source_port)
|
||||
else:
|
||||
var result_data: Variant = cmd_result.get("result", {})
|
||||
_send_response(source_port, id, result_data, null)
|
||||
var response_text := JSON.stringify(result_data)
|
||||
command_executed.emit(method, true)
|
||||
command_completed.emit(method, true, response_text, source_port)
|
||||
|
||||
|
||||
func _send_response(source_port: int, id: Variant, result: Variant, err: Variant) -> void:
|
||||
var response: Dictionary = {"jsonrpc": "2.0", "id": id}
|
||||
if err != null:
|
||||
response["error"] = err
|
||||
else:
|
||||
response["result"] = result if result != null else {}
|
||||
_send_to_port(source_port, JSON.stringify(response))
|
||||
Reference in New Issue
Block a user