Spaces:
Running
Running
deploy(S4): Blender headless pipeline + WebAR client + backend fixes
Browse files- backend/routes/mobile_bridge_routes.py +51 -0
- backend/ws/agent_ws.py +20 -0
- dist/app-release.apk +1 -1
- modules/activation_state.py +6 -2
backend/routes/mobile_bridge_routes.py
CHANGED
|
@@ -162,6 +162,57 @@ async def caps() -> dict[str, Any]:
|
|
| 162 |
return {"ok": True, "capabilities": labels}
|
| 163 |
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
@router.get("/link_info")
|
| 166 |
async def link_info(request: Request) -> dict[str, Any]:
|
| 167 |
base = _base_url(request)
|
|
|
|
| 162 |
return {"ok": True, "capabilities": labels}
|
| 163 |
|
| 164 |
|
| 165 |
+
@router.get("/link_status")
|
| 166 |
+
async def link_status() -> dict[str, Any]:
|
| 167 |
+
"""The truth about the phone -> cloud -> PC path, in one call.
|
| 168 |
+
|
| 169 |
+
Guardian used to build its status cards from two unrelated signals: a /health
|
| 170 |
+
ping (proves the Space is up) and a stored boolean. Neither said anything
|
| 171 |
+
about whether a command could actually REACH the desktop, so the app happily
|
| 172 |
+
rendered "PC CONTROL: CLOUD online" next to "NO ACTIVE LINK" while the phone
|
| 173 |
+
could not send a single command. A status light that does not test the path
|
| 174 |
+
it claims to describe is worse than no light at all.
|
| 175 |
+
|
| 176 |
+
Reaching this endpoint at all proves the cloud leg. `pc_connected` is the
|
| 177 |
+
number of desktops holding a live, registered WebSocket β the exact list
|
| 178 |
+
send_to_pcs() delivers to β so it is the same fact the command path uses,
|
| 179 |
+
not a parallel guess. `pc_unlocked` comes from the PC's own periodic report.
|
| 180 |
+
"""
|
| 181 |
+
pc_count = 0
|
| 182 |
+
status: dict[str, Any] = {}
|
| 183 |
+
age: float | None = None
|
| 184 |
+
queued = 0
|
| 185 |
+
try:
|
| 186 |
+
from backend.ws.agent_ws import ws_manager as _ws
|
| 187 |
+
pc_count = len(_ws.pc_connections)
|
| 188 |
+
status = dict(_ws.last_pc_status or {})
|
| 189 |
+
queued = len(_ws.pending_commands)
|
| 190 |
+
if _ws.last_pc_status_at:
|
| 191 |
+
age = round(time.time() - _ws.last_pc_status_at, 1)
|
| 192 |
+
except Exception as exc:
|
| 193 |
+
log.warning("link_status unavailable: %s", exc)
|
| 194 |
+
|
| 195 |
+
unlocked = bool(status.get("unlocked"))
|
| 196 |
+
if pc_count <= 0:
|
| 197 |
+
state, detail = "offline", "Your PC is not connected."
|
| 198 |
+
elif not unlocked:
|
| 199 |
+
state, detail = "locked", "Connected, but locked. Send: unlock pc <password>"
|
| 200 |
+
else:
|
| 201 |
+
state, detail = "ready", "Connected and ready for commands."
|
| 202 |
+
|
| 203 |
+
return {
|
| 204 |
+
"ok": True,
|
| 205 |
+
"cloud": True, # answering at all proves this leg
|
| 206 |
+
"pc_connected": pc_count,
|
| 207 |
+
"pc_unlocked": unlocked,
|
| 208 |
+
"state": state, # offline | locked | ready
|
| 209 |
+
"detail": detail,
|
| 210 |
+
"queued_commands": queued,
|
| 211 |
+
"pc_status_age_seconds": age,
|
| 212 |
+
"pc_status": status,
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
@router.get("/link_info")
|
| 217 |
async def link_info(request: Request) -> dict[str, Any]:
|
| 218 |
base = _base_url(request)
|
backend/ws/agent_ws.py
CHANGED
|
@@ -69,6 +69,14 @@ class ConnectionManager:
|
|
| 69 |
# simply dropped β the user had to remember it and send it again once the
|
| 70 |
# PC woke up.
|
| 71 |
self.pending_commands: list[dict] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
from backend.agent.react_agent import Tool
|
| 73 |
self.tools = []
|
| 74 |
for name, func in TOOL_REGISTRY.items():
|
|
@@ -280,6 +288,18 @@ class ConnectionManager:
|
|
| 280 |
# by the command_id the cloud minted. This closes the loop that used to
|
| 281 |
# be open-ended: without it the phone was told "Sent to your PC." and
|
| 282 |
# never learned that the desktop had refused the command.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
if data.get("event") == "relay:result":
|
| 284 |
payload = data.get("payload") or {}
|
| 285 |
command_id = str(payload.get("command_id") or "")
|
|
|
|
| 69 |
# simply dropped β the user had to remember it and send it again once the
|
| 70 |
# PC woke up.
|
| 71 |
self.pending_commands: list[dict] = []
|
| 72 |
+
# Last status the PC relay pushed, and when. The relay has always sent
|
| 73 |
+
# `relay:status` every 10 seconds and NOTHING read it β there was no
|
| 74 |
+
# branch for it in handle_client_event, so it fell through and was
|
| 75 |
+
# dropped. That is why the phone had no honest way to know anything about
|
| 76 |
+
# the desktop and fell back to showing cloud health in a card labelled
|
| 77 |
+
# "PC CONTROL".
|
| 78 |
+
self.last_pc_status: dict = {}
|
| 79 |
+
self.last_pc_status_at: float = 0.0
|
| 80 |
from backend.agent.react_agent import Tool
|
| 81 |
self.tools = []
|
| 82 |
for name, func in TOOL_REGISTRY.items():
|
|
|
|
| 288 |
# by the command_id the cloud minted. This closes the loop that used to
|
| 289 |
# be open-ended: without it the phone was told "Sent to your PC." and
|
| 290 |
# never learned that the desktop had refused the command.
|
| 291 |
+
# ββ THE PC'S OWN REPORT βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 292 |
+
# Received every 10s from pc_relay_client.status_pusher and, until now,
|
| 293 |
+
# silently discarded. Storing it is what lets /api/link_status tell the
|
| 294 |
+
# phone something TRUE about the desktop instead of guessing from cloud
|
| 295 |
+
# health.
|
| 296 |
+
if data.get("event") == "relay:status":
|
| 297 |
+
payload = data.get("payload")
|
| 298 |
+
if isinstance(payload, dict):
|
| 299 |
+
self.last_pc_status = payload
|
| 300 |
+
self.last_pc_status_at = time.time()
|
| 301 |
+
return
|
| 302 |
+
|
| 303 |
if data.get("event") == "relay:result":
|
| 304 |
payload = data.get("payload") or {}
|
| 305 |
command_id = str(payload.get("command_id") or "")
|
dist/app-release.apk
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 116538395
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aea6e0ae1cd4ee73eae1ba552343ab583401abf760f44e5966a156af9e38d4ff
|
| 3 |
size 116538395
|
modules/activation_state.py
CHANGED
|
@@ -12,9 +12,13 @@ UNLOCK_JSON = ROOT / "data" / "session_unlocked.json"
|
|
| 12 |
# One wording for the refusal, shared by every caller that has to explain it β
|
| 13 |
# core.commands (voice/GUI) and phone.command_runner (phone and cloud relay) β
|
| 14 |
# so a remote user is never told something different from a local one.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
LOCKED_MESSAGE = (
|
| 16 |
-
"This PC is locked.
|
| 17 |
-
"
|
| 18 |
)
|
| 19 |
|
| 20 |
|
|
|
|
| 12 |
# One wording for the refusal, shared by every caller that has to explain it β
|
| 13 |
# core.commands (voice/GUI) and phone.command_runner (phone and cloud relay) β
|
| 14 |
# so a remote user is never told something different from a local one.
|
| 15 |
+
# Tells the user what to do from WHERE THEY ARE. The old wording sent them to
|
| 16 |
+
# the desktop for an activation key, which is useless advice to someone holding
|
| 17 |
+
# a phone in another country β and that is the only situation in which a relayed
|
| 18 |
+
# command hits this message at all.
|
| 19 |
LOCKED_MESSAGE = (
|
| 20 |
+
"This PC is locked. Send: unlock pc <your password> β then this command "
|
| 21 |
+
"will run."
|
| 22 |
)
|
| 23 |
|
| 24 |
|