Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions companion/codex_display/app_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def find_codex_binary() -> str:
return bundled

raise AppServerError(
"找不到 Codex CLI;请设置 CODEX_BIN,或安装包含 codex 的 ChatGPT/Codex App"
"Codex CLI not found; set CODEX_BIN or install the ChatGPT/Codex app "
"that bundles codex"
)


Expand Down Expand Up @@ -84,9 +85,9 @@ async def stop(self) -> None:

async def wait_for_exit(self) -> None:
if self._reader_task is None:
raise AppServerError("Codex app-server 尚未启动")
raise AppServerError("Codex app-server has not started")
await self._reader_task
raise AppServerError("Codex app-server 已退出")
raise AppServerError("Codex app-server exited")

async def request(
self, method: str, params: Optional[Dict[str, Any]] = None
Expand All @@ -112,7 +113,7 @@ async def notify(self, method: str, params: Dict[str, Any]) -> None:

async def _send(self, message: Dict[str, Any]) -> None:
if self._process is None or self._process.stdin is None:
raise AppServerError("Codex app-server 尚未启动")
raise AppServerError("Codex app-server has not started")
payload = json.dumps(message, separators=(",", ":")) + "\n"
self._process.stdin.write(payload.encode("utf-8"))
await self._process.stdin.drain()
Expand All @@ -123,7 +124,7 @@ async def _read_loop(self) -> None:
while True:
line = await self._process.stdout.readline()
if not line:
error = AppServerError("Codex app-server 已退出")
error = AppServerError("Codex app-server exited")
for future in self._pending.values():
if not future.done():
future.set_exception(error)
Expand Down
20 changes: 10 additions & 10 deletions companion/codex_display/ble.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def run_forever(self) -> None:
backoff = 1.0
while True:
try:
logger.info("正在搜索 %s…", self._device_name)
logger.info("Scanning for %s...", self._device_name)
device = await BleakScanner.find_device_by_filter(
lambda candidate, advertisement: (
(candidate.name or "") == self._device_name
Expand All @@ -58,11 +58,11 @@ async def run_forever(self) -> None:
self.connected_event.clear()
if "turned off" in str(error).lower():
logger.error(
"蓝牙不可用或当前启动程序没有权限;请检查“系统设置 → "
"隐私与安全性 → 蓝牙”"
"Bluetooth is unavailable or permission is missing; "
"check System Settings > Privacy & Security > Bluetooth"
)
else:
logger.warning("BLE 扫描失败:%s", error)
logger.warning("BLE scan failed: %s", error)
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 60)
continue
Expand All @@ -83,12 +83,12 @@ def on_disconnect(_: Any) -> None:
try:
await client.connect()
backoff = 1.0
logger.info("已连接 %s", device.name or device.address)
logger.info("Connected to %s", device.name or device.address)
await self._run_connection(client, disconnected)
except asyncio.CancelledError:
raise
except Exception as error:
logger.warning("BLE 连接中断:%s", error)
logger.warning("BLE connection interrupted: %s", error)
finally:
self.connected_event.clear()
await self._disconnect(client)
Expand All @@ -105,9 +105,9 @@ async def _disconnect(self, client: Any) -> None:
timeout=BLE_DISCONNECT_TIMEOUT_SECONDS,
)
except asyncio.TimeoutError:
logger.warning("BLE 断开超时,继续重新扫描")
logger.warning("BLE disconnect timed out; resuming scan")
except Exception as error:
logger.warning("BLE 断开失败:%s", error)
logger.warning("BLE disconnect failed: %s", error)

async def _run_connection(
self, client: Any, disconnected: asyncio.Event
Expand Down Expand Up @@ -176,7 +176,7 @@ async def _write_gatt_char(
timeout=BLE_OPERATION_TIMEOUT_SECONDS,
)
except asyncio.TimeoutError as error:
raise asyncio.TimeoutError("BLE GATT 写入超时") from error
raise asyncio.TimeoutError("BLE GATT write timed out") from error

async def _command_loop(self, client: Any) -> None:
while client.is_connected:
Expand All @@ -196,7 +196,7 @@ async def _command_loop(self, client: Any) -> None:
except ValueError as error:
ok, message = False, str(error)
except Exception as error:
logger.exception("设备动作执行失败:%s", error)
logger.exception("Device action failed: %s", error)
ok, message = False, "ACTION FAILED"
payload = encode_result(request_id, ok, message)
if request_id > 0:
Expand Down
21 changes: 17 additions & 4 deletions companion/codex_display/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ async def refresh(self) -> None:
self._sequence += 1
current = self._current_snapshot()
logging.info(
"数据已更新:剩余 %d%%,today %s%d,7d %d,reset %d,running %d",
"Data updated: remaining %d%%, today %s%d, 7d %d, "
"reset %d, running %d",
current.remaining_percent,
"~" if current.tokens_today_estimated else "",
current.tokens_today,
Expand All @@ -81,7 +82,19 @@ async def refresh(self) -> None:
)

async def activity_changed(self) -> None:
try:
count, paths = await collect_active_thread_state(self._client)
except Exception as error:
logging.warning("Immediate RUN reconciliation failed: %s", error)
count = None
paths = set()
async with self._lock:
if self._snapshot is not None and count is not None:
self._snapshot = replace(
self._snapshot,
active_threads=count,
)
self._active_paths = paths
self._sequence += 1
self.status_changed.set()

Expand Down Expand Up @@ -110,7 +123,7 @@ async def reconcile_active_loop(
try:
await cache.reconcile_active()
except Exception as error:
logging.warning("RUN 定期校准失败:%s", error)
logging.warning("Periodic RUN reconciliation failed: %s", error)


async def run(args: argparse.Namespace) -> None:
Expand Down Expand Up @@ -155,7 +168,7 @@ async def run(args: argparse.Namespace) -> None:
if app_server_task in done:
app_server_task.result()
companion_task.result()
raise RuntimeError("BLE Companion 意外退出")
raise RuntimeError("BLE companion exited unexpectedly")
finally:
for task in tasks:
if not task.done():
Expand Down Expand Up @@ -206,7 +219,7 @@ def main() -> None:
except KeyboardInterrupt:
pass
except Exception:
logging.exception("Companion 异常退出")
logging.exception("Companion exited unexpectedly")
raise SystemExit(1)


Expand Down
20 changes: 18 additions & 2 deletions companion/tests/test_app_server.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import asyncio
import json
import unittest

from companion.codex_display.app_server import AppServerClient, AppServerError
from companion.codex_display.main import reconcile_active_loop
from companion.codex_display.main import SnapshotCache, reconcile_active_loop
from companion.codex_display.metrics import collect_snapshot


class FakeClient:
def __init__(self):
self.threads = [{"status": {"type": "active"}}]

async def request(self, method, params=None):
if method == "account/rateLimits/read":
return {
Expand All @@ -21,7 +25,7 @@ async def request(self, method, params=None):
return {"dailyUsageBuckets": []}
if method == "thread/list":
self.thread_params = params
return {"data": [{"status": {"type": "active"}}]}
return {"data": self.threads}
raise AssertionError(method)


Expand All @@ -36,6 +40,18 @@ def test_collects_all_three_sources(self):


class LifecycleTests(unittest.IsolatedAsyncioTestCase):
async def test_hook_change_reconciles_cached_active_count(self):
client = FakeClient()
cache = SnapshotCache(client, refresh_seconds=60)
await cache.refresh()
self.assertEqual(json.loads(await cache.encoded())["a"], 1)

client.threads = []
await cache.activity_changed()

self.assertEqual(json.loads(await cache.encoded())["a"], 0)
self.assertTrue(cache.status_changed.is_set())

async def test_wait_for_exit_reports_unexpected_app_server_exit(self):
client = AppServerClient("/usr/bin/false")
client._reader_task = asyncio.create_task(asyncio.sleep(0))
Expand Down