@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.
- package/CHANGELOG.md +208 -0
- package/README.md +48 -0
- package/cli.js +1 -1
- package/extensions/agents/__init__.py +1 -0
- package/extensions/agents/assistant/__init__.py +1 -0
- package/extensions/agents/assistant/entry.py +329 -0
- package/extensions/agents/assistant/module.md +22 -0
- package/extensions/agents/assistant/server.py +197 -0
- package/extensions/channels/__init__.py +1 -0
- package/extensions/channels/acp_channel/__init__.py +1 -0
- package/extensions/channels/acp_channel/entry.py +329 -0
- package/extensions/channels/acp_channel/module.md +22 -0
- package/extensions/channels/acp_channel/server.py +197 -0
- package/extensions/event_hub_bench/entry.py +624 -379
- package/extensions/event_hub_bench/module.md +2 -1
- package/extensions/services/backup/__init__.py +1 -0
- package/extensions/services/backup/entry.py +508 -0
- package/extensions/services/backup/module.md +22 -0
- package/extensions/services/model_service/__init__.py +1 -0
- package/extensions/services/model_service/entry.py +508 -0
- package/extensions/services/model_service/module.md +22 -0
- package/extensions/services/watchdog/entry.py +468 -102
- package/extensions/services/watchdog/module.md +3 -0
- package/extensions/services/watchdog/monitor.py +170 -69
- package/extensions/services/web/__init__.py +1 -0
- package/extensions/services/web/config.yaml +149 -0
- package/extensions/services/web/entry.py +390 -0
- package/extensions/services/web/module.md +24 -0
- package/extensions/services/web/routes/__init__.py +1 -0
- package/extensions/services/web/routes/routes_call.py +189 -0
- package/extensions/services/web/routes/routes_config.py +512 -0
- package/extensions/services/web/routes/routes_contacts.py +98 -0
- package/extensions/services/web/routes/routes_devlog.py +99 -0
- package/extensions/services/web/routes/routes_phone.py +81 -0
- package/extensions/services/web/routes/routes_sms.py +48 -0
- package/extensions/services/web/routes/routes_stats.py +17 -0
- package/extensions/services/web/routes/routes_voicechat.py +554 -0
- package/extensions/services/web/routes/schemas.py +216 -0
- package/extensions/services/web/server.py +375 -0
- package/extensions/services/web/static/css/style.css +1064 -0
- package/extensions/services/web/static/index.html +1445 -0
- package/extensions/services/web/static/js/app.js +4671 -0
- package/extensions/services/web/vendor/__init__.py +1 -0
- package/extensions/services/web/vendor/bluetooth/audio.py +348 -0
- package/extensions/services/web/vendor/bluetooth/contacts.py +251 -0
- package/extensions/services/web/vendor/bluetooth/manager.py +395 -0
- package/extensions/services/web/vendor/bluetooth/sms.py +290 -0
- package/extensions/services/web/vendor/bluetooth/telephony.py +274 -0
- package/extensions/services/web/vendor/config.py +139 -0
- package/extensions/services/web/vendor/conversation/asr.py +936 -0
- package/extensions/services/web/vendor/conversation/engine.py +548 -0
- package/extensions/services/web/vendor/conversation/llm.py +534 -0
- package/extensions/services/web/vendor/conversation/mcp_tools.py +190 -0
- package/extensions/services/web/vendor/conversation/tts.py +322 -0
- package/extensions/services/web/vendor/conversation/vad.py +138 -0
- package/extensions/services/web/vendor/storage/__init__.py +1 -0
- package/extensions/services/web/vendor/storage/identity.py +312 -0
- package/extensions/services/web/vendor/storage/store.py +507 -0
- package/extensions/services/web/vendor/task/manager.py +864 -0
- package/extensions/services/web/vendor/task/models.py +45 -0
- package/extensions/services/web/vendor/task/webhook.py +263 -0
- package/extensions/services/web/vendor/tools/registry.py +321 -0
- package/kernel/__init__.py +0 -0
- package/kernel/entry.py +407 -0
- package/{core/event_hub/hub.py → kernel/event_hub.py} +62 -74
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +23 -8
- package/kernel/rpc_router.py +388 -0
- package/kernel/server.py +267 -0
- package/launcher/__init__.py +10 -0
- package/launcher/__main__.py +6 -0
- package/launcher/count_lines.py +258 -0
- package/launcher/entry.py +1778 -0
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/launcher/process_manager.py +880 -0
- package/main.py +11 -210
- package/package.json +6 -9
- package/__init__.py +0 -1
- package/__main__.py +0 -15
- package/core/event_hub/BENCHMARK.md +0 -94
- package/core/event_hub/bench.py +0 -459
- package/core/event_hub/bench_extreme.py +0 -308
- package/core/event_hub/bench_perf.py +0 -350
- package/core/event_hub/entry.py +0 -157
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -206
- package/core/launcher/entry.py +0 -1158
- package/core/launcher/process_manager.py +0 -470
- package/core/registry/entry.py +0 -110
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -289
- package/extensions/services/watchdog/server.py +0 -167
- /package/{core → extensions/services/web/vendor/bluetooth}/__init__.py +0 -0
- /package/{core/event_hub → extensions/services/web/vendor/conversation}/__init__.py +0 -0
- /package/{core/launcher → extensions/services/web/vendor/task}/__init__.py +0 -0
- /package/{core/registry → extensions/services/web/vendor/tools}/__init__.py +0 -0
- /package/{core/event_hub → kernel}/dedup.py +0 -0
- /package/{core/event_hub → kernel}/router.py +0 -0
- /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 @@
|
|
|
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 发布通道状态事件
|