@agentunion/kite 1.0.6 → 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 (112) hide show
  1. package/cli.js +127 -25
  2. package/core/event_hub/entry.py +384 -61
  3. package/core/event_hub/hub.py +8 -0
  4. package/core/event_hub/module.md +0 -1
  5. package/core/event_hub/server.py +169 -38
  6. package/core/kite_log.py +241 -0
  7. package/core/launcher/entry.py +1306 -425
  8. package/core/launcher/module_scanner.py +10 -9
  9. package/core/launcher/process_manager.py +555 -121
  10. package/core/registry/entry.py +335 -30
  11. package/core/registry/server.py +339 -256
  12. package/core/registry/store.py +13 -2
  13. package/extensions/agents/__init__.py +1 -0
  14. package/extensions/agents/assistant/__init__.py +1 -0
  15. package/extensions/agents/assistant/entry.py +380 -0
  16. package/extensions/agents/assistant/module.md +22 -0
  17. package/extensions/agents/assistant/server.py +236 -0
  18. package/extensions/channels/__init__.py +1 -0
  19. package/extensions/channels/acp_channel/__init__.py +1 -0
  20. package/extensions/channels/acp_channel/entry.py +380 -0
  21. package/extensions/channels/acp_channel/module.md +22 -0
  22. package/extensions/channels/acp_channel/server.py +236 -0
  23. package/{core → extensions}/event_hub_bench/entry.py +664 -371
  24. package/{core → extensions}/event_hub_bench/module.md +4 -2
  25. package/extensions/services/backup/__init__.py +1 -0
  26. package/extensions/services/backup/entry.py +380 -0
  27. package/extensions/services/backup/module.md +22 -0
  28. package/extensions/services/backup/server.py +244 -0
  29. package/extensions/services/model_service/__init__.py +1 -0
  30. package/extensions/services/model_service/entry.py +380 -0
  31. package/extensions/services/model_service/module.md +22 -0
  32. package/extensions/services/model_service/server.py +236 -0
  33. package/extensions/services/watchdog/entry.py +460 -143
  34. package/extensions/services/watchdog/module.md +3 -0
  35. package/extensions/services/watchdog/monitor.py +128 -13
  36. package/extensions/services/watchdog/server.py +75 -13
  37. package/extensions/services/web/__init__.py +1 -0
  38. package/extensions/services/web/config.yaml +149 -0
  39. package/extensions/services/web/entry.py +487 -0
  40. package/extensions/services/web/module.md +24 -0
  41. package/extensions/services/web/routes/__init__.py +1 -0
  42. package/extensions/services/web/routes/routes_call.py +189 -0
  43. package/extensions/services/web/routes/routes_config.py +512 -0
  44. package/extensions/services/web/routes/routes_contacts.py +98 -0
  45. package/extensions/services/web/routes/routes_devlog.py +99 -0
  46. package/extensions/services/web/routes/routes_phone.py +81 -0
  47. package/extensions/services/web/routes/routes_sms.py +48 -0
  48. package/extensions/services/web/routes/routes_stats.py +17 -0
  49. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  50. package/extensions/services/web/routes/schemas.py +216 -0
  51. package/extensions/services/web/server.py +332 -0
  52. package/extensions/services/web/static/css/style.css +1064 -0
  53. package/extensions/services/web/static/index.html +1445 -0
  54. package/extensions/services/web/static/js/app.js +4671 -0
  55. package/extensions/services/web/vendor/__init__.py +1 -0
  56. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  57. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  58. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  59. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  60. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  61. package/extensions/services/web/vendor/config.py +139 -0
  62. package/extensions/services/web/vendor/conversation/__init__.py +0 -0
  63. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  64. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  65. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  66. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  67. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  68. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  69. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  70. package/extensions/services/web/vendor/storage/identity.py +312 -0
  71. package/extensions/services/web/vendor/storage/store.py +507 -0
  72. package/extensions/services/web/vendor/task/__init__.py +0 -0
  73. package/extensions/services/web/vendor/task/manager.py +864 -0
  74. package/extensions/services/web/vendor/task/models.py +45 -0
  75. package/extensions/services/web/vendor/task/webhook.py +263 -0
  76. package/extensions/services/web/vendor/tools/__init__.py +0 -0
  77. package/extensions/services/web/vendor/tools/registry.py +321 -0
  78. package/main.py +344 -4
  79. package/package.json +11 -2
  80. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/core/__pycache__/data_dir.cpython-313.pyc +0 -0
  82. package/core/data_dir.py +0 -62
  83. package/core/event_hub/__pycache__/__init__.cpython-313.pyc +0 -0
  84. package/core/event_hub/__pycache__/bench.cpython-313.pyc +0 -0
  85. package/core/event_hub/__pycache__/bench_perf.cpython-313.pyc +0 -0
  86. package/core/event_hub/__pycache__/dedup.cpython-313.pyc +0 -0
  87. package/core/event_hub/__pycache__/entry.cpython-313.pyc +0 -0
  88. package/core/event_hub/__pycache__/hub.cpython-313.pyc +0 -0
  89. package/core/event_hub/__pycache__/router.cpython-313.pyc +0 -0
  90. package/core/event_hub/__pycache__/server.cpython-313.pyc +0 -0
  91. package/core/event_hub/bench_results/2026-02-28_13-26-48.json +0 -51
  92. package/core/event_hub/bench_results/2026-02-28_13-44-45.json +0 -51
  93. package/core/event_hub/bench_results/2026-02-28_13-45-39.json +0 -51
  94. package/core/launcher/__pycache__/__init__.cpython-313.pyc +0 -0
  95. package/core/launcher/__pycache__/entry.cpython-313.pyc +0 -0
  96. package/core/launcher/__pycache__/module_scanner.cpython-313.pyc +0 -0
  97. package/core/launcher/__pycache__/process_manager.cpython-313.pyc +0 -0
  98. package/core/launcher/data/log/lifecycle.jsonl +0 -1158
  99. package/core/launcher/data/token.txt +0 -1
  100. package/core/registry/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/registry/__pycache__/entry.cpython-313.pyc +0 -0
  102. package/core/registry/__pycache__/server.cpython-313.pyc +0 -0
  103. package/core/registry/__pycache__/store.cpython-313.pyc +0 -0
  104. package/core/registry/data/port.txt +0 -1
  105. package/core/registry/data/port_484.txt +0 -1
  106. package/extensions/__pycache__/__init__.cpython-313.pyc +0 -0
  107. package/extensions/services/__pycache__/__init__.cpython-313.pyc +0 -0
  108. package/extensions/services/watchdog/__pycache__/__init__.cpython-313.pyc +0 -0
  109. package/extensions/services/watchdog/__pycache__/entry.cpython-313.pyc +0 -0
  110. package/extensions/services/watchdog/__pycache__/monitor.cpython-313.pyc +0 -0
  111. package/extensions/services/watchdog/__pycache__/server.cpython-313.pyc +0 -0
  112. /package/{core/event_hub/bench_results/.gitkeep → extensions/services/web/vendor/bluetooth/__init__.py} +0 -0
