@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
@@ -4,13 +4,243 @@ Reads token from stdin boot_info, starts HTTP server on dynamic port,
4
4
  outputs port via stdout structured message, waits for Event Hub to trigger module.ready.
5
5
  """
6
6
 
7
+ import builtins
7
8
  import json
8
9
  import os
10
+ import re
9
11
  import sys
10
12
  import socket
13
+ import threading
14
+ import time
15
+ import traceback
16
+ from datetime import datetime, timezone
11
17
 
12
18
  import uvicorn
13
19
 
20
+
21
+ # ── Module configuration ──
22
+ MODULE_NAME = "registry"
23
+
24
+
25
+ def _fmt_elapsed(t0: float) -> str:
26
+ """Format elapsed time since t0: <1s → 'NNNms', >=1s → 'N.Ns', >=10s → 'NNs'."""
27
+ d = time.monotonic() - t0
28
+ if d < 1:
29
+ return f"{d * 1000:.0f}ms"
30
+ if d < 10:
31
+ return f"{d:.1f}s"
32
+ return f"{d:.0f}s"
33
+
34
+
35
+ # ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
36
+
37
+ class _SafeWriter:
38
+ """Wraps a stream to silently swallow BrokenPipeError on write/flush."""
39
+ def __init__(self, stream):
40
+ self._stream = stream
41
+
42
+ def write(self, s):
43
+ try:
44
+ self._stream.write(s)
45
+ except (BrokenPipeError, OSError):
46
+ pass
47
+
48
+ def flush(self):
49
+ try:
50
+ self._stream.flush()
51
+ except (BrokenPipeError, OSError):
52
+ pass
53
+
54
+ def __getattr__(self, name):
55
+ return getattr(self._stream, name)
56
+
57
+ sys.stdout = _SafeWriter(sys.stdout)
58
+ sys.stderr = _SafeWriter(sys.stderr)
59
+
60
+
61
+ # ── Timestamped print + log file writer ──
62
+ # Implements format: [timestamp] HH:MM:SS.mmm +delta [module_name] message
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]}",
123
+ }
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
+ # 1. Write to crashes.jsonl (current run)
138
+ if _crash_log_path:
139
+ try:
140
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
141
+ f.write(line)
142
+ except Exception:
143
+ pass
144
+
145
+ # 2. Write to daily crash archive
146
+ if _log_dir:
147
+ try:
148
+ today = datetime.now().strftime("%Y-%m-%d")
149
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
150
+ os.makedirs(archive_dir, exist_ok=True)
151
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
152
+ with open(archive_path, "a", encoding="utf-8") as f:
153
+ f.write(line)
154
+ except Exception:
155
+ pass
156
+
157
+ def _print_crash_summary(exc_type, exc_tb, thread_name=None):
158
+ """Print crash summary to console (red highlight)."""
159
+ RED = "\033[91m"
160
+ RESET = "\033[0m"
161
+
162
+ if exc_tb:
163
+ tb_entries = traceback.extract_tb(exc_tb)
164
+ if tb_entries:
165
+ last = tb_entries[-1]
166
+ location = f"{os.path.basename(last.filename)}:{last.lineno}"
167
+ else:
168
+ location = "unknown"
169
+ else:
170
+ location = "unknown"
171
+
172
+ prefix = f"[{MODULE_NAME}]"
173
+ if thread_name:
174
+ _builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
175
+ f"{exc_type.__name__} in {location}{RESET}")
176
+ else:
177
+ _builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
178
+ if _crash_log_path:
179
+ _builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
180
+
181
+ def _setup_exception_hooks():
182
+ """Set up global exception hooks (sys.excepthook + threading.excepthook)."""
183
+ _orig_excepthook = sys.excepthook
184
+
185
+ def _excepthook(exc_type, exc_value, exc_tb):
186
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
187
+ _print_crash_summary(exc_type, exc_tb)
188
+ _orig_excepthook(exc_type, exc_value, exc_tb)
189
+
190
+ sys.excepthook = _excepthook
191
+
192
+ if hasattr(threading, "excepthook"):
193
+ def _thread_excepthook(args):
194
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
195
+ thread_name=args.thread.name if args.thread else "unknown",
196
+ severity="error", handled=False)
197
+ _print_crash_summary(args.exc_type, args.exc_traceback,
198
+ thread_name=args.thread.name if args.thread else None)
199
+
200
+ threading.excepthook = _thread_excepthook
201
+
202
+ def _tprint(*args, **kwargs):
203
+ """Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
204
+ global _last_ts
205
+ now = time.monotonic()
206
+ elapsed = now - _start_ts
207
+ delta = now - _last_ts
208
+ _last_ts = now
209
+
210
+ # Format elapsed (cumulative time since start)
211
+ if elapsed < 1:
212
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
213
+ elif elapsed < 100:
214
+ elapsed_str = f"{elapsed:.1f}s"
215
+ else:
216
+ elapsed_str = f"{elapsed:.0f}s"
217
+
218
+ # Format delta (time since last print)
219
+ if delta < 0.001:
220
+ delta_str = ""
221
+ elif delta < 1:
222
+ delta_str = f"+{delta * 1000:.0f}ms"
223
+ elif delta < 100:
224
+ delta_str = f"+{delta:.1f}s"
225
+ else:
226
+ delta_str = f"+{delta:.0f}s"
227
+
228
+ # Current time
229
+ ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
230
+
231
+ # Print to console (original behavior)
232
+ _builtin_print(*args, **kwargs)
233
+
234
+ # Write to log files with timestamp prefix
235
+ if _log_latest_path or _log_daily_path:
236
+ sep = kwargs.get("sep", " ")
237
+ end = kwargs.get("end", "\n")
238
+ text = sep.join(str(a) for a in args)
239
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
240
+ _write_log(prefix + _strip_ansi(text) + end)
241
+
242
+ builtins.print = _tprint
243
+
14
244
  # Ensure project root is on sys.path (set by main.py or cli.js)
