@agentunion/kite 1.0.7 → 1.3.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 (100) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +48 -0
  3. package/cli.js +1 -1
  4. package/extensions/agents/__init__.py +1 -0
  5. package/extensions/agents/assistant/__init__.py +1 -0
  6. package/extensions/agents/assistant/entry.py +329 -0
  7. package/extensions/agents/assistant/module.md +22 -0
  8. package/extensions/agents/assistant/server.py +197 -0
  9. package/extensions/channels/__init__.py +1 -0
  10. package/extensions/channels/acp_channel/__init__.py +1 -0
  11. package/extensions/channels/acp_channel/entry.py +329 -0
  12. package/extensions/channels/acp_channel/module.md +22 -0
  13. package/extensions/channels/acp_channel/server.py +197 -0
  14. package/extensions/event_hub_bench/entry.py +624 -379
  15. package/extensions/event_hub_bench/module.md +2 -1
  16. package/extensions/services/backup/__init__.py +1 -0
  17. package/extensions/services/backup/entry.py +508 -0
  18. package/extensions/services/backup/module.md +22 -0
  19. package/extensions/services/model_service/__init__.py +1 -0
  20. package/extensions/services/model_service/entry.py +508 -0
  21. package/extensions/services/model_service/module.md +22 -0
  22. package/extensions/services/watchdog/entry.py +468 -102
  23. package/extensions/services/watchdog/module.md +3 -0
  24. package/extensions/services/watchdog/monitor.py +170 -69
  25. package/extensions/services/web/__init__.py +1 -0
  26. package/extensions/services/web/config.yaml +149 -0
  27. package/extensions/services/web/entry.py +390 -0
  28. package/extensions/services/web/module.md +24 -0
  29. package/extensions/services/web/routes/__init__.py +1 -0
  30. package/extensions/services/web/routes/routes_call.py +189 -0
  31. package/extensions/services/web/routes/routes_config.py +512 -0
  32. package/extensions/services/web/routes/routes_contacts.py +98 -0
  33. package/extensions/services/web/routes/routes_devlog.py +99 -0
  34. package/extensions/services/web/routes/routes_phone.py +81 -0
  35. package/extensions/services/web/routes/routes_sms.py +48 -0
  36. package/extensions/services/web/routes/routes_stats.py +17 -0
  37. package/extensions/services/web/routes/routes_voicechat.py +554 -0
  38. package/extensions/services/web/routes/schemas.py +216 -0
  39. package/extensions/services/web/server.py +375 -0
  40. package/extensions/services/web/static/css/style.css +1064 -0
  41. package/extensions/services/web/static/index.html +1445 -0
  42. package/extensions/services/web/static/js/app.js +4671 -0
  43. package/extensions/services/web/vendor/__init__.py +1 -0
  44. package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
  45. package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
  46. package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
  47. package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
  48. package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
  49. package/extensions/services/web/vendor/config.py +139 -0
  50. package/extensions/services/web/vendor/conversation/asr.py +936 -0
  51. package/extensions/services/web/vendor/conversation/engine.py +548 -0
  52. package/extensions/services/web/vendor/conversation/llm.py +534 -0
  53. package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
  54. package/extensions/services/web/vendor/conversation/tts.py +322 -0
  55. package/extensions/services/web/vendor/conversation/vad.py +138 -0
  56. package/extensions/services/web/vendor/storage/__init__.py +1 -0
  57. package/extensions/services/web/vendor/storage/identity.py +312 -0
  58. package/extensions/services/web/vendor/storage/store.py +507 -0
  59. package/extensions/services/web/vendor/task/manager.py +864 -0
  60. package/extensions/services/web/vendor/task/models.py +45 -0
  61. package/extensions/services/web/vendor/task/webhook.py +263 -0
  62. package/extensions/services/web/vendor/tools/registry.py +321 -0
  63. package/kernel/__init__.py +0 -0
  64. package/kernel/entry.py +407 -0
  65. package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
  66. package/kernel/module.md +33 -0
  67. package/{core/registry/store.py → kernel/registry_store.py} +23 -8
  68. package/kernel/rpc_router.py +388 -0
  69. package/kernel/server.py +267 -0
  70. package/launcher/__init__.py +10 -0
  71. package/launcher/__main__.py +6 -0
  72. package/launcher/count_lines.py +258 -0
  73. package/launcher/entry.py +1778 -0
  74. package/launcher/logging_setup.py +289 -0
  75. package/{core/launcher → launcher}/module_scanner.py +11 -6
  76. package/launcher/process_manager.py +880 -0
  77. package/main.py +11 -210
  78. package/package.json +6 -9
  79. package/__init__.py +0 -1
  80. package/__main__.py +0 -15
  81. package/core/event_hub/BENCHMARK.md +0 -94
  82. package/core/event_hub/bench.py +0 -459
  83. package/core/event_hub/bench_extreme.py +0 -308
  84. package/core/event_hub/bench_perf.py +0 -350
  85. package/core/event_hub/entry.py +0 -157
  86. package/core/event_hub/module.md +0 -20
  87. package/core/event_hub/server.py +0 -206
  88. package/core/launcher/entry.py +0 -1158
  89. package/core/launcher/process_manager.py +0 -470
  90. package/core/registry/entry.py +0 -110
  91. package/core/registry/module.md +0 -30
  92. package/core/registry/server.py +0 -289
  93. package/extensions/services/watchdog/server.py +0 -167
  94. /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
  95. /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
  96. /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
  97. /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
  98. /package/{core/event_hub → kernel}/dedup.py +0 -0
  99. /package/{core/event_hub → kernel}/router.py +0 -0
  100. /package/{core/launcher → launcher}/module.md +0 -0