@@ -1,112 +1,435 @@
1
1
  """
2
2
  Event Hub entry point.
3
- Reads boot_info from stdin, starts FastAPI server, registers to Registry.
3
+ Reads token from stdin boot_info, then reads launcher_ws_token + registry_port from
4
+ stdin kite messages (in any order), starts FastAPI server, outputs ws_endpoint via
5
+ stdout, waits for Launcher to connect, then registers to Registry after stdio disconnect.
4
6
  """
5
7
 
8
+ import builtins
6
9
  import json
7
10
  import os
11
+ import re
8
12
  import socket
9
13
  import sys
14
+ import threading
15
+ import time
16
+ import traceback
17
+ from datetime import datetime, timezone
10
18
 
11
- import httpx
12
19
  import uvicorn
13
20
 
14
- # Ensure project root is on sys.path
15
- _this_dir = os.path.dirname(os.path.abspath(__file__))
16
- _project_root = os.path.dirname(os.path.dirname(_this_dir))
17
- if _project_root not in sys.path:
18
- sys.path.insert(0, _project_root)
19
21
 
20
- from core.event_hub.hub import EventHub
21
- from core.event_hub.server import EventHubServer
22
+ # ── Module configuration ──
23
+ MODULE_NAME = "event_hub"
22
24
 
