@agentunion/kite 1.0.7 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/core/event_hub/entry.py +305 -26
  2. package/core/event_hub/hub.py +8 -0
  3. package/core/event_hub/server.py +80 -17
  4. package/core/kite_log.py +241 -0
  5. package/core/launcher/entry.py +978 -284
  6. package/core/launcher/process_manager.py +456 -46
  7. package/core/registry/entry.py +272 -3
  8. package/core/registry/server.py +339 -289
  9. package/core/registry/store.py +10 -4
  10. package/extensions/agents/__init__.py +1 -0
  11. package/extensions/agents/assistant/__init__.py +1 -0
  12. package/extensions/agents/assistant/entry.py +380 -0
  13. package/extensions/agents/assistant/module.md +22 -0
  14. package/extensions/agents/assistant/server.py +236 -0
  15. package/extensions/channels/__init__.py +1 -0
  16. package/extensions/channels/acp_channel/__init__.py +1 -0
  17. package/extensions/channels/acp_channel/entry.py +380 -0
  18. package/extensions/channels/acp_channel/module.md +22 -0
  19. package/extensions/channels/acp_channel/server.py +236 -0
  20. package/extensions/event_hub_bench/entry.py +664 -379
  21. package/extensions/event_hub_bench/module.md +2 -1
  22. package/extensions/services/backup/__init__.py +1 -0
  23. package/extensions/services/backup/entry.py +380 -0
  24. package/extensions/services/backup/module.md +22 -0
  25. package/extensions/services/backup/server.py +244 -0
  26. package/extensions/services/model_service/__init__.py +1 -0
  27. package/extensions/services/model_service/entry.py +380 -0
  28. package/extensions/services/model_service/module.md +22 -0
  29. package/extensions/services/model_service/server.py +236 -0
  30. package/extensions/services/watchdog/entry.py +460 -147
  31. package/extensions/services/watchdog/module.md +3 -0
  32. package/extensions/services/watchdog/monitor.py +128 -13
  33. package/extensions/services/watchdog/server.py +75 -13
  34. package/extensions/services/web/__init__.py +1 -0
  35. package/extensions/services/web/config.yaml +149 -0
  36. package/extensions/services/web/entry.py +487 -0
  37. package/extensions/services/web/module.md +24 -0
  38. package/extensions/services/web/routes/__init__.py +1 -0
  39. package/extensions/services/web/routes/routes_call.py +189 -0
  40. package/extensions/services/web/routes/routes_config.py +512 -0
  41. package/extensions/services/web/routes/routes_contacts.py +98 -0
  42. package/extensions/services/web/routes/routes_devlog.py +99 -0
  43. package/extensions/services/web/routes/routes_phone.py +81 -0
  44. package/extensions/services/web/routes/routes_sms.py +48 -0
  45. package/extensions/services/web/routes/routes_stats.py +17 -0
  46. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  47. package/extensions/services/web/routes/schemas.py +216 -0
  48. package/extensions/services/web/server.py +332 -0
  49. package/extensions/services/web/static/css/style.css +1064 -0
  50. package/extensions/services/web/static/index.html +1445 -0
  51. package/extensions/services/web/static/js/app.js +4671 -0
  52. package/extensions/services/web/vendor/__init__.py +1 -0
  53. package/extensions/services/web/vendor/bluetooth/__init__.py +0 -0
  54. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  55. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  56. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  57. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  58. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  59. package/extensions/services/web/vendor/config.py +139 -0
  60. package/extensions/services/web/vendor/conversation/__init__.py +0 -0
  61. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  62. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  63. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  64. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  65. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  66. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  67. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  68. package/extensions/services/web/vendor/storage/identity.py +312 -0
  69. package/extensions/services/web/vendor/storage/store.py +507 -0
  70. package/extensions/services/web/vendor/task/__init__.py +0 -0
  71. package/extensions/services/web/vendor/task/manager.py +864 -0
  72. package/extensions/services/web/vendor/task/models.py +45 -0
  73. package/extensions/services/web/vendor/task/webhook.py +263 -0
  74. package/extensions/services/web/vendor/tools/__init__.py +0 -0
  75. package/extensions/services/web/vendor/tools/registry.py +321 -0
  76. package/main.py +230 -90
  77. package/package.json +1 -1