@@ -0,0 +1,197 @@
1
+ """
2
+ Assistant WebSocket client.
3
+ Connects to Kernel via WebSocket JSON-RPC 2.0 for event publishing and subscription.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import time
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+
12
+ import websockets
13
+
14
+
15
+ class AssistantServer:
16
+
17
+ def __init__(self, token: str = "", kernel_port: int = 0, boot_t0: float = 0):
18
+ self.token = token
19
+ self.kernel_port = kernel_port
20
+ self.boot_t0 = boot_t0
21
+ self._ws_task: asyncio.Task | None = None
22
+ self._test_task: asyncio.Task | None = None
23
+ self._ws: object | None = None
24
+ self._ready_sent = False
25
+ self._shutting_down = False
26
+ self._start_time = time.time()
27
+
28
+ async def run(self):
29
+ """Main entry point: start WebSocket loop and test event loop."""
30
+ if self.kernel_port:
31
+ self._ws_task = asyncio.create_task(self._ws_loop())
32
+ self._test_task = asyncio.create_task(self._test_event_loop())
33
+
34
+ # Wait for tasks to complete
35
+ tasks = [t for t in [self._ws_task, self._test_task] if t]
36
+ if tasks:
37
+ await asyncio.gather(*tasks, return_exceptions=True)
38
+
39
+ print("[assistant] Shutdown complete")
40
+
41
+ # ── Kernel WebSocket client ──
42
+
43
+ async def _ws_loop(self):
44
+ """Connect to Kernel, subscribe, register, and listen. Reconnect on failure."""
45
+ retry_delay = 0.5
46
+ max_delay = 30
47
+ while not self._shutting_down:
48
+ try:
49
+ await self._ws_connect()
50
+ except asyncio.CancelledError:
51
+ return
52
+ except Exception as e:
53
+ print(f"[assistant] Kernel connection error: {e}, retrying in {retry_delay:.1f}s")
54
+ self._ws = None
55
+ if self._shutting_down:
56
+ return
57
+ await asyncio.sleep(retry_delay)
58
+ retry_delay = min(retry_delay * 2, max_delay)
59
+
60
+ async def _ws_connect(self):
61
+ """Single WebSocket session: connect, subscribe, register, ready, receive loop."""
62
+ url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=assistant"
63
+ print(f"[assistant] Connecting to Kernel (port {self.kernel_port})")
64
+ async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
65
+ self._ws = ws
66
+ elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
67
+ elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
68
+ print(f"[assistant] Connected to Kernel{elapsed_str}")
69
+
70
+ # Step 1: Subscribe to events (先订阅)
71
+ await self._rpc_call(ws, "event.subscribe", {
72
+ "events": ["module.started", "module.stopped", "module.shutdown"],
73
+ })
74
+
75
+ # Step 2: Register to Kernel (再注册)
76
+ await self._rpc_call(ws, "registry.register", {
77
+ "module_id": "assistant",
78
+ "module_type": "agent",
79
+ "name": "Assistant",
80
+ "events_publish": {
81
+ "assistant.test": {},
82
+ },
83
+ "events_subscribe": [
84
+ "module.started",
85
+ "module.stopped",
86
+ "module.shutdown",
87
+ ],
88
+ })
89
+
90
+ # Step 3: Publish module.ready (once)
91
+ if not self._ready_sent:
92
+ await self._rpc_call(ws, "event.publish", {
93
+ "event_id": str(uuid.uuid4()),
94
+ "event": "module.ready",
95
+ "data": {
96
+ "module_id": "assistant",
97
+ "graceful_shutdown": True,
98
+ },
99
+ })
100
+ self._ready_sent = True
101
+ elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
102
+ elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
103
+ print(f"[assistant] module.ready published{elapsed_str}")
104
+
105
+ # Reset retry delay on successful connection
106
+ retry_delay = 0.5
107
+
108
+ # Receive loop
109
+ async for raw in ws:
110
+ try:
111
+ msg = json.loads(raw)
112
+ except (json.JSONDecodeError, TypeError):
113
+ continue
114
+
115
+ try:
116
+ has_method = "method" in msg
117
+ has_id = "id" in msg
118
+
119
+ if has_method and not has_id:
120
+ # JSON-RPC Notification (event delivery)
121
+ params = msg.get("params", {})
122
+ event_name = params.get("event", "")
123
+ if event_name == "module.shutdown":
124
+ data = params.get("data", {})
125
+ target = data.get("module_id", "")
126
+ if target == "assistant":
127
+ await self._handle_shutdown(ws)
128
+ return
129
+ elif not has_method and has_id:
130
+ # JSON-RPC Response (to our RPC calls)
131
+ pass
132
+ except Exception as e:
133
+ print(f"[assistant] 事件处理异常(已忽略): {e}")
134
+
135
+ async def _handle_shutdown(self, ws):
136
+ """Handle module.shutdown: ack → cleanup → ready → exit."""
137
+ print("[assistant] Received module.shutdown")
138
+ self._shutting_down = True
139
+
140
+ # Step 1: Send ack
141
+ await self._rpc_call(ws, "event.publish", {
142
+ "event_id": str(uuid.uuid4()),
143
+ "event": "module.shutdown.ack",
144
+ "data": {"module_id": "assistant", "estimated_cleanup": 2},
145
+ })
146
+ print("[assistant] shutdown ack sent")
147
+
148
+ # Step 2: Cleanup (cancel background tasks)
149
+ if self._test_task:
150
+ self._test_task.cancel()
151
+
152
+ # Step 3: Send ready (before closing WS!)
153
+ await self._rpc_call(ws, "event.publish", {
154
+ "event_id": str(uuid.uuid4()),
155
+ "event": "module.shutdown.ready",
156
+ "data": {"module_id": "assistant"},
157
+ })
158
+ print("[assistant] Shutdown ready sent")
159
+
160
+ # Step 4: Exit process
161
+ import sys
162
+ sys.exit(0)
163
+
164
+ async def _rpc_call(self, ws, method: str, params: dict = None):
165
+ """Send a JSON-RPC 2.0 request."""
166
+ msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
167
+ if params:
168
+ msg["params"] = params
169
+ await ws.send(json.dumps(msg))
170
+
171
+ async def _publish_event(self, event: dict):
172
+ """Publish an event via JSON-RPC event.publish."""
173
+ if not self._ws:
174
+ return
175
+ try:
176
+ await self._rpc_call(self._ws, "event.publish", {
177
+ "event_id": str(uuid.uuid4()),
178
+ "event": event.get("event", ""),
179
+ "data": event.get("data", {}),
180
+ })
181
+ except Exception as e:
182
+ print(f"[assistant] Failed to publish event: {e}")
183
+
184
+ # ── Test event loop ──
185
+
186
+ async def _test_event_loop(self):
187
+ """Publish a test event every 10 seconds."""
188
+ while True:
189
+ await asyncio.sleep(10)
190
+ await self._publish_event({
191
+ "event": "assistant.test",
192
+ "data": {
193
+ "message": "test event from assistant",
194
+ "timestamp": datetime.now(timezone.utc).isoformat(),
195
+ },
196
+ })
197
+ print("[assistant] test event published")
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,329 @@
1
+ """
2
+ ACP Channel entry point.
3
+ Reads boot_info from stdin, connects to Kernel via WebSocket JSON-RPC 2.0.
4
+ """
5
+
6
+ import asyncio
7
+ import builtins
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ import threading
13
+ import time
14
+ import traceback
15
+ from datetime import datetime, timezone
16
+
17
+
18
+ # ── Safe stdout/stderr: ignore BrokenPipeError after Launcher closes stdio ──
19
+
20
+
21
+ # ── Module configuration ──
22
+ MODULE_NAME = "acp_channel"
23
+
24
+
25
+ class _SafeWriter:
26
+ """Wraps a stream to silently swallow BrokenPipeError on write/flush."""
27
+ def __init__(self, stream):
28
+ self._stream = stream
29
+
30
+ def write(self, s):
31
+ try:
32
+ self._stream.write(s)
33
+ except (BrokenPipeError, OSError):
34
+ pass
35
+
36
+ def flush(self):
37
+ try:
38
+ self._stream.flush()
39
+ except (BrokenPipeError, OSError):
40
+ pass
41
+
42
+ def __getattr__(self, name):
43
+ return getattr(self._stream, name)
44
+
45
+ sys.stdout = _SafeWriter(sys.stdout)
46
+ sys.stderr = _SafeWriter(sys.stderr)
47
+
48
+
49
+ # ── Timestamped print + log file writer ──
50
+ # Independent implementation per module (no shared code dependency)
51
+
52
+ _builtin_print = builtins.print
53
+ _start_ts = time.monotonic()
54
+ _last_ts = time.monotonic()
55
+ _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
56
+ _log_lock = threading.Lock()
57
+ _log_latest_path = None
58
+ _log_daily_path = None
59
+ _log_daily_date = ""
60
+ _log_dir = None
61
+ _crash_log_path = None
62
+
63
+ def _strip_ansi(s: str) -> str:
64
+ return _ANSI_RE.sub("", s)
65
+
66
+ def _resolve_daily_log_path():
67
+ """Resolve daily log path based on current date."""
68
+ global _log_daily_path, _log_daily_date
69
+ if not _log_dir:
70
+ return
71
+ today = datetime.now().strftime("%Y-%m-%d")
72
+ if today == _log_daily_date and _log_daily_path:
73
+ return
74
+ month_dir = os.path.join(_log_dir, today[:7])
75
+ os.makedirs(month_dir, exist_ok=True)
76
+ _log_daily_path = os.path.join(month_dir, f"{today}.log")
77
+ _log_daily_date = today
78
+
79
+ def _write_log(plain_line: str):
80
+ """Write a plain-text line to both latest.log and daily log."""
81
+ with _log_lock:
82
+ if _log_latest_path:
83
+ try:
84
+ with open(_log_latest_path, "a", encoding="utf-8") as f:
85
+ f.write(plain_line)
86
+ except Exception:
87
+ pass
88
+ _resolve_daily_log_path()
89
+ if _log_daily_path:
90
+ try:
91
+ with open(_log_daily_path, "a", encoding="utf-8") as f:
92
+ f.write(plain_line)
93
+ except Exception:
94
+ pass
95
+
96
+ def _write_crash(exc_type, exc_value, exc_tb, thread_name=None, severity="critical", handled=False):
97
+ """Write crash record to crashes.jsonl + daily crash archive."""
98
+ record = {
99
+ "timestamp": datetime.now(timezone.utc).isoformat(),
100
+ "module": MODULE_NAME,
101
+ "thread": thread_name or threading.current_thread().name,
102
+ "exception_type": exc_type.__name__ if exc_type else "Unknown",
103
+ "exception_message": str(exc_value),
104
+ "traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
105
+ "severity": severity,
106
+ "handled": handled,
107
+ "process_id": os.getpid(),
108
+ "platform": sys.platform,
109
+ "runtime_version": f"Python {sys.version.split()[0]}",
110
+ }
111
+
112
+ if exc_tb:
113
+ tb_entries = traceback.extract_tb(exc_tb)
114
+ if tb_entries:
115
+ last = tb_entries[-1]
116
+ record["context"] = {
117
+ "function": last.name,
118
+ "file": os.path.basename(last.filename),
119
+ "line": last.lineno,
120
+ }
121
+
122
+ line = json.dumps(record, ensure_ascii=False) + "\n"
123
+
124
+ if _crash_log_path:
125
+ try:
126
+ with open(_crash_log_path, "a", encoding="utf-8") as f:
127
+ f.write(line)
128
+ except Exception:
129
+ pass
130
+
131
+ if _log_dir:
132
+ try:
133
+ today = datetime.now().strftime("%Y-%m-%d")
134
+ archive_dir = os.path.join(_log_dir, "crashes", today[:7])
135
+ os.makedirs(archive_dir, exist_ok=True)
136
+ archive_path = os.path.join(archive_dir, f"{today}.jsonl")
137
+ with open(archive_path, "a", encoding="utf-8") as f:
138
+ f.write(line)
139
+ except Exception:
140
+ pass
141
+
142
+ def _print_crash_summary(exc_type, exc_tb, thread_name=None):
143
+ """Print crash summary to console (red highlight)."""
144
+ RED = "\033[91m"
145
+ RESET = "\033[0m"
146
+
147
+ if exc_tb:
148
+ tb_entries = traceback.extract_tb(exc_tb)
149
+ if tb_entries:
150
+ last = tb_entries[-1]
151
+ location = f"{os.path.basename(last.filename)}:{last.lineno}"
152
+ else:
153
+ location = "unknown"
154
+ else:
155
+ location = "unknown"
156
+
157
+ prefix = f"[{MODULE_NAME}]"
158
+ if thread_name:
159
+ _builtin_print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
160
+ f"{exc_type.__name__} in {location}{RESET}")
161
+ else:
162
+ _builtin_print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
163
+ if _crash_log_path:
164
+ _builtin_print(f"{prefix} 崩溃日志: {_crash_log_path}")
165
+
166
+ def _setup_exception_hooks():
167
+ """Set up global exception hooks."""
168
+ _orig_excepthook = sys.excepthook
169
+
170
+ def _excepthook(exc_type, exc_value, exc_tb):
171
+ _write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
172
+ _print_crash_summary(exc_type, exc_tb)
173
+ _orig_excepthook(exc_type, exc_value, exc_tb)
174
+
175
+ sys.excepthook = _excepthook
176
+
177
+ if hasattr(threading, "excepthook"):
178
+ def _thread_excepthook(args):
179
+ _write_crash(args.exc_type, args.exc_value, args.exc_traceback,
180
+ thread_name=args.thread.name if args.thread else "unknown",
181
+ severity="error", handled=False)
182
+ _print_crash_summary(args.exc_type, args.exc_traceback,
183
+ thread_name=args.thread.name if args.thread else None)
184
+
185
+ threading.excepthook = _thread_excepthook
186
+
187
+ def _tprint(*args, **kwargs):
188
+ """Timestamped print that adds [timestamp] HH:MM:SS.mmm +delta prefix."""
189
+ global _last_ts
190
+ now = time.monotonic()
191
+ elapsed = now - _start_ts
192
+ delta = now - _last_ts
193
+ _last_ts = now
194
+
195
+ if elapsed < 1:
196
+ elapsed_str = f"{elapsed * 1000:.0f}ms"
197
+ elif elapsed < 100:
198
+ elapsed_str = f"{elapsed:.1f}s"
199
+ else:
200
+ elapsed_str = f"{elapsed:.0f}s"
201
+
202
+ if delta < 0.001:
203
+ delta_str = ""
204
+ elif delta < 1:
205
+ delta_str = f"+{delta * 1000:.0f}ms"
206
+ elif delta < 100:
207
+ delta_str = f"+{delta:.1f}s"
208
+ else:
209
+ delta_str = f"+{delta:.0f}s"
210
+
211
+ ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
212
+
213
+ _builtin_print(*args, **kwargs)
214
+
215
+ if _log_latest_path or _log_daily_path:
216
+ sep = kwargs.get("sep", " ")
217
+ end = kwargs.get("end", "\n")
218
+ text = sep.join(str(a) for a in args)
219
+ prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
220
+ _write_log(prefix + _strip_ansi(text) + end)
221
+
222
+ builtins.print = _tprint
223
+
224
+ # Ensure project root is on sys.path (set by main.py or cli.js)
225
+ _project_root = os.environ.get("KITE_PROJECT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
226
+ if _project_root not in sys.path:
227
+ sys.path.insert(0, _project_root)
228
+
229
+ from extensions.channels.acp_channel.server import AcpChannelServer
230
+
231
+
232
+ def _fmt_elapsed(t0: float) -> str:
233
+ d = time.monotonic() - t0
234
+ if d < 1:
235
+ return f"{d * 1000:.0f}ms"
236
+ if d < 10:
237
+ return f"{d:.1f}s"
238
+ return f"{d:.0f}s"
239
+
240
+
241
+ def _read_stdin_kite_message(expected_type: str, timeout: float = 10) -> dict | None:
242
+ """Read a single kite message of expected type from stdin with timeout."""
243
+ result = [None]
244
+
245
+ def _read():
246
+ try:
247
+ line = sys.stdin.readline().strip()
248
+ if line:
249
+ msg = json.loads(line)
250
+ if isinstance(msg, dict) and msg.get("kite") == expected_type:
251
+ result[0] = msg
252
+ except Exception:
253
+ pass
254
+
255
+ t = threading.Thread(target=_read, daemon=True)
256
+ t.start()
257
+ t.join(timeout=timeout)
258
+ return result[0]
259
+
260
+
261
+ def main():
262
+ # Initialize log file paths
263
+ global _log_dir, _log_latest_path, _crash_log_path
264
+ module_data = os.environ.get("KITE_MODULE_DATA")
265
+ if module_data:
266
+ _log_dir = os.path.join(module_data, "log")
267
+ os.makedirs(_log_dir, exist_ok=True)
268
+ suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
269
+
270
+ _log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
271
+ try:
272
+ with open(_log_latest_path, "w", encoding="utf-8") as f:
273
+ pass
274
+ except Exception:
275
+ _log_latest_path = None
276
+
277
+ _crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
278
+ try:
279
+ with open(_crash_log_path, "w", encoding="utf-8") as f:
280
+ pass
281
+ except Exception:
282
+ _crash_log_path = None
283
+
284
+ _resolve_daily_log_path()
285
+
286
+ _setup_exception_hooks()
287
+
288
+ _t0 = time.monotonic()
289
+
290
+ # Read boot_info from stdin (only token)
291
+ token = ""
292
+ try:
293
+ line = sys.stdin.readline().strip()
294
+ if line:
295
+ boot_info = json.loads(line)
296
+ token = boot_info.get("token", "")
297
+ except Exception:
298
+ pass
299
+
300
+ # Read kernel_port: env first (fast path), stdin fallback (parallel start)
301
+ kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
302
+ if not kernel_port:
303
+ msg = _read_stdin_kite_message("kernel_port", timeout=10)
304
+ if msg:
305
+ kernel_port = int(msg.get("kernel_port", 0))
306
+
307
+ if not token or not kernel_port:
308
+ print("[acp_channel] ERROR: Missing token or kernel_port")
309
+ sys.exit(1)
310
+
311
+ print(f"[acp_channel] Token received ({len(token)} chars), kernel port: {kernel_port} ({_fmt_elapsed(_t0)})")
312
+
313
+ server = AcpChannelServer(
314
+ token=token,
315
+ kernel_port=kernel_port,
316
+ boot_t0=_t0,
317
+ )
318
+
319
+ print(f"[acp_channel] Starting ({_fmt_elapsed(_t0)})")
320
+ try:
321
+ asyncio.run(server.run())
322
+ except Exception as e:
323
+ _write_crash(type(e), e, e.__traceback__, severity="critical", handled=True)
324
+ _print_crash_summary(type(e), e.__traceback__)
325
+ sys.exit(1)
326
+
327
+
328
+ if __name__ == "__main__":
329
+ main()
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: acp_channel
3
+ display_name: ACP Channel
4
+ version: "1.0"
5
+ type: channel
6
+ state: enabled
7
+ runtime: python
8
+ entry: entry.py
9
+ events:
10
+ - acp_channel.test
11
+ subscriptions:
12
+ - module.started
13
+ - module.stopped
14
+ - module.shutdown
15
+ ---
16
+
17
+ # ACP Channel(ACP 协议通道)
18
+
19
+ ACP 协议通道模块,负责接入外部消息通道。
20
+
21
+ - 消息接入 — 通过 ACP 协议接收和发送消息
22
+ - 事件通知 — 通过 Kernel 发布通道状态事件