@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
package/main.py CHANGED
@@ -2,14 +2,16 @@
2
2
  Kite development entry point.
3
3
  Loads .env, sets KITE_* defaults, parses --debug, starts Launcher.
4
4
  """
5
- import atexit
6
5
  import builtins
6
+ import json
7
7
  import os
8
+ import re
8
9
  import secrets
9
10
  import sys
10
11
  import threading
11
12
  import time
12
- from datetime import datetime
13
+ import traceback
14
+ from datetime import datetime, timezone
13
15
 
14
16
  # ── Timestamped print with delta + color ──
15
17
  # Covers the entire Launcher process. Child processes (registry, event_hub) are
@@ -17,23 +19,35 @@ from datetime import datetime
17
19
  # → print(), so timestamps & deltas are added at the relay point automatically.
18
20
 
19
21
  _builtin_print = builtins.print
22
+ _start_ts = time.monotonic()
20
23
  _last_ts = time.monotonic()
24
+ _first_line = True
25
+ _module_last_ts: dict[str, float] = {} # module_name -> last print timestamp
21
26
 
22
- # ANSI escape codes
23
- _DIM = "\033[90m" # gray — fast, normal
24
- _GREEN = "\033[32m" # green — noticeable but expected (subprocess launch, network wait)
25
- _RED = "\033[91m" # red — abnormally slow, likely a problem
26
- _RESET = "\033[0m"
27
+ # Regex to extract [module_name] prefix from print text
28
+ _MODULE_PREFIX_RE = re.compile(r"^\[([a-z_]+)\]")
27
29
 
28
- # ── Log file handles ──
29
- # Initialized lazily after KITE_MODULE_DATA is resolved (see _init_log_files)
30
+ # ANSI escape codes
31
+ _DIM = "\033[90m" # gray
32
+ _GREEN = "\033[32m" # green
33
+ _RED = "\033[91m" # red
34
+ _ORANGE = "\033[38;5;208m" # orange — first line highlight
35
+ _RESET = "\033[0m"
36
+
37
+ # ── Log file paths ──
38
+ # Initialized lazily after KITE_MODULE_DATA is resolved (see _init_log_files).
39
+ # We store paths (not handles) and open/write/close on each log line to avoid
40
+ # holding file handles for the entire process lifetime.
30
41
  _log_lock = threading.Lock()
31
- _log_latest = None # file handle: latest.log (cleared each startup)
32
- _log_daily = None # file handle: {YYYY-MM}/{YYYY-MM-DD}.log (append)
33
- _log_daily_date = "" # current date string to detect day rollover
42
+ _log_latest_path = None # path to latest.log
43
+ _log_daily_path = None # path to {YYYY-MM}/{YYYY-MM-DD}.log
44
+ _log_daily_date = "" # current date string to detect day rollover
45
+ _log_dir = None # base log directory
46
+
47
+ # Crash log paths
48
+ _crash_log_path = None # path to crashes.jsonl (current run)
34
49
 
35
50
  # Strip ANSI escape sequences for plain-text log files
36
- import re
37
51
  _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
38
52
 
39
53
  def _strip_ansi(s: str) -> str:
@@ -41,123 +55,238 @@ def _strip_ansi(s: str) -> str:
41
55
 
42
56
 
43
57
  def _init_log_files():
44
- """Initialize log file handles. Called after KITE_MODULE_DATA is set."""
45
- global _log_latest, _log_daily, _log_daily_date
58
+ """Initialize log file paths. Called after KITE_MODULE_DATA is set."""
59
+ global _log_latest_path, _log_dir, _crash_log_path
46
60
  module_data = os.environ.get("KITE_MODULE_DATA")
47
61
  if not module_data:
48
62
  return
49
- log_dir = os.path.join(module_data, "log")
50
- os.makedirs(log_dir, exist_ok=True)
63
+ _log_dir = os.path.join(module_data, "log")
64
+ os.makedirs(_log_dir, exist_ok=True)
65
+
66
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
51
67
 
52
- # latest.log — truncate on each startup
68
+ # latest.log — truncate on each startup (write empty to clear)
69
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
53
70
  try:
54
- _log_latest = open(os.path.join(log_dir, "latest.log"), "w", encoding="utf-8")
71
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
72
+ pass # truncate
55
73
  except Exception as e:
56
- _builtin_print(f"[launcher] 警告: 无法打开 latest.log: {e}")
74
+ _builtin_print(f"[launcher] 警告: 无法初始化 {os.path.basename(_log_latest_path)}: {e}")
75
+ _log_latest_path = None
57
76
 
58
- # daily log append
59
- _open_daily_log(log_dir)
77
+ # crashes.jsonltruncate on each startup
78
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
79
+ try:
80
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
81
+ pass # truncate
82
+ except Exception:
83
+ _crash_log_path = None
84
+
85
+ # daily log — ensure directory exists
86
+ _resolve_daily_log_path()
60
87
 
61
88
 
62
- def _open_daily_log(log_dir: str = None):
63
- """Open (or rotate) the daily log file based on current date."""
64
- global _log_daily, _log_daily_date
89
+ def _resolve_daily_log_path():
90
+ """Resolve the daily log file path based on current date."""
91
+ global _log_daily_path, _log_daily_date
92
+ if not _log_dir:
93
+ return
65
94
  today = datetime.now().strftime("%Y-%m-%d")
66
- if today == _log_daily_date and _log_daily:
95
+ if today == _log_daily_date and _log_daily_path:
67
96
  return
68
- if _log_daily:
69
- try:
70
- _log_daily.close()
71
- except Exception:
72
- pass
73
- if not log_dir:
74
- module_data = os.environ.get("KITE_MODULE_DATA")
75
- if not module_data:
76
- return
77
- log_dir = os.path.join(module_data, "log")
78
- month_dir = os.path.join(log_dir, today[:7]) # YYYY-MM
97
+ month_dir = os.path.join(_log_dir, today[:7]) # YYYY-MM
79
98
  os.makedirs(month_dir, exist_ok=True)
80
- try:
81
- _log_daily = open(os.path.join(month_dir, f"{today}.log"), "a", encoding="utf-8")
82
- _log_daily_date = today
83
- except Exception as e:
84
- _builtin_print(f"[launcher] 警告: 无法打开每日日志: {e}")
99
+ _log_daily_path = os.path.join(month_dir, f"{today}.log")
100
+ _log_daily_date = today
85
101
 
86
102
 
87
103
  def _write_log(plain_line: str):
88
- """Write a plain-text line to both latest.log and daily log."""
89
- global _log_daily
104
+ """Write a plain-text line to both latest.log and daily log (open-write-close)."""
90
105
  with _log_lock:
91
- if _log_latest:
106
+ if _log_latest_path:
92
107
  try:
93
- _log_latest.write(plain_line)
94
- _log_latest.flush()
108
+ with open(_log_latest_path, "a", encoding="utf-8") as f:
109
+ f.write(plain_line)
95
110
  except Exception:
96
111
  pass
97
112
  # Check daily rotation
98
- today = datetime.now().strftime("%Y-%m-%d")
99
- if today != _log_daily_date:
100
- _open_daily_log()
101
- if _log_daily:
113
+ _resolve_daily_log_path()
114
+ if _log_daily_path:
102
115
  try:
103
- _log_daily.write(plain_line)
104
- _log_daily.flush()
116
+ with open(_log_daily_path, "a", encoding="utf-8") as f:
117
+ f.write(plain_line)
105
118
  except Exception:
106
119
  pass
107
120
 
108
121
 
109
- def _close_log_files():
110
- """Flush and close log files on exit."""
111
- global _log_latest, _log_daily
112
- for f in (_log_latest, _log_daily):
113
- if f:
114
- try:
115
- f.flush()
116
- f.close()
117
- except Exception:
118
- pass
119
- _log_latest = None
120
- _log_daily = None
122
+ def _write_crash(exc_type, exc_value, exc_tb,
123
+ thread_name=None, severity="critical", handled=False):
124
+ """Write crash record to crashes.jsonl + daily archive."""
125
+ record = {
126
+ "timestamp": datetime.now(timezone.utc).isoformat(),
127
+ "module": "launcher",
128
+ "thread": thread_name or threading.current_thread().name,
129
+ "exception_type": exc_type.__name__ if exc_type else "Unknown",
130
+ "exception_message": str(exc_value),
131
+ "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
132
+ "severity": severity,
133
+ "handled": handled,
134
+ "process_id": os.getpid(),
135
+ "platform": sys.platform,
136
+ "runtime_version": f"Python {sys.version.split()[0]}",
137
+ }
138
+
139
+ if exc_tb:
140
+ tb_entries = traceback.extract_tb(exc_tb)
141
+ if tb_entries:
142
+ last = tb_entries[-1]
143
+ record["context"] = {
144
+ "function": last.name,
145
+ "file": os.path.basename(last.filename),
146
+ "line": last.lineno,
147
+ }
148
+
149
+ line = json.dumps(record, ensure_ascii=False) + "\n"
150
+
151
+ # 1. Write to crashes.jsonl (current run)
152
+ if _crash_log_path:
153
+ try:
154
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
155
+ f.write(line)
156
+ except Exception:
157
+ pass
158
+
159
+ # 2. Write to daily archive
160
+ if _log_dir:
161
+ try:
162
+ today = datetime.now().strftime("%Y-%m-%d")
163
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
164
+ os.makedirs(archive_dir, exist_ok=True)
165
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
166
+ with open(archive_path, "a", encoding="utf-8") as f:
167
+ f.write(line)
168
+ except Exception:
169
+ pass
170
+
171
+
172
+ def _print_crash_summary(exc_type, exc_tb, thread_name=None):
173
+ """Print crash summary to console (red highlight)."""
174
+ if exc_tb:
175
+ tb_entries = traceback.extract_tb(exc_tb)
176
+ if tb_entries:
177
+ last = tb_entries[-1]
178
+ location = f"{os.path.basename(last.filename)}:{last.lineno}"
179
+ else:
180
+ location = "unknown"
181
+ else:
182
+ location = "unknown"
183
+
184
+ prefix = "[launcher]"
185
+ if thread_name:
186
+ print(f"{prefix} {_RED}线程 {thread_name} 崩溃: "
187
+ f"{exc_type.__name__} in {location}{_RESET}")
188
+ else:
189
+ print(f"{prefix} {_RED}崩溃: {exc_type.__name__} in {location}{_RESET}")
190
+ if _crash_log_path:
191
+ print(f"{prefix} 崩溃日志: {_crash_log_path}")
192
+
193
+
194
+ def _setup_exception_hooks():
195
+ """Set up global exception hooks for launcher process."""
196
+ _orig_excepthook = sys.excepthook
197
+
198
+ def _excepthook(exc_type, exc_value, exc_tb):
199
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
200
+ _print_crash_summary(exc_type, exc_tb)
201
+ _orig_excepthook(exc_type, exc_value, exc_tb)
202
+
203
+ sys.excepthook = _excepthook
204
+
205
+ if hasattr(threading, "excepthook"):
206
+ def _thread_excepthook(args):
207
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
208
+ thread_name=args.thread.name if args.thread else "unknown",
209
+ severity="error", handled=False)
210
+ _print_crash_summary(args.exc_type, args.exc_traceback,
211
+ thread_name=args.thread.name if args.thread else None)
212
+
213
+ threading.excepthook = _thread_excepthook
121
214
 
122
- atexit.register(_close_log_files)
123
215
 
124
216
 
125
217
  def _tprint(*args, **kwargs):
126
- global _last_ts
218
+ global _last_ts, _first_line
127
219
  now = time.monotonic()
128
- delta = now - _last_ts
129
- _last_ts = now
220
+ elapsed = now - _start_ts
130
221
 
131
222
  # Timestamp
132
223
  ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
133
224
 
225
+ # Format elapsed string (time since startup)
226
+ if elapsed < 1:
227
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
228
+ elif elapsed < 100:
229
+ elapsed_str = f"{elapsed:.1f}s"
230
+ else:
231
+ elapsed_str = f"{elapsed:.0f}s"
232
+
233
+ # Extract module prefix from text for per-module delta tracking
234
+ text_for_module = " ".join(str(a) for a in args) if args else ""
235
+ plain_text = _ANSI_RE.sub("", text_for_module).strip()
236
+ m = _MODULE_PREFIX_RE.match(plain_text)
237
+ module_name = m.group(1) if m else None
238
+
239
+ if module_name:
240
+ # Per-module delta: interval since this module's last print
241
+ if module_name in _module_last_ts:
242
+ delta = now - _module_last_ts[module_name]
243
+ else:
244
+ delta = None # first line for this module — no delta
245
+ _module_last_ts[module_name] = now
246
+ else:
247
+ # Fallback: global last-line delta
248
+ delta = now - _last_ts
249
+ _last_ts = now
250
+
134
251
  # Format delta string
135
- if delta < 1:
252
+ if delta is None:
253
+ delta_str = ""
254
+ delta_color = _DIM
255
+ elif delta < 1:
136
256
  delta_str = f"+{delta * 1000:.0f}ms"
137
257
  elif delta < 100:
138
258
  delta_str = f"+{delta:.1f}s"
139
259
  else:
140
260
  delta_str = f"+{delta:.0f}s"
141
261
 
142
- # Color: gray < 1s, green 1–5s, red > 5s
143
- if delta >= 5:
144
- color = _RED
145
- elif delta >= 1:
146
- color = _GREEN
262
+ # Color for delta: gray < 1s, green 1–5s, red > 5s
263
+ if delta is not None:
264
+ if delta >= 5:
265
+ delta_color = _RED
266
+ elif delta >= 1:
267
+ delta_color = _GREEN
268
+ else:
269
+ delta_color = _DIM
270
+
271
+ # First line: entire line in orange
272
+ if _first_line:
273
+ _first_line = False
274
+ prefix = f"{_ORANGE}[{elapsed_str:>6}] {ts} {delta_str:>8} "
275
+ _builtin_print(prefix, end="")
276
+ _builtin_print(*args, end="", **{k: v for k, v in kwargs.items() if k != "end"})
277
+ _builtin_print(_RESET, end=kwargs.get("end", "\n"))
147
278
  else:
148
- color = _DIM
149
-
150
- prefix = f"{ts} {color}{delta_str:>8}{_RESET} "
151
- _builtin_print(prefix, end="")
152
- _builtin_print(*args, **kwargs)
279
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_color}{delta_str:>8}{_RESET} "
280
+ _builtin_print(prefix, end="")
281
+ _builtin_print(*args, **kwargs)
153
282
 
154
283
  # Write to log files (plain text, no ANSI)
155
- if _log_latest or _log_daily:
284
+ if _log_latest_path or _log_daily_path:
156
285
  sep = kwargs.get("sep", " ")
157
286
  end = kwargs.get("end", "\n")
158
287
  text = sep.join(str(a) for a in args)
159
- plain_prefix = f"{ts} {delta_str:>8} "
160
- _write_log(plain_prefix + text + end)
288
+ plain_prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
289
+ _write_log(plain_prefix + _strip_ansi(text) + end)
161
290
 
162
291
  builtins.print = _tprint
163
292
 
@@ -197,20 +326,31 @@ if "--debug" in sys.argv:
197
326
  # Ensure project root is on sys.path
198
327
  sys.path.insert(0, os.environ["KITE_PROJECT"])
199
328
 
329
+ _builtin_print("[launcher] 正在加载模块...")
200
330
  from core.launcher.entry import Launcher
201
331
 
202
332
 
203
333
  def main():
334
+ # Reset timing baseline to exclude import overhead
335
+ global _start_ts, _last_ts
336
+ _start_ts = time.monotonic()
337
+ _last_ts = time.monotonic()
338
+
204
339
  token = secrets.token_hex(32)
205
- print(f"[main] KITE_TOKEN 已生成 ({len(token)} 字符)")
206
- env = os.environ.get("KITE_ENV", "development")
207
- debug = os.environ.get("KITE_DEBUG") == "1"
208
- print(f"[main] 环境={env}, 调试={debug}, 项目={os.environ['KITE_PROJECT']}")
209
340
  launcher = Launcher(kite_token=token)
210
- # KITE_MODULE_DATA is now set — initialize log files
341
+ # KITE_MODULE_DATA is now set by constructor — initialize log files
211
342
  _init_log_files()
212
- print(f"[main] 日志文件已初始化: {os.environ.get('KITE_MODULE_DATA', '')}/log/")
213
- launcher.run()
343
+ _setup_exception_hooks()
344
+ # First lines in both console AND log file
345
+ print("[launcher] Kite 启动中...")
346
+ log_dir = os.path.join(os.environ.get("KITE_MODULE_DATA", ""), "log")
347
+ print(f"[launcher] 日志: {log_dir}")
348
+ try:
349
+ launcher.run()
350
+ except Exception as e:
351
+ _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
352
+ _print_crash_summary(type(e), e.__traceback__)
353
+ sys.exit(1)
214
354
 
215
355
 
216
356
  if __name__ == "__main__":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentunion/kite",
3
- "version": "1.0.7",
3
+ "version": "1.2.0",
4
4
  "description": "Kite framework launcher — start Kite from anywhere",
5
5
  "bin": {
6
6
  "kite": "./cli.js"