15
245
  _this_dir = os.path.dirname(os.path.abspath(__file__))
16
246
  _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(_this_dir))
@@ -58,6 +288,37 @@ def _bind_port(preferred: int, host: str) -> int:
58
288
 
59
289
 
60
290
  def main():
291
+ # Initialize log file paths
292
+ global _log_dir, _log_latest_path, _crash_log_path
293
+ module_data = os.environ.get("KITE_MODULE_DATA")
294
+ if module_data:
295
+ _log_dir = os.path.join(module_data, "log")
296
+ os.makedirs(_log_dir, exist_ok=True)
297
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
298
+
299
+ # latest.log — truncate on each startup
300
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
301
+ try:
302
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
303
+ pass # truncate
304
+ except Exception:
305
+ _log_latest_path = None
306
+
307
+ # crashes.jsonl — truncate on each startup
308
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
309
+ try:
310
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
311
+ pass # truncate
312
+ except Exception:
313
+ _crash_log_path = None
314
+
315
+ _resolve_daily_log_path()
316
+
317
+ # Setup exception hooks
318
+ _setup_exception_hooks()
319
+
320
+ _t0 = time.monotonic()
321
+
61
322
  # Kite environment
62
323
  kite_instance = os.environ.get("KITE_INSTANCE", "")
63
324
  is_debug = os.environ.get("KITE_DEBUG") == "1"
@@ -76,7 +337,7 @@ def main():
76
337
  print("[registry] 错误: 未通过 stdin boot_info 提供启动器令牌")
77
338
  sys.exit(1)
78
339
 
79
- print(f"[registry] 已收到启动器令牌 ({len(launcher_token)} 字符)")
340
+ print(f"[registry] 已收到启动器令牌 ({len(launcher_token)} 字符) ({_fmt_elapsed(_t0)})")
80
341
 
81
342
  if is_debug:
82
343
  print("[registry] 调试模式已启用 (KITE_DEBUG=1),接受所有令牌")
@@ -98,12 +359,20 @@ def main():
98
359
  # This message proves HTTP is about to start — Launcher uses it as readiness signal
99
360
  print(json.dumps({"kite": "port", "port": port}), flush=True)
100
361
 
101
- print(f"[registry] 启动中 {bind_host}:{port}")
362
+ print(f"[registry] 启动中 {bind_host}:{port} ({_fmt_elapsed(_t0)})")
102
363
 
103
364
  # Store port and advertise_ip for module.ready event later
104
365
  server.port = port
105
366
 
106
- uvicorn.run(server.app, host=bind_host, port=port, log_level="warning")
367
+ try:
368
+ config = uvicorn.Config(server.app, host=bind_host, port=port, log_level="warning")
369
+ uvi_server = uvicorn.Server(config)
370
+ server._uvicorn_server = uvi_server
371
+ uvi_server.run()
372
+ except Exception as e:
373
+ _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
374
+ _print_crash_summary(type(e), e.__traceback__)
375
+ sys.exit(1)
107
376
 
108
377
 
109
378
  if __name__ == "__main__":