From 8e5d33517013be47d2865b032dc5a006a954d391 Mon Sep 17 00:00:00 2001 From: daixian Date: Mon, 13 Jul 2026 15:08:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(client):=20=E5=A4=9A=E5=BC=80=20Unity=20?= =?UTF-8?q?=E6=97=B6=E4=BC=98=E5=85=88=E8=BF=9E=E6=8E=A5=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unity-skills~/scripts/unity_skills.py | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/SkillsForUnity/unity-skills~/scripts/unity_skills.py b/SkillsForUnity/unity-skills~/scripts/unity_skills.py index d5e1a4f2..7e7ab39e 100644 --- a/SkillsForUnity/unity-skills~/scripts/unity_skills.py +++ b/SkillsForUnity/unity-skills~/scripts/unity_skills.py @@ -40,6 +40,41 @@ # heartbeat interval is ~30s). REGISTRY_STALE_SECONDS = 120 + +def _normalize_project_path(path: str) -> Optional[str]: + """Return a canonical path suitable for comparing project directories.""" + if not path: + return None + + try: + return os.path.normcase( + os.path.realpath(os.path.abspath(os.path.expanduser(os.fspath(path)))) + ) + except (OSError, TypeError, ValueError): + return None + + +def _project_path_match_score(current_directory: str, project_path: str) -> int: + """Return a positive score when the current directory belongs to a project. + + A project root also matches its subdirectories. The normalized path length is + used as the score so the most specific project wins when projects are nested. + """ + normalized_current = _normalize_project_path(current_directory) + normalized_project = _normalize_project_path(project_path) + if not normalized_current or not normalized_project: + return 0 + + try: + if os.path.commonpath([normalized_current, normalized_project]) != normalized_project: + return 0 + except ValueError: + # commonpath raises for paths on different drives on Windows. + return 0 + + return len(normalized_project) + + def get_registry_path(): return os.path.join(os.path.expanduser("~"), ".unity_skills", "registry.json") @@ -134,7 +169,7 @@ def __init__(self, port: int = None, target: str = None, url: str = None, versio version: Connect to instance by Unity version (e.g. "6", "2022", "2022.3") - auto-discovers port. agent_id: Custom agent identifier (e.g. "MyScript", "ClaudeCode") timeout: Request timeout in seconds (default: 900) - Priority: url > port > target > version > default port 8090 + Priority: url > port > target > version > auto-discovery """ self.url = url self.agent_id = agent_id or _get_agent_id() @@ -166,7 +201,8 @@ def __init__(self, port: int = None, target: str = None, url: str = None, versio else: raise ValueError(f"Could not find Unity instance matching version '{version}'.") else: - # Auto-discover: registry ports first, then scan 8090-8100 + # Auto-discover: prefer the current project's registry entry, then other + # registry ports, and finally scan 8090-8100. found_port = self._find_first_available() self.url = f"http://localhost:{found_port}" @@ -205,15 +241,18 @@ def _sync_timeout_from_server(self): self.timeout = int(minutes) * 60 def _registry_candidate_ports(self) -> List[int]: - """Ports from registry.json worth probing first, freshest heartbeat first. + """Return live registry ports in their preferred probing order. Entries whose last_active heartbeat (unix seconds, refreshed every ~30s by the server) is older than REGISTRY_STALE_SECONDS are skipped, as are - malformed entries — the caller's port scan remains the safety net. + malformed entries. Entries matching the current working directory are + preferred; within each group, the freshest heartbeat wins. The caller's + port scan remains the safety net. """ candidates = [] now = time.time() - for info in _load_registry().values(): + current_directory = os.getcwd() + for registry_path, info in _load_registry().items(): if not isinstance(info, dict): continue port = info.get('port') @@ -222,17 +261,22 @@ def _registry_candidate_ports(self) -> List[int]: continue if not isinstance(last_active, (int, float)) or (now - last_active) > REGISTRY_STALE_SECONDS: continue - candidates.append((last_active, port)) + + project_path = info.get('path') or registry_path + path_match_score = _project_path_match_score(current_directory, project_path) + candidates.append((path_match_score, last_active, port)) + candidates.sort(reverse=True) - return list(dict.fromkeys(port for _, port in candidates)) + return list(dict.fromkeys(port for _, _, port in candidates)) def _find_first_available(self) -> int: """Find the first responsive Unity instance. - Tries ports recorded in registry.json first (no blind scan when instances - are registered), falling back to scanning ports 8090-8100 when the registry - is missing, stale, or its entries stop responding. The winning /health - payload is kept on self._health_info so it is not fetched a second time. + Tries the current project's live registry entry first, followed by other + live entries ordered by heartbeat. Falls back to scanning ports 8090-8100 + when the registry is missing, stale, or its entries stop responding. The + winning /health payload is kept on self._health_info so it is not fetched + a second time. """ tried = set() for port in self._registry_candidate_ports():