From 16f50172caaec506bf82b00786abbe7be0075f8f Mon Sep 17 00:00:00 2001 From: yunyaozhou Date: Tue, 9 Jun 2026 15:59:44 +0800 Subject: [PATCH] feat: bind monitors by base url hash --- README.md | 4 +++- pyproject.toml | 2 +- sub2api_quota_tui.py | 6 ++++++ tests/test_payload.py | 25 +++++++++++++++++++++++++ uv.lock | 2 +- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c889b9a..b6e4a79 100644 --- a/README.md +++ b/README.md @@ -82,4 +82,6 @@ aliases exist: `id < slow < fast < sfast`. The table shows `sfast` first, then When `--status-url` is configured, the status line shows channel monitor health, and the selected account detail shows the matching monitor status when -one exists. +one exists. Monitor binding first uses the shared `base_url_hash` emitted by +the account API and `sub2api-status`; name-token matching remains only as a +fallback for older status payloads. diff --git a/pyproject.toml b/pyproject.toml index b2dc4ed..70e0aca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "shusub2" -version = "0.1.2" +version = "0.1.3" description = "Terminal UI for Sub2API account quota and daily usage" readme = "README.md" requires-python = ">=3.11" diff --git a/sub2api_quota_tui.py b/sub2api_quota_tui.py index 8234823..8533e94 100644 --- a/sub2api_quota_tui.py +++ b/sub2api_quota_tui.py @@ -226,6 +226,11 @@ def text_tokens(value: Any) -> set[str]: def matching_monitor(row: dict[str, Any], status_payload: dict[str, Any]) -> dict[str, Any] | None: + base_url_hash = str(row.get("base_url_hash") or "").strip() + if base_url_hash: + for item in monitor_items(status_payload): + if str(item.get("base_url_hash") or "").strip() == base_url_hash: + return item account_tokens = text_tokens(row.get("name")) if not account_tokens: return None @@ -324,6 +329,7 @@ def normalize_account_rows(payload: dict[str, Any], filter_text: str = "") -> li "id": as_int(account.get("id")), "name": str(account.get("name") or ""), "routing_group": routing_group, + "base_url_hash": str(account.get("base_url_hash") or ""), "provider": provider_label(account), "kind": str(account.get("kind") or "unknown"), "kind_label": kind_label(account.get("kind")), diff --git a/tests/test_payload.py b/tests/test_payload.py index 708a718..dfa9ba9 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -174,6 +174,31 @@ class Sub2APIQuotaTUITests(unittest.TestCase): self.assertIn("monitors 5 ok / 0 failed / 1 unknown", mod.monitor_summary(status_payload)) self.assertIn("monitor operational 2606ms", mod.monitor_detail(row, status_payload)) + def test_monitor_match_prefers_base_url_hash_over_name_tokens(self) -> None: + mod = load_module() + status_payload = { + "channel_monitors": { + "items": [ + { + "name": "input responses", + "base_url_hash": "wrong-hash", + "latest_status": "operational", + "latency_ms": 111, + }, + { + "name": "unrelated responses", + "base_url_hash": "right-hash", + "latest_status": "degraded", + "latency_ms": 222, + }, + ], + } + } + row = {"name": "input 300", "base_url_hash": "right-hash"} + + self.assertEqual(mod.matching_monitor(row, status_payload)["name"], "unrelated responses") + self.assertIn("monitor degraded 222ms", mod.monitor_detail(row, status_payload)) + def test_monitor_error_is_non_blocking_in_once_output(self) -> None: mod = load_module() payload = { diff --git a/uv.lock b/uv.lock index 422c00c..8ce2daf 100644 --- a/uv.lock +++ b/uv.lock @@ -85,7 +85,7 @@ wheels = [ [[package]] name = "shusub2" -version = "0.1.2" +version = "0.1.3" source = { editable = "." } dependencies = [ { name = "textual" },