@@ -1,379 +1,664 @@
1
- """
2
- Event Hub Benchmark module entry point.
3
- Discovers hub via Registry, runs benchmarks, saves results, exits.
4
- """
5
-
6
- import asyncio
7
- import json
8
- import os
9
- import platform
10
- import sys
11
- import time
12
- from datetime import datetime, timezone
13
-
14
- import statistics
15
- import uuid
16
-
17
- import httpx
18
- import websockets
19
-
20
- _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
- if _project_root not in sys.path:
22
- sys.path.insert(0, _project_root)
23
-
24
- # Results go to KITE_MODULE_DATA (set by Launcher per module)
25
- # Fallback to source tree for standalone runs
26
- RESULTS_DIR = os.environ.get("KITE_MODULE_DATA") or os.path.join(
27
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
28
- "core", "event_hub", "bench_results",
29
- )
30
- TAG = "[event_hub_bench]"
31
-
32
-
33
- def _discover_hub(registry_url: str, token: str, timeout: float = 30) -> str | None:
34
- """Poll registry until event_hub is found. Returns ws:// URL or None."""
35
- headers = {"Authorization": f"Bearer {token}"}
36
- deadline = time.time() + timeout
37
- while time.time() < deadline:
38
- try:
39
- resp = httpx.get(
40
- f"{registry_url}/get/event_hub",
41
- headers=headers, timeout=5,
42
- )
43
- if resp.status_code == 200:
44
- info = resp.json()
45
- ws_url = (info.get("metadata") or {}).get("ws_endpoint")
46
- if ws_url:
47
- return ws_url
48
- except Exception:
49
- pass
50
- time.sleep(1)
51
- return None
52
-
53
-
54
- def _get_hub_stats(ws_url: str) -> dict:
55
- """Fetch hub /stats via HTTP (derive from ws:// URL)."""
56
- http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
57
- try:
58
- resp = httpx.get(f"{http_url}/stats", timeout=5)
59
- if resp.status_code == 200:
60
- return resp.json()
61
- except Exception:
62
- pass
63
- return {}
64
-
65
-
66
- def _sample_hub_resources(ws_url: str) -> dict:
67
- """Sample hub process CPU/memory via psutil. Returns {} if unavailable."""
68
- try:
69
- import psutil
70
- except ImportError:
71
- return {"error": "psutil not installed"}
72
- http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
73
- port = int(http_url.rsplit(":", 1)[1].split("/")[0])
74
- for conn in psutil.net_connections(kind="tcp"):
75
- if conn.laddr.port == port and conn.status == "LISTEN":
76
- try:
77
- p = psutil.Process(conn.pid)
78
- mem = p.memory_info()
79
- return {
80
- "pid": conn.pid,
81
- "cpu_percent": p.cpu_percent(interval=1),
82
- "rss_mb": round(mem.rss / 1048576, 1),
83
- "vms_mb": round(mem.vms / 1048576, 1),
84
- "threads": p.num_threads(),
85
- }
86
- except Exception:
87
- pass
88
- return {"error": "hub process not found"}
89
-
90
-
91
- # ── WebSocket client ──
92
-
93
- class WsClient:
94
- """Lightweight async WebSocket client for hub benchmarks."""
95
-
96
- def __init__(self, ws_url: str, token: str, client_id: str):
97
- self.url = f"{ws_url}?token={token}&id={client_id}"
98
- self.id = client_id
99
- self.ws = None
100
-
101
- async def connect(self):
102
- self.ws = await websockets.connect(self.url, max_size=None)
103
-
104
- async def subscribe(self, patterns: list[str]):
105
- await self.ws.send(json.dumps({"type": "subscribe", "events": patterns}))
106
-
107
- async def publish(self, event_type: str, data: dict) -> str:
108
- eid = str(uuid.uuid4())
109
- await self.ws.send(json.dumps({
110
- "type": "event", "event_id": eid, "event": event_type,
111
- "source": self.id,
112
- "timestamp": datetime.now(timezone.utc).isoformat(),
113
- "data": data,
114
- }))
115
- return eid
116
-
117
- async def recv(self, timeout=5.0):
118
- try:
119
- return json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
120
- except Exception:
121
- return None
122
-
123
- async def close(self):
124
- if self.ws:
125
- await self.ws.close()
126
-
127
-
128
- # ── Benchmark runner ──
129
-
130
- class BenchRunner:
131
- def __init__(self, ws_url: str, token: str):
132
- self.ws_url = ws_url
133
- self.token = token
134
- self.results = {}
135
-
136
- async def _make_client(self, name: str) -> WsClient:
137
- c = WsClient(self.ws_url, self.token, name)
138
- await c.connect()
139
- return c
140
-
141
- # ── Throughput: burst N events, measure hub processing rate ──
142
-
143
- async def bench_throughput(self, n: int = 10000):
144
- print(f"{TAG} Throughput test: {n} events...")
145
- pub = await self._make_client("bench_tp_pub")
146
- sub = await self._make_client("bench_tp_sub")
147
- await sub.subscribe(["bench.tp.*"])
148
- await asyncio.sleep(0.2)
149
-
150
- recvd = 0
151
- stop = asyncio.Event()
152
-
153
- async def _recv():
154
- nonlocal recvd
155
- while not stop.is_set():
156
- try:
157
- raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
158
- if '"event"' in raw and '"ack"' not in raw:
159
- recvd += 1
160
- except Exception:
161
- pass
162
-
163
- async def _drain_acks():
164
- while not stop.is_set():
165
- try:
166
- await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
167
- except Exception:
168
- pass
169
-
170
- recv_task = asyncio.create_task(_recv())
171
- ack_task = asyncio.create_task(_drain_acks())
172
-
173
- hub_before = (_get_hub_stats(self.ws_url).get("counters") or {})
174
-
175
- t0 = time.time()
176
- for i in range(n):
177
- await pub.publish("bench.tp.test", {"i": i})
178
- send_time = time.time() - t0
179
-
180
- deadline = time.time() + max(15, n / 300)
181
- while recvd < n and time.time() < deadline:
182
- await asyncio.sleep(0.1)
183
- total_time = time.time() - t0
184
-
185
- stop.set()
186
- await recv_task
187
- ack_task.cancel()
188
-
189
- hub_after = (_get_hub_stats(self.ws_url).get("counters") or {})
190
-
191
- await pub.close()
192
- await sub.close()
193
-
194
- self.results["throughput"] = {
195
- "events": n,
196
- "send_rate": round(n / send_time),
197
- "recv_rate": round(recvd / total_time) if total_time > 0 else 0,
198
- "client_recv": recvd,
199
- "hub_queued": hub_after.get("events_queued", 0) - hub_before.get("events_queued", 0),
200
- "hub_routed": hub_after.get("events_routed", 0) - hub_before.get("events_routed", 0),
201
- "send_time_s": round(send_time, 2),
202
- "total_time_s": round(total_time, 2),
203
- }
204
- print(f"{TAG} send_rate={self.results['throughput']['send_rate']} evt/s, "
205
- f"recv={recvd}/{n}, time={round(total_time, 1)}s")
206
-
207
- # ── Latency: sequential pub→recv round-trip ──
208
-
209
- async def bench_latency(self, n: int = 200):
210
- print(f"{TAG} Latency test: {n} samples...")
211
- pub = await self._make_client("bench_lat_pub")
212
- sub = await self._make_client("bench_lat_sub")
213
- await sub.subscribe(["bench.lat.*"])
214
- await asyncio.sleep(0.2)
215
-
216
- latencies = []
217
- for i in range(n):
218
- t0 = time.time()
219
- await pub.publish("bench.lat.test", {"i": i})
220
- await pub.recv(timeout=2) # ack
221
- msg = await sub.recv(timeout=2)
222
- if msg and msg.get("type") == "event":
223
- latencies.append((time.time() - t0) * 1000)
224
-
225
- await pub.close()
226
- await sub.close()
227
-
228
- if latencies:
229
- latencies.sort()
230
- self.results["latency"] = {
231
- "samples": len(latencies),
232
- "avg_ms": round(statistics.mean(latencies), 2),
233
- "p50_ms": round(latencies[len(latencies) // 2], 2),
234
- "p95_ms": round(latencies[int(len(latencies) * 0.95)], 2),
235
- "p99_ms": round(latencies[int(len(latencies) * 0.99)], 2),
236
- }
237
- print(f"{TAG} avg={self.results['latency']['avg_ms']}ms, "
238
- f"p50={self.results['latency']['p50_ms']}ms, "
239
- f"p99={self.results['latency']['p99_ms']}ms")
240
-
241
- # ── Fan-out: 1 pub, N subs ──
242
-
243
- async def bench_fanout(self, n_events: int = 2000):
244
- for n_subs in [1, 5, 10, 50]:
245
- print(f"{TAG} Fan-out x{n_subs}: {n_events} events...")
246
- pub = await self._make_client("bench_fo_pub")
247
- subs = []
248
- for i in range(n_subs):
249
- s = await self._make_client(f"bench_fo_sub_{i}")
250
- await s.subscribe(["bench.fo.*"])
251
- subs.append(s)
252
- await asyncio.sleep(0.3)
253
-
254
- counts = [0] * n_subs
255
- stop = asyncio.Event()
256
-
257
- async def _recv(idx, client):
258
- while not stop.is_set():
259
- try:
260
- raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
261
- if '"event"' in raw and '"ack"' not in raw:
262
- counts[idx] += 1
263
- except Exception:
264
- pass
265
-
266
- tasks = [asyncio.create_task(_recv(i, s)) for i, s in enumerate(subs)]
267
- ack_stop = asyncio.Event()
268
-
269
- async def _drain():
270
- while not ack_stop.is_set():
271
- try:
272
- await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
273
- except Exception:
274
- pass
275
-
276
- ack_task = asyncio.create_task(_drain())
277
-
278
- t0 = time.time()
279
- for i in range(n_events):
280
- await pub.publish("bench.fo.test", {"i": i})
281
- send_time = time.time() - t0
282
-
283
- await asyncio.sleep(max(3, n_events * n_subs / 3000))
284
- stop.set()
285
- ack_stop.set()
286
- for t in tasks:
287
- await t
288
- ack_task.cancel()
289
-
290
- await pub.close()
291
- for s in subs:
292
- await s.close()
293
-
294
- avg_recv = sum(counts) / n_subs if n_subs else 0
295
- self.results[f"fanout_{n_subs}"] = {
296
- "subs": n_subs,
297
- "events": n_events,
298
- "send_rate": round(n_events / send_time) if send_time > 0 else 0,
299
- "avg_recv": round(avg_recv),
300
- "min_recv": min(counts),
301
- }
302
- print(f"{TAG} subs={n_subs}, avg_recv={round(avg_recv)}/{n_events}, "
303
- f"min={min(counts)}")
304
-
305
- # ── Save results ──
306
-
307
- def save(self, ws_url: str):
308
- os.makedirs(RESULTS_DIR, exist_ok=True)
309
- now = datetime.now()
310
- filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
311
-
312
- hub_stats = _get_hub_stats(ws_url)
313
- resources = _sample_hub_resources(ws_url)
314
-
315
- data = {
316
- "timestamp": now.isoformat(),
317
- "env": {
318
- "platform": sys.platform,
319
- "python": platform.python_version(),
320
- },
321
- "hub_resources": resources,
322
- "hub_counters": hub_stats.get("counters", {}),
323
- **self.results,
324
- }
325
- with open(filepath, "w", encoding="utf-8") as f:
326
- json.dump(data, f, indent=2, ensure_ascii=False)
327
- print(f"{TAG} Results saved: {filepath}")
328
-
329
-
330
- # ── Entry point ──
331
-
332
- async def _run(ws_url: str, token: str):
333
- bench = BenchRunner(ws_url, token)
334
- await bench.bench_throughput()
335
- await bench.bench_latency()
336
- await bench.bench_fanout()
337
- bench.save(ws_url)
338
-
339
-
340
- def main():
341
- # Read boot_info from stdin (only token)
342
- token = ""
343
- try:
344
- line = sys.stdin.readline().strip()
345
- if line:
346
- boot = json.loads(line)
347
- token = boot.get("token", "")
348
- except Exception:
349
- pass
350
-
351
- # Read registry_port from environment variable
352
- registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
353
-
354
- if not token or not registry_port:
355
- print(f"{TAG} ERROR: Missing token or KITE_REGISTRY_PORT")
356
- sys.exit(1)
357
-
358
- if os.environ.get("KITE_DEBUG") != "1":
359
- print(f"{TAG} ERROR: Benchmark requires KITE_DEBUG=1 (multiple clients share one token)")
360
- sys.exit(1)
361
-
362
- registry_url = f"http://127.0.0.1:{registry_port}"
363
- print(f"{TAG} Discovering event_hub via registry...")
364
-
365
- ws_url = _discover_hub(registry_url, token)
366
- if not ws_url:
367
- print(f"{TAG} ERROR: event_hub not found")
368
- sys.exit(1)
369
-
370
- print(f"{TAG} Hub found: {ws_url}")
371
- print(f"{TAG} Starting benchmarks...")
372
-
373
- asyncio.run(_run(ws_url, token))
374
-
375
- print(f"{TAG} Benchmarks complete, exiting.")
376
-
377
-
378
- if __name__ == "__main__":
379
- main()
1
+ """
2
+ Event Hub Benchmark module entry point.
3
+ Discovers hub via Registry, runs benchmarks, saves results, exits.
4
+ """
5
+
6
+ import asyncio
7
+ import builtins
8
+ import json
9
+ import os
10
+ import platform
11
+ import re
12
+ import sys
13
+ import threading
14
+ import time
15
+ import traceback
16
+ from datetime import datetime, timezone
17
+
18
+ import statistics
19
+ import uuid
20
+
21
+ import httpx
22
+ import websockets
23
+
24
+ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
25
+ if _project_root not in sys.path:
26
+ sys.path.insert(0, _project_root)
27
+
28
+ # Results go to KITE_MODULE_DATA (set by Launcher per module)
29
+ # Fallback to source tree for standalone runs
30
+ RESULTS_DIR = os.environ.get("KITE_MODULE_DATA") or os.path.join(
31
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
32
+ "core", "event_hub", "bench_results",
33
+ )
34
+ TAG = "[event_hub_bench]"
35
+
36
+
37
+ # ── Module configuration ──
38
+ MODULE_NAME = "event_hub_bench"
39
+
40
+
41
+ # ── Timestamped print + log file writer ──
42
+ _builtin_print = builtins.print
43
+ _start_ts = time.monotonic()
44
+ _last_ts = time.monotonic()
45
+ _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
46
+ _log_lock = threading.Lock()
47
+ _log_latest_path = None
48
+ _log_daily_path = None
49
+ _log_daily_date = ""
50
+ _log_dir = None
51
+ _crash_log_path = None
52
+
53
+ def _strip_ansi(s: str) -> str:
54
+ return _ANSI_RE.sub("", s)
55
+
56
+ def _resolve_daily_log_path():
57
+ global _log_daily_path, _log_daily_date
58
+ if not _log_dir:
59
+ return
60
+ today = datetime.now().strftime("%Y-%m-%d")
61
+ if today == _log_daily_date and _log_daily_path:
62
+ return
63
+ month_dir = os.path.join(_log_dir, today[:7])
64
+ os.makedirs(month_dir, exist_ok=True)
65
+ _log_daily_path = os.path.join(month_dir, f"{today}.log")
66
+ _log_daily_date = today
67
+
68
+ def _write_log(plain_line: str):
69
+ with _log_lock:
70
+ if _log_latest_path:
71
+ try:
72
+ with open(_log_latest_path, "a", encoding="utf-8") as f:
73
+ f.write(plain_line)
74
+ except Exception:
75
+ pass
76
+ _resolve_daily_log_path()
77
+ if _log_daily_path:
78
+ try:
79
+ with open(_log_daily_path, "a", encoding="utf-8") as f:
80
+ f.write(plain_line)
81
+ except Exception:
82
+ pass
83
+
84
+ def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
85
+ record = {
86
+ "timestamp": datetime.now(timezone.utc).isoformat(),
87
+ "module": MODULE_NAME,
88
+ "thread": thread_name or threading.current_thread().name,
89
+ "exception_type": exc_type.__name__ if exc_type else "Unknown",
90
+ "exception_message": str(exc_value),
91
+ "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
92
+ "severity": severity,
93
+ "handled": handled,
94
+ "process_id": os.getpid(),
95
+ "platform": sys.platform,
96
+ "runtime_version": f"Python {sys.version.split()[0]}",
97
+ }
98
+
99
+ if exc_tb:
100
+ tb_entries = traceback.extract_tb(exc_tb)
101
+ if tb_entries:
102
+ last = tb_entries[-1]
103
+ record["context"] = {
104
+ "function": last.name,
105
+ "file": os.path.basename(last.filename),
106
+ "line": last.lineno,
107
+ }
108
+
109
+ line = json.dumps(record, ensure_ascii=False) + "\n"
110
+
111
+ if _crash_log_path:
112
+ try:
113
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
114
+ f.write(line)
115
+ except Exception:
116
+ pass
117
+
118
+ if _log_dir:
119
+ try:
120
+ today = datetime.now().strftime("%Y-%m-%d")
121
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
122
+ os.makedirs(archive_dir, exist_ok=True)
123
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
124
+ with open(archive_path, "a", encoding="utf-8") as f:
125
+ f.write(line)
126
+ except Exception:
127
+ pass
128
+
129
+ def _print_crash_summary(exc_type, exc_tb, thread_name=None):
130
+ RED = "\033[91m"
131
+ RESET = "\033[0m"
132
+
133
+ if exc_tb:
134
+ tb_entries = traceback.extract_tb(exc_tb)
135
+ if tb_entries:
136
+ last = tb_entries[-1]
137
+ location = f"{os.path.basename(last.filename)}:{last.lineno}"
138
+ else:
139
+ location = "unknown"
140
+ else:
141
+ location = "unknown"
142
+
143
+ prefix = f"[{MODULE_NAME}]"
144
+ if thread_name:
145
+ _builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
146
+ f"{exc_type.__name__} in {location}{RESET}")
147
+ else:
148
+ _builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
149
+ if _crash_log_path:
150
+ _builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
151
+
152
+ def _setup_exception_hooks():
153
+ _orig_excepthook = sys.excepthook
154
+
155
+ def _excepthook(exc_type, exc_value, exc_tb):
156
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
157
+ _print_crash_summary(exc_type, exc_tb)
158
+ _orig_excepthook(exc_type, exc_value, exc_tb)
159
+
160
+ sys.excepthook = _excepthook
161
+
162
+ if hasattr(threading, "excepthook"):
163
+ def _thread_excepthook(args):
164
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
165
+ thread_name=args.thread.name if args.thread else "unknown",
166
+ severity="error", handled=False)
167
+ _print_crash_summary(args.exc_type, args.exc_traceback,
168
+ thread_name=args.thread.name if args.thread else None)
169
+
170
+ threading.excepthook = _thread_excepthook
171
+
172
+ def _tprint(*args, **kwargs):
173
+ global _last_ts
174
+ now = time.monotonic()
175
+ elapsed = now - _start_ts
176
+ delta = now - _last_ts
177
+ _last_ts = now
178
+
179
+ if elapsed < 1:
180
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
181
+ elif elapsed < 100:
182
+ elapsed_str = f"{elapsed:.1f}s"
183
+ else:
184
+ elapsed_str = f"{elapsed:.0f}s"
185
+
186
+ if delta < 0.001:
187
+ delta_str = ""
188
+ elif delta < 1:
189
+ delta_str = f"+{delta * 1000:.0f}ms"
190
+ elif delta < 100:
191
+ delta_str = f"+{delta:.1f}s"
192
+ else:
193
+ delta_str = f"+{delta:.0f}s"
194
+
195
+ ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
196
+
197
+ _builtin_print(*args, **kwargs)
198
+
199
+ if _log_latest_path or _log_daily_path:
200
+ sep = kwargs.get("sep", " ")
201
+ end = kwargs.get("end", "\n")
202
+ text = sep.join(str(a) for a in args)
203
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
204
+ _write_log(prefix + _strip_ansi(text) + end)
205
+
206
+ builtins.print = _tprint
207
+
208
+ # Initialize log files
209
+ module_data = os.environ.get("KITE_MODULE_DATA")
210
+ if module_data:
211
+ _log_dir = os.path.join(module_data, "log")
212
+ os.makedirs(_log_dir, exist_ok=True)
213
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
214
+
215
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
216
+ try:
217
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
218
+ pass
219
+ except Exception:
220
+ _log_latest_path = None
221
+
222
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
223
+ try:
224
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
225
+ pass
226
+ except Exception:
227
+ _crash_log_path = None
228
+
229
+ _resolve_daily_log_path()
230
+
231
+ _setup_exception_hooks()
232
+
233
+
234
+ def _register_to_registry(client: httpx.Client, registry_url: str, token: str):
235
+ """Register module identity to Registry (events, metadata — no API endpoint)."""
236
+ payload = {
237
+ "action": "register",
238
+ "module_id": "event_hub_bench",
239
+ "module_type": "tool",
240
+ "name": "Event Hub Benchmark",
241
+ }
242
+ headers = {"Authorization": f"Bearer {token}"}
243
+ try:
244
+ resp = client.post(
245
+ f"{registry_url}/modules",
246
+ json=payload, headers=headers,
247
+ )
248
+ if resp.status_code == 200:
249
+ print(f"{TAG} Registered to Registry")
250
+ else:
251
+ print(f"{TAG} WARNING: Registry returned {resp.status_code}")
252
+ except Exception as e:
253
+ print(f"{TAG} WARNING: Registry registration failed: {e}")
254
+
255
+
256
+ def _discover_hub(client: httpx.Client, registry_url: str, token: str, timeout: float = 30) -> str | None:
257
+ """Poll registry until event_hub is found. Returns ws:// URL or None."""
258
+ headers = {"Authorization": f"Bearer {token}"}
259
+ deadline = time.time() + timeout
260
+ while time.time() < deadline:
261
+ try:
262
+ resp = client.get(
263
+ f"{registry_url}/get/event_hub",
264
+ headers=headers,
265
+ )
266
+ if resp.status_code == 200:
267
+ info = resp.json()
268
+ ws_url = (info.get("metadata") or {}).get("ws_endpoint")
269
+ if ws_url:
270
+ return ws_url
271
+ except Exception:
272
+ pass
273
+ time.sleep(0.2)
274
+ return None
275
+
276
+
277
+ def _get_hub_stats(ws_url: str) -> dict:
278
+ """Fetch hub /stats via HTTP (derive from ws:// URL)."""
279
+ http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
280
+ try:
281
+ resp = httpx.get(f"{http_url}/stats", timeout=5)
282
+ if resp.status_code == 200:
283
+ return resp.json()
284
+ except Exception:
285
+ pass
286
+ return {}
287
+
288
+
289
+ def _sample_hub_resources(ws_url: str) -> dict:
290
+ """Sample hub process CPU/memory via psutil. Returns {} if unavailable."""
291
+ try:
292
+ import psutil
293
+ except ImportError:
294
+ return {"error": "psutil not installed"}
295
+ http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
296
+ port = int(http_url.rsplit(":", 1)[1].split("/")[0])
297
+ for conn in psutil.net_connections(kind="tcp"):
298
+ if conn.laddr.port == port and conn.status == "LISTEN":
299
+ try:
300
+ p = psutil.Process(conn.pid)
301
+ mem = p.memory_info()
302
+ return {
303
+ "pid": conn.pid,
304
+ "cpu_percent": p.cpu_percent(interval=1),
305
+ "rss_mb": round(mem.rss / 1048576, 1),
306
+ "vms_mb": round(mem.vms / 1048576, 1),
307
+ "threads": p.num_threads(),
308
+ }
309
+ except Exception:
310
+ pass
311
+ return {"error": "hub process not found"}
312
+
313
+
314
+ # ── WebSocket client ──
315
+
316
+ class WsClient:
317
+ """Lightweight async WebSocket client for hub benchmarks."""
318
+
319
+ def __init__(self, ws_url: str, token: str, client_id: str):
320
+ self.url = f"{ws_url}?token={token}&id={client_id}"
321
+ self.id = client_id
322
+ self.ws = None
323
+
324
+ async def connect(self):
325
+ self.ws = await websockets.connect(self.url, max_size=None)
326
+
327
+ async def subscribe(self, patterns: list[str]):
328
+ await self.ws.send(json.dumps({"type": "subscribe", "events": patterns}))
329
+
330
+ async def publish(self, event_type: str, data: dict) -> str:
331
+ eid = str(uuid.uuid4())
332
+ await self.ws.send(json.dumps({
333
+ "type": "event", "event_id": eid, "event": event_type,
334
+ "source": self.id,
335
+ "timestamp": datetime.now(timezone.utc).isoformat(),
336
+ "data": data,
337
+ }))
338
+ return eid
339
+
340
+ async def recv(self, timeout=5.0):
341
+ try:
342
+ return json.loads(await asyncio.wait_for(self.ws.recv(), timeout=timeout))
343
+ except Exception:
344
+ return None
345
+
346
+ async def close(self):
347
+ if self.ws:
348
+ await self.ws.close()
349
+
350
+
351
+ # ── Benchmark runner ──
352
+
353
+ class BenchRunner:
354
+ def __init__(self, ws_url: str, token: str):
355
+ self.ws_url = ws_url
356
+ self.token = token
357
+ self.results = {}
358
+
359
+ async def _make_client(self, name: str) -> WsClient:
360
+ c = WsClient(self.ws_url, self.token, name)
361
+ await c.connect()
362
+ return c
363
+
364
+ # ── Throughput: burst N events, measure hub processing rate ──
365
+
366
+ async def bench_throughput(self, n: int = 10000):
367
+ print(f"{TAG} Throughput test: {n} events...")
368
+ pub = await self._make_client("bench_tp_pub")
369
+ sub = await self._make_client("bench_tp_sub")
370
+ await sub.subscribe(["bench.tp.*"])
371
+ await asyncio.sleep(0.2)
372
+
373
+ recvd = 0
374
+ stop = asyncio.Event()
375
+
376
+ async def _recv():
377
+ nonlocal recvd
378
+ while not stop.is_set():
379
+ try:
380
+ raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
381
+ if '"event"' in raw and '"ack"' not in raw:
382
+ recvd += 1
383
+ except Exception:
384
+ pass
385
+
386
+ async def _drain_acks():
387
+ while not stop.is_set():
388
+ try:
389
+ await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
390
+ except Exception:
391
+ pass
392
+
393
+ recv_task = asyncio.create_task(_recv())
394
+ ack_task = asyncio.create_task(_drain_acks())
395
+
396
+ hub_before = (_get_hub_stats(self.ws_url).get("counters") or {})
397
+
398
+ t0 = time.time()
399
+ for i in range(n):
400
+ await pub.publish("bench.tp.test", {"i": i})
401
+ send_time = time.time() - t0
402
+
403
+ deadline = time.time() + max(15, n / 300)
404
+ while recvd < n and time.time() < deadline:
405
+ await asyncio.sleep(0.1)
406
+ total_time = time.time() - t0
407
+
408
+ stop.set()
409
+ await recv_task
410
+ ack_task.cancel()
411
+
412
+ hub_after = (_get_hub_stats(self.ws_url).get("counters") or {})
413
+
414
+ await pub.close()
415
+ await sub.close()
416
+
417
+ self.results["throughput"] = {
418
+ "events": n,
419
+ "send_rate": round(n / send_time),
420
+ "recv_rate": round(recvd / total_time) if total_time > 0 else 0,
421
+ "client_recv": recvd,
422
+ "hub_queued": hub_after.get("events_queued", 0) - hub_before.get("events_queued", 0),
423
+ "hub_routed": hub_after.get("events_routed", 0) - hub_before.get("events_routed", 0),
424
+ "send_time_s": round(send_time, 2),
425
+ "total_time_s": round(total_time, 2),
426
+ }
427
+ print(f"{TAG} send_rate={self.results['throughput']['send_rate']} evt/s, "
428
+ f"recv={recvd}/{n}, time={round(total_time, 1)}s")
429
+
430
+ # ── Latency: sequential pub→recv round-trip ──
431
+
432
+ async def bench_latency(self, n: int = 200):
433
+ print(f"{TAG} Latency test: {n} samples...")
434
+ pub = await self._make_client("bench_lat_pub")
435
+ sub = await self._make_client("bench_lat_sub")
436
+ await sub.subscribe(["bench.lat.*"])
437
+ await asyncio.sleep(0.2)
438
+
439
+ latencies = []
440
+ for i in range(n):
441
+ t0 = time.time()
442
+ await pub.publish("bench.lat.test", {"i": i})
443
+ await pub.recv(timeout=2) # ack
444
+ msg = await sub.recv(timeout=2)
445
+ if msg and msg.get("type") == "event":
446
+ latencies.append((time.time() - t0) * 1000)
447
+
448
+ await pub.close()
449
+ await sub.close()
450
+
451
+ if latencies:
452
+ latencies.sort()
453
+ self.results["latency"] = {
454
+ "samples": len(latencies),
455
+ "avg_ms": round(statistics.mean(latencies), 2),
456
+ "p50_ms": round(latencies[len(latencies) // 2], 2),
457
+ "p95_ms": round(latencies[int(len(latencies) * 0.95)], 2),
458
+ "p99_ms": round(latencies[int(len(latencies) * 0.99)], 2),
459
+ }
460
+ print(f"{TAG} avg={self.results['latency']['avg_ms']}ms, "
461
+ f"p50={self.results['latency']['p50_ms']}ms, "
462
+ f"p99={self.results['latency']['p99_ms']}ms")
463
+
464
+ # ── Fan-out: 1 pub, N subs ──
465
+
466
+ async def bench_fanout(self, n_events: int = 2000):
467
+ for n_subs in [1, 5, 10, 50]:
468
+ print(f"{TAG} Fan-out x{n_subs}: {n_events} events...")
469
+ pub = await self._make_client("bench_fo_pub")
470
+ subs = []
471
+ for i in range(n_subs):
472
+ s = await self._make_client(f"bench_fo_sub_{i}")
473
+ await s.subscribe(["bench.fo.*"])
474
+ subs.append(s)
475
+ await asyncio.sleep(0.3)
476
+
477
+ counts = [0] * n_subs
478
+ stop = asyncio.Event()
479
+
480
+ async def _recv(idx, client):
481
+ while not stop.is_set():
482
+ try:
483
+ raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
484
+ if '"event"' in raw and '"ack"' not in raw:
485
+ counts[idx] += 1
486
+ except Exception:
487
+ pass
488
+
489
+ tasks = [asyncio.create_task(_recv(i, s)) for i, s in enumerate(subs)]
490
+ ack_stop = asyncio.Event()
491
+
492
+ async def _drain():
493
+ while not ack_stop.is_set():
494
+ try:
495
+ await asyncio.wait_for(pub.ws.recv(), timeout=0.5)
496
+ except Exception:
497
+ pass
498
+
499
+ ack_task = asyncio.create_task(_drain())
500
+
501
+ t0 = time.time()
502
+ for i in range(n_events):
503
+ await pub.publish("bench.fo.test", {"i": i})
504
+ send_time = time.time() - t0
505
+
506
+ await asyncio.sleep(max(3, n_events * n_subs / 3000))
507
+ stop.set()
508
+ ack_stop.set()
509
+ for t in tasks:
510
+ await t
511
+ ack_task.cancel()
512
+
513
+ await pub.close()
514
+ for s in subs:
515
+ await s.close()
516
+
517
+ avg_recv = sum(counts) / n_subs if n_subs else 0
518
+ self.results[f"fanout_{n_subs}"] = {
519
+ "subs": n_subs,
520
+ "events": n_events,
521
+ "send_rate": round(n_events / send_time) if send_time > 0 else 0,
522
+ "avg_recv": round(avg_recv),
523
+ "min_recv": min(counts),
524
+ }
525
+ print(f"{TAG} subs={n_subs}, avg_recv={round(avg_recv)}/{n_events}, "
526
+ f"min={min(counts)}")
527
+
528
+ # ── Save results ──
529
+
530
+ def save(self, ws_url: str):
531
+ os.makedirs(RESULTS_DIR, exist_ok=True)
532
+ now = datetime.now()
533
+ filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
534
+
535
+ hub_stats = _get_hub_stats(ws_url)
536
+ resources = _sample_hub_resources(ws_url)
537
+
538
+ data = {
539
+ "timestamp": now.isoformat(),
540
+ "env": {
541
+ "platform": sys.platform,
542
+ "python": platform.python_version(),
543
+ },
544
+ "hub_resources": resources,
545
+ "hub_counters": hub_stats.get("counters", {}),
546
+ **self.results,
547
+ }
548
+ with open(filepath, "w", encoding="utf-8") as f:
549
+ json.dump(data, f, indent=2, ensure_ascii=False)
550
+ print(f"{TAG} Results saved: {filepath}")
551
+
552
+
553
+ def _send_ready_event(ws_url: str, token: str):
554
+ """Send module.ready to Event Hub. Startup phase complete."""
555
+ try:
556
+ import websockets.sync.client as ws_sync
557
+ url = f"{ws_url}?token={token}&id=event_hub_bench"
558
+ with ws_sync.connect(url, close_timeout=3) as ws:
559
+ msg = {
560
+ "type": "event",
561
+ "event_id": str(uuid.uuid4()),
562
+ "event": "module.ready",
563
+ "source": "event_hub_bench",
564
+ "timestamp": datetime.now(timezone.utc).isoformat(),
565
+ "data": {"module_id": "event_hub_bench"},
566
+ }
567
+ ws.send(json.dumps(msg))
568
+ time.sleep(0.1)
569
+ except Exception as e:
570
+ print(f"{TAG} WARNING: Could not send module.ready: {e}")
571
+
572
+
573
+ def _send_exiting_event(ws_url: str, token: str, reason: str):
574
+ """Send module.exiting event to Event Hub before exit. Best-effort, non-blocking."""
575
+ try:
576
+ import websockets.sync.client as ws_sync
577
+ url = f"{ws_url}?token={token}&id=event_hub_bench"
578
+ with ws_sync.connect(url, close_timeout=3) as ws:
579
+ msg = {
580
+ "type": "event",
581
+ "event_id": str(uuid.uuid4()),
582
+ "event": "module.exiting",
583
+ "source": "event_hub_bench",
584
+ "timestamp": datetime.now(timezone.utc).isoformat(),
585
+ "data": {
586
+ "module_id": "event_hub_bench",
587
+ "reason": reason,
588
+ "action": "none",
589
+ },
590
+ }
591
+ ws.send(json.dumps(msg))
592
+ # Brief wait for delivery
593
+ time.sleep(0.3)
594
+ except Exception as e:
595
+ print(f"{TAG} WARNING: Could not send module.exiting: {e}")
596
+
597
+
598
+ # ── Entry point ──
599
+
600
+ async def _run(ws_url: str, token: str):
601
+ bench = BenchRunner(ws_url, token)
602
+ await bench.bench_throughput()
603
+ await bench.bench_latency()
604
+ await bench.bench_fanout()
605
+ bench.save(ws_url)
606
+
607
+
608
+ def main():
609
+ # Read boot_info from stdin (only token)
610
+ token = ""
611
+ try:
612
+ line = sys.stdin.readline().strip()
613
+ if line:
614
+ boot = json.loads(line)
615
+ token = boot.get("token", "")
616
+ except Exception:
617
+ pass
618
+
619
+ # Read registry_port from environment variable
620
+ registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
621
+
622
+ if not token or not registry_port:
623
+ print(f"{TAG} ERROR: Missing token or KITE_REGISTRY_PORT")
624
+ sys.exit(1)
625
+
626
+ registry_url = f"http://127.0.0.1:{registry_port}"
627
+
628
+ client = httpx.Client(timeout=5)
629
+
630
+ # Register to Registry first (identity declaration)
631
+ _register_to_registry(client, registry_url, token)
632
+
633
+ print(f"{TAG} Discovering event_hub via registry...")
634
+
635
+ ws_url = _discover_hub(client, registry_url, token)
636
+
637
+ client.close()
638
+ if not ws_url:
639
+ print(f"{TAG} ERROR: event_hub not found")
640
+ sys.exit(1)
641
+
642
+ print(f"{TAG} Hub found: {ws_url}")
643
+
644
+ # Debug mode required: multiple benchmark clients share one token
645
+ if os.environ.get("KITE_DEBUG") != "1":
646
+ print(f"{TAG} 需要调试模式才能运行 (启动时加 --debug)")
647
+ _send_exiting_event(ws_url, token, "requires KITE_DEBUG=1")
648
+ sys.exit(0)
649
+
650
+ # ── Startup complete — notify Launcher ──
651
+ _send_ready_event(ws_url, token)
652
+
653
+ # ── Business logic: run benchmarks ──
654
+ print(f"{TAG} Starting benchmarks...")
655
+
656
+ asyncio.run(_run(ws_url, token))
657
+
658
+ print(f"{TAG} Benchmarks complete, sending exit intent...")
659
+ _send_exiting_event(ws_url, token, "benchmarks complete")
660
+ print(f"{TAG} Exiting.")
661
+
662
+
663
+ if __name__ == "__main__":
664
+ main()