23
25
 
24
- def _get_free_port() -> int:
25
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
26
- s.bind(("127.0.0.1", 0))
27
- return s.getsockname()[1]
26
+ def _fmt_elapsed(t0: float) -> str:
27
+ """Format elapsed time since t0: <1s → 'NNNms', >=1s → 'N.Ns', >=10s → 'NNs'."""
28
+ d = time.monotonic() - t0
29
+ if d < 1:
30
+ return f"{d * 1000:.0f}ms"
31
+ if d < 10:
32
+ return f"{d:.1f}s"
33
+ return f"{d:.0f}s"
34
+
35
+
36
+ # ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
37
+
38
+ class _SafeWriter:
39
+ """Wraps a stream to silently swallow BrokenPipeError on write/flush."""
40
+ def __init__(self, stream):
41
+ self._stream = stream
28
42
 
43
+ def write(self, s):
44
+ try:
45
+ self._stream.write(s)
46
+ except (BrokenPipeError, OSError):
47
+ pass
29
48
 
30
- def _register_to_registry(token: str, registry_url: str, port: int, advertise_ip: str = "127.0.0.1"):
31
- """Synchronous registration to Registry at startup."""
32
- payload = {
33
- "action": "register",
34
- "module_id": "event_hub",
35
- "module_type": "infrastructure",
36
- "name": "Event Hub",
37
- "api_endpoint": f"http://{advertise_ip}:{port}",
38
- "health_endpoint": "/health",
39
- "metadata": {
40
- "ws_endpoint": f"ws://{advertise_ip}:{port}/ws",
41
- },
49
+ def flush(self):
50
+ try:
51
+ self._stream.flush()
52
+ except (BrokenPipeError, OSError):
53
+ pass
54
+
55
+ def __getattr__(self, name):
56
+ return getattr(self._stream, name)
57
+
58
+ sys.stdout = _SafeWriter(sys.stdout)
59
+ sys.stderr = _SafeWriter(sys.stderr)
60
+
61
+
62
+ # ── Timestamped print + log file writer ──
63
+ # Independent implementation per module (no shared code dependency)
64
+
65
+ _builtin_print = builtins.print
66
+ _start_ts = time.monotonic()
67
+ _last_ts = time.monotonic()
68
+ _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
69
+ _log_lock = threading.Lock()
70
+ _log_latest_path = None
71
+ _log_daily_path = None
72
+ _log_daily_date = ""
73
+ _log_dir = None
74
+ _crash_log_path = None
75
+
76
+ def _strip_ansi(s: str) -> str:
77
+ return _ANSI_RE.sub("", s)
78
+
79
+ def _resolve_daily_log_path():
80
+ """Resolve daily log path based on current date."""
81
+ global _log_daily_path, _log_daily_date
82
+ if not _log_dir:
83
+ return
84
+ today = datetime.now().strftime("%Y-%m-%d")
85
+ if today == _log_daily_date and _log_daily_path:
86
+ return
87
+ month_dir = os.path.join(_log_dir, today[:7])
88
+ os.makedirs(month_dir, exist_ok=True)
89
+ _log_daily_path = os.path.join(month_dir, f"{today}.log")
90
+ _log_daily_date = today
91
+
92
+ def _write_log(plain_line: str):
93
+ """Write a plain-text line to both latest.log and daily log."""
94
+ with _log_lock:
95
+ if _log_latest_path:
96
+ try:
97
+ with open(_log_latest_path, "a", encoding="utf-8") as f:
98
+ f.write(plain_line)
99
+ except Exception:
100
+ pass
101
+ _resolve_daily_log_path()
102
+ if _log_daily_path:
103
+ try:
104
+ with open(_log_daily_path, "a", encoding="utf-8") as f:
105
+ f.write(plain_line)
106
+ except Exception:
107
+ pass
108
+
109
+ def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
110
+ """Write crash record to crashes.jsonl + daily crash archive."""
111
+ record = {
112
+ "timestamp": datetime.now(timezone.utc).isoformat(),
113
+ "module": MODULE_NAME,
114
+ "thread": thread_name or threading.current_thread().name,
115
+ "exception_type": exc_type.__name__ if exc_type else "Unknown",
116
+ "exception_message": str(exc_value),
117
+ "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
118
+ "severity": severity,
119
+ "handled": handled,
120
+ "process_id": os.getpid(),
121
+ "platform": sys.platform,
122
+ "runtime_version": f"Python {sys.version.split()[0]}",
42
123
  }
43
- headers = {"Authorization": f"Bearer {token}"}
44
- resp = httpx.post(
45
- f"{registry_url}/modules",
46
- json=payload,
47
- headers=headers,
48
- timeout=5,
49
- )
50
- if resp.status_code == 200:
51
- print(f"[event_hub] Registered to Registry")
124
+
125
+ if exc_tb:
126
+ tb_entries = traceback.extract_tb(exc_tb)
127
+ if tb_entries:
128
+ last = tb_entries[-1]
129
+ record["context"] = {
130
+ "function": last.name,
131
+ "file": os.path.basename(last.filename),
132
+ "line": last.lineno,
133
+ }
134
+
135
+ line = json.dumps(record, ensure_ascii=False) + "\n"
136
+
137
+ if _crash_log_path:
138
+ try:
139
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
140
+ f.write(line)
141
+ except Exception:
142
+ pass
143
+
144
+ if _log_dir:
145
+ try:
146
+ today = datetime.now().strftime("%Y-%m-%d")
147
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
148
+ os.makedirs(archive_dir, exist_ok=True)
149
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
150
+ with open(archive_path, "a", encoding="utf-8") as f:
151
+ f.write(line)
152
+ except Exception:
153
+ pass
154
+
155
+ def _print_crash_summary(exc_type, exc_tb, thread_name=None):
156
+ """Print crash summary to console (red highlight)."""
157
+ RED = "\033[91m"
158
+ RESET = "\033[0m"
159
+
160
+ if exc_tb:
161
+ tb_entries = traceback.extract_tb(exc_tb)
162
+ if tb_entries:
163
+ last = tb_entries[-1]
164
+ location = f"{os.path.basename(last.filename)}:{last.lineno}"
165
+ else:
166
+ location = "unknown"
167
+ else:
168
+ location = "unknown"
169
+
170
+ prefix = f"[{MODULE_NAME}]"
171
+ if thread_name:
172
+ _builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
173
+ f"{exc_type.__name__} in {location}{RESET}")
174
+ else:
175
+ _builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
176
+ if _crash_log_path:
177
+ _builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
178
+
179
+ def _setup_exception_hooks():
180
+ """Set up global exception hooks."""
181
+ _orig_excepthook = sys.excepthook
182
+
183
+ def _excepthook(exc_type, exc_value, exc_tb):
184
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
185
+ _print_crash_summary(exc_type, exc_tb)
186
+ _orig_excepthook(exc_type, exc_value, exc_tb)
187
+
188
+ sys.excepthook = _excepthook
189
+
190
+ if hasattr(threading, "excepthook"):
191
+ def _thread_excepthook(args):
192
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
193
+ thread_name=args.thread.name if args.thread else "unknown",
194
+ severity="error", handled=False)
195
+ _print_crash_summary(args.exc_type, args.exc_traceback,
196
+ thread_name=args.thread.name if args.thread else None)
197
+
198
+ threading.excepthook = _thread_excepthook
199
+
200
+ def _tprint(*args, **kwargs):
201
+ """Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
202
+ global _last_ts
203
+ now = time.monotonic()
204
+ elapsed = now - _start_ts
205
+ delta = now - _last_ts
206
+ _last_ts = now
207
+
208
+ if elapsed < 1:
209
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
210
+ elif elapsed < 100:
211
+ elapsed_str = f"{elapsed:.1f}s"
212
+ else:
213
+ elapsed_str = f"{elapsed:.0f}s"
214
+
215
+ if delta < 0.001:
216
+ delta_str = ""
217
+ elif delta < 1:
218
+ delta_str = f"+{delta * 1000:.0f}ms"
219
+ elif delta < 100:
220
+ delta_str = f"+{delta:.1f}s"
52
221
  else:
53
- print(f"[event_hub] WARNING: Registry returned {resp.status_code}")
222
+ delta_str = f"+{delta:.0f}s"
223
+
224
+ ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
225
+
226
+ _builtin_print(*args, **kwargs)
54
227
 
228
+ if _log_latest_path or _log_daily_path:
229
+ sep = kwargs.get("sep", " ")
230
+ end = kwargs.get("end", "\n")
231
+ text = sep.join(str(a) for a in args)
232
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
233
+ _write_log(prefix + _strip_ansi(text) + end)
55
234
 
56
- def _read_test_mode() -> bool:
57
- """Read test_mode from module.md frontmatter."""
235
+ builtins.print = _tprint
236
+
237
+ # Ensure project root is on sys.path
238
+ _this_dir = os.path.dirname(os.path.abspath(__file__))
239
+ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(_this_dir))
240
+ if _project_root not in sys.path:
241
+ sys.path.insert(0, _project_root)
242
+
243
+ from core.event_hub.hub import EventHub
244
+ from core.event_hub.server import EventHubServer
245
+
246
+
247
+ def _read_module_md() -> dict:
248
+ """Read preferred_port, advertise_ip from own module.md."""
58
249
  md_path = os.path.join(_this_dir, "module.md")
250
+ result = {"preferred_port": 0, "advertise_ip": "127.0.0.1"}
59
251
  try:
60
252
  with open(md_path, encoding="utf-8") as f:
61
253
  text = f.read()
62
- import re, yaml
63
254
  m = re.match(r'^---\s*\n(.*?)\n---', text, re.DOTALL)
64
255
  if m:
65
- fm = yaml.safe_load(m.group(1)) or {}
66
- return bool(fm.get("test_mode", False))
256
+ try:
257
+ import yaml
258
+ fm = yaml.safe_load(m.group(1)) or {}
259
+ except ImportError:
260
+ fm = {}
261
+ result["preferred_port"] = int(fm.get("preferred_port", 0))
262
+ result["advertise_ip"] = fm.get("advertise_ip", "127.0.0.1")
67
263
  except Exception:
68
264
  pass
69
- return False
265
+ return result
266
+
267
+
268
+ def _bind_port(preferred: int, host: str) -> int:
269
+ """Try preferred port first, fall back to OS-assigned."""
270
+ if preferred:
271
+ try:
272
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
273
+ s.bind((host, preferred))
274
+ return preferred
275
+ except OSError:
276
+ pass
277
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
278
+ s.bind((host, 0))
279
+ return s.getsockname()[1]
280
+
281
+
282
+ def _read_stdin_kite_messages(expected: set[str], timeout: float = 10) -> dict[str, dict]:
283
+ """Read multiple structured kite messages from stdin until all expected types received.
284
+
285
+ Args:
286
+ expected: set of kite message types to wait for (e.g. {"launcher_ws_token", "registry_port"})
287
+ timeout: total timeout in seconds
288
+
289
+ Returns:
290
+ dict mapping kite type -> message dict. Missing types are absent from the result.
291
+ """
292
+ collected: dict[str, dict] = {}
293
+ remaining = set(expected)
294
+
295
+ def _read():
296
+ while remaining:
297
+ try:
298
+ line = sys.stdin.readline().strip()
299
+ if not line:
300
+ return # stdin closed
301
+ msg = json.loads(line)
302
+ if isinstance(msg, dict) and "kite" in msg:
303
+ kite_type = msg["kite"]
304
+ collected[kite_type] = msg
305
+ remaining.discard(kite_type)
306
+ except Exception:
307
+ return # parse error or stdin closed
308
+
309
+ t = threading.Thread(target=_read, daemon=True)
310
+ t.start()
311
+ t.join(timeout=timeout)
312
+ return collected
70
313
 
71
314
 
72
315
  def main():
73
- # Read boot_info from stdin
316
+ # Initialize log file paths
317
+ global _log_dir, _log_latest_path, _crash_log_path
318
+ module_data = os.environ.get("KITE_MODULE_DATA")
319
+ if module_data:
320
+ _log_dir = os.path.join(module_data, "log")
321
+ os.makedirs(_log_dir, exist_ok=True)
322
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
323
+
324
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
325
+ try:
326
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
327
+ pass
328
+ except Exception:
329
+ _log_latest_path = None
330
+
331
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
332
+ try:
333
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
334
+ pass
335
+ except Exception:
336
+ _crash_log_path = None
337
+
338
+ _resolve_daily_log_path()
339
+
340
+ _setup_exception_hooks()
341
+
342
+ _t0 = time.monotonic()
343
+
344
+ # Kite environment
345
+ kite_instance = os.environ.get("KITE_INSTANCE", "")
346
+ is_debug = os.environ.get("KITE_DEBUG") == "1"
347
+
348
+ # Step 1: Read token from stdin boot_info
74
349
  token = ""
75
- registry_port = 0
76
- bind_host = "127.0.0.1"
77
- advertise_ip = "127.0.0.1"
78
350
  try:
79
351
  line = sys.stdin.readline().strip()
80
352
  if line:
81
353
  boot_info = json.loads(line)
82
354
  token = boot_info.get("token", "")
83
- registry_port = boot_info.get("registry_port", 0)
84
- bind_host = boot_info.get("bind", "127.0.0.1")
85
- advertise_ip = boot_info.get("advertise_ip", "127.0.0.1")
86
355
  except Exception:
87
356
  pass
88
357
 
89
- if not token or not registry_port:
90
- print("[event_hub] ERROR: Missing token or registry_port in boot_info")
358
+ if not token:
359
+ print("[event_hub] 错误: boot_info 中缺少令牌")
360
+ sys.exit(1)
361
+
362
+ # Step 2: Check env for registry_port first (fast path for restart / Phase 3.5+ scenarios)
363
+ registry_port = int(os.environ.get("KITE_REGISTRY_PORT", "0"))
364
+
365
+ # Determine which stdin messages to wait for
366
+ stdin_expected = {"launcher_ws_token"}
367
+ if not registry_port:
368
+ stdin_expected.add("registry_port")
369
+
370
+ kite_msgs = _read_stdin_kite_messages(stdin_expected, timeout=10)
371
+
372
+ launcher_ws_token = ""
373
+ ws_msg = kite_msgs.get("launcher_ws_token")
374
+ if ws_msg:
375
+ launcher_ws_token = ws_msg.get("launcher_ws_token", "")
376
+
377
+ if launcher_ws_token:
378
+ print(f"[event_hub] 已收到启动器 WS 令牌 ({len(launcher_ws_token)} 字符)")
379
+ else:
380
+ print("[event_hub] 警告: 未收到 launcher_ws_token,启动器引导认证已禁用")
381
+
382
+ # Step 3: registry_port — env already had it, or read from stdin
383
+ if not registry_port:
384
+ port_msg = kite_msgs.get("registry_port")
385
+ if port_msg:
386
+ registry_port = int(port_msg.get("registry_port", 0))
387
+ if not registry_port:
388
+ print("[event_hub] 错误: 未收到 registry_port(stdin 和环境变量均无)")
91
389
  sys.exit(1)
92
390
 
93
- print(f"[event_hub] Token received ({len(token)} chars), registry port: {registry_port}")
391
+ print(f"[event_hub] 已收到令牌 ({len(token)} 字符),Registry 端口: {registry_port} ({_fmt_elapsed(_t0)})")
94
392
 
393
+ # Step 4: Read config from own module.md
394
+ md_config = _read_module_md()
395
+ advertise_ip = md_config["advertise_ip"]
396
+ preferred_port = md_config["preferred_port"]
397
+
398
+ # Step 5: Bind port and create server
399
+ bind_host = advertise_ip
400
+ port = _bind_port(preferred_port, bind_host)
95
401
  registry_url = f"http://127.0.0.1:{registry_port}"
96
- port = _get_free_port()
97
402
 
98
- # Register to Registry
99
- _register_to_registry(token, registry_url, port, advertise_ip)
403
+ if is_debug:
404
+ print("[event_hub] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
100
405
 
101
- # Create hub and server
102
- test_mode = _read_test_mode()
103
- if test_mode:
104
- print("[event_hub] WARNING: test_mode enabled, all tokens accepted")
105
406
  hub = EventHub()
106
- server = EventHubServer(hub, own_token=token, registry_url=registry_url, test_mode=test_mode)
407
+ server = EventHubServer(
408
+ hub,
409
+ own_token=token,
410
+ registry_url=registry_url,
411
+ launcher_ws_token=launcher_ws_token,
412
+ advertise_ip=advertise_ip,
413
+ port=port,
414
+ )
415
+
416
+ # Step 6: Output ws_endpoint via stdout (Launcher reads this)
417
+ ws_endpoint = f"ws://{advertise_ip}:{port}/ws"
418
+ print(json.dumps({"kite": "ws_endpoint", "ws_endpoint": ws_endpoint}), flush=True)
107
419
 
108
- print(f"[event_hub] Starting on {bind_host}:{port}")
109
- uvicorn.run(server.app, host=bind_host, port=port, log_level="warning")
420
+ # Step 7: Start HTTP + WS server
421
+ # Launcher will connect with launcher_ws_token → Event Hub sends module.ready stdio disconnect
422
+ # After stdio disconnect, Event Hub registers to Registry (done by server on_launcher_connected callback)
423
+ print(f"[event_hub] 启动中 {bind_host}:{port} ({_fmt_elapsed(_t0)})")
424
+ try:
425
+ config = uvicorn.Config(server.app, host=bind_host, port=port, log_level="warning")
426
+ uvi_server = uvicorn.Server(config)
427
+ server._uvicorn_server = uvi_server
428
+ uvi_server.run()
429
+ except Exception as e:
430
+ _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
431
+ _print_crash_summary(type(e), e.__traceback__)
432
+ sys.exit(1)
110
433
 
111
434
 
112
435
  if __name__ == "__main__":
@@ -55,6 +55,8 @@ class EventHub:
55
55
  self._cnt_dedup = 0
56
56
  self._cnt_errors = 0
57
57
  self._start_time = time.time()
58
+ # Shutdown callback: set by EventHubServer to handle own shutdown
59
+ self._shutdown_cb = None
58
60
 
59
61
  # ── Connection lifecycle ──
60
62
 
@@ -186,6 +188,12 @@ class EventHub:
186
188
  await self._route_event(module_id, msg)
187
189
  await self._send_ack(ws, event_id)
188
190
 
191
+ # Check if this is a shutdown event targeting event_hub
192
+ if event_type == "module.shutdown" and self._shutdown_cb:
193
+ data = msg.get("data", {})
194
+ if data.get("module_id") == "event_hub":
195
+ asyncio.create_task(self._shutdown_cb(data))
196
+
189
197
  # ── Routing ──
190
198
 
191
199
  async def _route_event(self, sender_id: str, msg: dict):
@@ -8,7 +8,6 @@ runtime: python
8
8
  entry: entry.py
9
9
  events: []
10
10
  subscriptions: []
11
- test_mode: true
12
11
  ---
13
12
 
14
13
  # Event Hub