更新godot ai 到最新版本

This commit is contained in:
2026-09-22 16:07:50 +08:00
parent 9dc02dbb31
commit 020ab88606
46 changed files with 5816 additions and 400 deletions
+53 -17
View File
@@ -105,6 +105,11 @@ func _begin_start_episode(existing_id := 0, probe := true) -> void:
_cancel_effect()
_replacement_authorization = null
_transport = null
var config_error := McpAllowHosts.configuration_error(str(_plan.get("allow_hosts", "")))
if not config_error.is_empty():
_block_without_effect("unsupported_remote_access", config_error)
startup_finished.emit("blocked:unsupported_remote_access")
return
if existing_id <= 0:
_next_episode_id += 1
existing_id = _next_episode_id
@@ -313,6 +318,7 @@ func transport_lost(reason := "Authenticated server endpoint was lost.") -> void
_ready_since_msec = 0
var limit := ENDPOINT_RECOVERY_DELAYS_SECONDS.size()
if _endpoint_recovery_attempts >= limit:
_endpoint_recovery_pending_episode = 0
_block(
"endpoint_lost",
"%s Automatic re-probing gave up after %d attempts; click Restart." % [reason, limit],
@@ -320,6 +326,7 @@ func transport_lost(reason := "Authenticated server endpoint was lost.") -> void
return
_endpoint_recovery_attempts += 1
var delay := float(ENDPOINT_RECOVERY_DELAYS_SECONDS[_endpoint_recovery_attempts - 1])
_endpoint_recovery_pending_episode = int(_episode.get("id", 0))
_block(
"endpoint_lost",
"%s Re-probing in %ds (attempt %d of %d)." % [
@@ -330,7 +337,6 @@ func transport_lost(reason := "Authenticated server endpoint was lost.") -> void
"MCP | server endpoint lost (%s); re-probing in %ds (attempt %d of %d)"
% [reason, int(delay), _endpoint_recovery_attempts, limit]
)
_endpoint_recovery_pending_episode = int(_episode.get("id", 0))
if bool(_plan.get("automatic_effects", true)):
_recover_lost_endpoint_after(delay, _endpoint_recovery_pending_episode)
@@ -700,8 +706,9 @@ func _effect_probe(payload: Dictionary) -> Dictionary:
if disposition == "unproven":
return _blocked_probe_result("process_authority_mismatch", port, {}, false,
"managed process identity could not be proven; click Restart to retry")
var listeners := PortResolver.windows_listener_snapshot() if OS.get_name() == "Windows" else {}
var capability := _read_capability(port)
var live := _probe_with_capability(port, capability, int(payload.timeout_ms))
var live := _probe_with_capability(port, capability, int(payload.timeout_ms), listeners)
if _authenticated_status_matches_record(live, capability):
var version := str(live.get("version", ""))
var ws_port := int(live.get("ws_port", 0))
@@ -719,7 +726,15 @@ func _effect_probe(payload: Dictionary) -> Dictionary:
"transport": _transport_from(port, ws_port, live, capability),
}
return _blocked_probe_result("incompatible", port, live, true)
if PortResolver.is_port_in_use(port):
var occupied: bool
if OS.get_name() == "Windows":
var occupancy := PortResolver.windows_port_occupancy(port, listeners)
if occupancy == PortResolver.PortOccupancy.UNKNOWN:
return _blocked_probe_result("port_occupancy_unknown", port, live)
occupied = occupancy == PortResolver.PortOccupancy.OCCUPIED
else:
occupied = PortResolver.is_port_in_use(port)
if occupied:
var detail := _record_probe_failure_detail(capability, live)
var blocked := _blocked_probe_result("occupied", port, live, false, detail)
var pre_v4 := _untrusted_pre_v4_occupant_version(port, int(payload.timeout_ms))
@@ -730,8 +745,13 @@ func _effect_probe(payload: Dictionary) -> Dictionary:
## The server binds both ports before it publishes anything. A held
## WebSocket port (a server moved off the HTTP port, or another editor)
## would kill the launch at preflight; say so now and name the setting.
if expected_ws_port > 0 and PortResolver.is_port_in_use(expected_ws_port):
return ws_port_blocked_result(expected_ws_port)
if expected_ws_port > 0:
var ws_occupied := (
PortResolver.windows_port_occupancy(expected_ws_port, listeners) == PortResolver.PortOccupancy.OCCUPIED
if OS.get_name() == "Windows" else PortResolver.is_port_in_use(expected_ws_port)
)
if ws_occupied:
return ws_port_blocked_result(expected_ws_port)
return {
"outcome": "free",
"owned_disposition": disposition,
@@ -941,7 +961,8 @@ func _effect_prove(payload: Dictionary) -> Dictionary:
or str(capability.get("websocket", "")) != str(payload.get("ws_capability", ""))
):
return {"pending": true, "reason": "capability_pair"}
var live := _probe_with_capability(port, capability, int(payload.timeout_ms))
var listeners := PortResolver.windows_listener_snapshot() if OS.get_name() == "Windows" else {}
var live := _probe_with_capability(port, capability, int(payload.timeout_ms), listeners)
if not _authenticated_status_matches_record(live, capability):
return {"pending": true, "reason": "authenticated_status"}
var version := str(live.get("version", ""))
@@ -953,7 +974,7 @@ func _effect_prove(payload: Dictionary) -> Dictionary:
var pid := PortResolver.read_pid_file(str(payload.get("pid_file", "")))
if pid <= 1:
return {"pending": true, "reason": "pid_file"}
if not PortResolver.find_all_pids_on_port(port).has(pid):
if not PortResolver.find_all_pids_on_port(port, Callable(), listeners).has(pid):
return {"pending": true, "reason": "listener_pid"}
if first_snapshot == null or pid != hinted_pid:
first_snapshot = (
@@ -973,13 +994,14 @@ func _effect_prove(payload: Dictionary) -> Dictionary:
## record must still name the authenticated instance, and the process
## fingerprint must remain unchanged after the final probe.
var final_capability := _read_capability(port)
var final_live := _probe_with_capability(port, final_capability, int(payload.timeout_ms))
var final_live := _probe_with_capability(port, final_capability, int(payload.timeout_ms), listeners)
var final_snapshot: Variant = _capture_process_snapshot(pid)
if PortResolver.capture_failed(final_snapshot):
return {"pending": true, "reason": "identity_unavailable"}
var final_listeners := PortResolver.windows_listener_snapshot() if OS.get_name() == "Windows" else {}
if (
PortResolver.read_pid_file(str(payload.get("pid_file", ""))) != pid
or not PortResolver.find_all_pids_on_port(port).has(pid)
or not PortResolver.find_all_pids_on_port(port, Callable(), final_listeners).has(pid)
or not PortResolver.process_descends_from(pid, launch_pid, final_snapshot)
or not PortResolver.pid_cmdline_is_godot_ai(pid, final_snapshot)
or PortResolver.process_fingerprint(pid, final_snapshot) != fingerprint
@@ -1085,8 +1107,8 @@ func _effect_replace(payload: Dictionary) -> Dictionary:
)
return {"ok": false, "reason": "replacement_failed"}
if launch.is_empty():
PortResolver.wait_for_port_free(port, 5.0)
return {"ok": not PortResolver.is_port_in_use(port), "reason": ""}
var occupancy := PortResolver.wait_for_port_free(port, 5.0)
return {"ok": occupancy == PortResolver.PortOccupancy.FREE, "reason": ""}
return {"ok": true, "reason": "", "launch": launch}
@@ -1165,13 +1187,14 @@ func _effect_stop(payload: Dictionary) -> Dictionary:
if disposition == "owned":
grants.append({"pid": pid, "fingerprint": fingerprint})
PortResolver.kill_exact_processes(grants, true)
var occupancy := PortResolver.PortOccupancy.FREE
if port > 0 and not grants.is_empty():
PortResolver.wait_for_port_free(port, 3.0)
occupancy = PortResolver.wait_for_port_free(port, 3.0)
var targets_gone := true
for exact in grants:
if PortResolver.process_fingerprint(int(exact.pid)) == str(exact.fingerprint):
targets_gone = false
var port_free := port <= 0 or not PortResolver.is_port_in_use(port)
var port_free := port <= 0 or (occupancy != PortResolver.PortOccupancy.UNKNOWN and not PortResolver.is_port_in_use(port))
## Killing something of ours must also free its listener. When there was
## nothing of ours left to kill, whoever holds the port now is not ours to
## prove stopped; the next start's probe deals with that occupant.
@@ -1283,7 +1306,7 @@ static func probe_live_server_status(
return _probe_with_capability(port, capability, timeout_ms)
static func _probe_with_capability(port: int, capability: Dictionary, timeout_ms: int) -> Dictionary:
static func _probe_with_capability(port: int, capability: Dictionary, timeout_ms: int, listeners: Dictionary = {}) -> Dictionary:
var result := {"reachable": false, "name": "", "version": "", "ws_port": 0, "instance_id": "", "status_code": 0, "error": ""}
var http_capability := str(capability.get("http", ""))
if http_capability.is_empty():
@@ -1292,7 +1315,10 @@ static func _probe_with_capability(port: int, capability: Dictionary, timeout_ms
## Windows can otherwise spend the whole connect deadline on an unbound
## loopback port. This is only an unreachable probe; callers still check
## occupancy and prove any later listener before using it.
if OS.get_name() == "Windows" and PortResolver.can_bind_local_port(port):
if (
OS.get_name() == "Windows" and PortResolver.can_bind_local_port(port)
and PortResolver.windows_port_occupancy(port, listeners) == PortResolver.PortOccupancy.FREE
):
result.error = "port_unbound"
return result
var client := HTTPClient.new()
@@ -1412,7 +1438,9 @@ static func _blocked_probe_result(
var message := "Port %d is occupied by another process." % port
if not detail.is_empty():
message = "Port %d is occupied by another process (%s)." % [port, detail]
if replaceable:
if reason == "port_occupancy_unknown":
message = "Windows could not query listening ports; retry when port discovery is available."
elif replaceable:
message = "Port %d is occupied by godot-ai v%s; choose Replace to continue." % [port, version]
return {
"outcome": "blocked",
@@ -1627,6 +1655,13 @@ func get_status_dict() -> Dictionary:
return {
"episode_id": int(_episode.get("id", 0)),
"episode_state": state,
"reason": reason,
"recovery_pending": (
state == BLOCKED and reason == "endpoint_lost"
and _endpoint_recovery_pending_episode > 0
and _endpoint_recovery_pending_episode == int(_episode.get("id", 0))
),
"episode_reason": reason,
"phase": str(_episode.get("phase", "")),
"ready_kind": str(_episode.get("ready_kind", "")),
"state": _dock_state(state, reason),
@@ -1713,9 +1748,10 @@ static func _dock_state(state: String, reason: String) -> int:
return ServerState.STOPPING
BLOCKED:
match reason:
"unsupported_remote_access": return ServerState.UNSUPPORTED_CONFIG
"incompatible": return ServerState.INCOMPATIBLE
"no_command": return ServerState.NO_COMMAND
"port_reserved": return ServerState.PORT_EXCLUDED
"occupied", "replacement_unproven": return ServerState.FOREIGN_PORT
"occupied", "replacement_unproven", "port_occupancy_unknown": return ServerState.FOREIGN_PORT
_: return ServerState.CRASHED
return ServerState.UNINITIALIZED