@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.
- package/cli.js +127 -25
- package/core/event_hub/entry.py +384 -61
- package/core/event_hub/hub.py +8 -0
- package/core/event_hub/module.md +0 -1
- package/core/event_hub/server.py +169 -38
- package/core/kite_log.py +241 -0
- package/core/launcher/entry.py +1306 -425
- package/core/launcher/module_scanner.py +10 -9
- package/core/launcher/process_manager.py +555 -121
- package/core/registry/entry.py +335 -30
- package/core/registry/server.py +339 -256
- package/core/registry/store.py +13 -2
- package/extensions/agents/__init__.py +1 -0
- package/extensions/agents/assistant/__init__.py +1 -0
- package/extensions/agents/assistant/entry.py +380 -0
- package/extensions/agents/assistant/module.md +22 -0
- package/extensions/agents/assistant/server.py +236 -0
- package/extensions/channels/__init__.py +1 -0
- package/extensions/channels/acp_channel/__init__.py +1 -0
- package/extensions/channels/acp_channel/entry.py +380 -0
- package/extensions/channels/acp_channel/module.md +22 -0
- package/extensions/channels/acp_channel/server.py +236 -0
- package/{core → extensions}/event_hub_bench/entry.py +664 -371
- package/{core → extensions}/event_hub_bench/module.md +4 -2
- package/extensions/services/backup/__init__.py +1 -0
- package/extensions/services/backup/entry.py +380 -0
- package/extensions/services/backup/module.md +22 -0
- package/extensions/services/backup/server.py +244 -0
- package/extensions/services/model_service/__init__.py +1 -0
- package/extensions/services/model_service/entry.py +380 -0
- package/extensions/services/model_service/module.md +22 -0
- package/extensions/services/model_service/server.py +236 -0
- package/extensions/services/watchdog/entry.py +460 -143
- package/extensions/services/watchdog/module.md +3 -0
- package/extensions/services/watchdog/monitor.py +128 -13
- package/extensions/services/watchdog/server.py +75 -13
- package/extensions/services/web/__init__.py +1 -0
- package/extensions/services/web/config.yaml +149 -0
- package/extensions/services/web/entry.py +487 -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 +332 -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/__init__.py +0 -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/__init__.py +0 -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/__init__.py +0 -0
- package/extensions/services/web/vendor/tools/registry.py +321 -0
- package/main.py +344 -4
- package/package.json +11 -2
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/data_dir.cpython-313.pyc +0 -0
- package/core/data_dir.py +0 -62
- package/core/event_hub/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/bench.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/bench_perf.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/dedup.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/hub.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/router.cpython-313.pyc +0 -0
- package/core/event_hub/__pycache__/server.cpython-313.pyc +0 -0
- package/core/event_hub/bench_results/2026-02-28_13-26-48.json +0 -51
- package/core/event_hub/bench_results/2026-02-28_13-44-45.json +0 -51
- package/core/event_hub/bench_results/2026-02-28_13-45-39.json +0 -51
- package/core/launcher/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/module_scanner.cpython-313.pyc +0 -0
- package/core/launcher/__pycache__/process_manager.cpython-313.pyc +0 -0
- package/core/launcher/data/log/lifecycle.jsonl +0 -1158
- package/core/launcher/data/token.txt +0 -1
- package/core/registry/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/entry.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/server.cpython-313.pyc +0 -0
- package/core/registry/__pycache__/store.cpython-313.pyc +0 -0
- package/core/registry/data/port.txt +0 -1
- package/core/registry/data/port_484.txt +0 -1
- package/extensions/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/__init__.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/entry.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/monitor.cpython-313.pyc +0 -0
- package/extensions/services/watchdog/__pycache__/server.cpython-313.pyc +0 -0
- /package/{core/event_hub/bench_results/.gitkeep → extensions/services/web/vendor/bluetooth/__init__.py} +0 -0
package/core/event_hub/server.py
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
Event Hub HTTP + WebSocket server.
|
|
3
3
|
FastAPI app: /ws (WebSocket), /health, /stats.
|
|
4
4
|
30s timer for heartbeat renewal + dedup cleanup.
|
|
5
|
+
|
|
6
|
+
Launcher bootstrap sequence:
|
|
7
|
+
Launcher connects with launcher_ws_token → Event Hub verifies locally →
|
|
8
|
+
registers to Registry → sends module.ready.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import uuid
|
|
15
|
+
from datetime import datetime, timezone
|
|
8
16
|
|
|
9
17
|
import httpx
|
|
10
18
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
@@ -14,35 +22,159 @@ from .hub import EventHub
|
|
|
14
22
|
|
|
15
23
|
class EventHubServer:
|
|
16
24
|
|
|
17
|
-
def __init__(self, hub: EventHub, own_token: str, registry_url: str,
|
|
25
|
+
def __init__(self, hub: EventHub, own_token: str, registry_url: str,
|
|
26
|
+
launcher_ws_token: str = "",
|
|
27
|
+
advertise_ip: str = "127.0.0.1", port: int = 0):
|
|
18
28
|
self.hub = hub
|
|
19
29
|
self.own_token = own_token
|
|
20
30
|
self.registry_url = registry_url
|
|
21
|
-
self.
|
|
31
|
+
self.is_debug = os.environ.get("KITE_DEBUG") == "1"
|
|
32
|
+
self.launcher_ws_token = launcher_ws_token
|
|
33
|
+
self.advertise_ip = advertise_ip
|
|
34
|
+
self.port = port
|
|
22
35
|
self._timer_task: asyncio.Task | None = None
|
|
36
|
+
self._launcher_connected = False
|
|
37
|
+
self._registered_to_registry = False
|
|
38
|
+
self._uvicorn_server = None # set by entry.py for graceful shutdown
|
|
39
|
+
self._launcher_ws: WebSocket | None = None # reference to Launcher's WS
|
|
40
|
+
self._http_client: httpx.AsyncClient | None = None # reused for Registry calls
|
|
23
41
|
self.app = self._create_app()
|
|
42
|
+
# Register shutdown callback on hub
|
|
43
|
+
self.hub._shutdown_cb = self._handle_shutdown
|
|
24
44
|
|
|
25
|
-
# ── Token verification
|
|
45
|
+
# ── Token verification ──
|
|
26
46
|
|
|
27
47
|
async def _verify_token(self, token: str, module_id_hint: str = "") -> str | None:
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
"""Verify a token. Launcher's launcher_ws_token is verified locally.
|
|
49
|
+
Other tokens are verified via Registry POST /verify.
|
|
50
|
+
In debug mode (KITE_DEBUG=1), any non-empty token is accepted."""
|
|
51
|
+
if self.is_debug and token:
|
|
52
|
+
return module_id_hint or "debug"
|
|
53
|
+
# Local verification for Launcher bootstrap (before Registry registration)
|
|
54
|
+
if self.launcher_ws_token and token == self.launcher_ws_token:
|
|
55
|
+
return "launcher"
|
|
56
|
+
# Normal verification via Registry
|
|
57
|
+
import time as _time
|
|
58
|
+
_t0 = _time.monotonic()
|
|
59
|
+
try:
|
|
60
|
+
resp = await self._http_client.post(
|
|
61
|
+
f"{self.registry_url}/verify",
|
|
62
|
+
json={"token": token},
|
|
63
|
+
headers={"Authorization": f"Bearer {self.own_token}"},
|
|
64
|
+
timeout=5,
|
|
65
|
+
)
|
|
66
|
+
_t1 = _time.monotonic()
|
|
67
|
+
if resp.status_code == 200:
|
|
68
|
+
body = resp.json()
|
|
69
|
+
if body.get("ok"):
|
|
70
|
+
mid = body.get("module_id")
|
|
71
|
+
print(f"[event_hub] _verify_token({mid}): {(_t1-_t0)*1000:.0f}ms")
|
|
72
|
+
return mid
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"[event_hub] Token verification failed: {e}")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# ── Launcher bootstrap ──
|
|
78
|
+
|
|
79
|
+
async def _on_launcher_connected(self, ws: WebSocket):
|
|
80
|
+
"""Called on first Launcher WS connect. Registers to Registry first, then sends module.ready.
|
|
81
|
+
This order ensures all startup-phase prints are done before Launcher calls close_stdio."""
|
|
82
|
+
self._launcher_connected = True
|
|
83
|
+
self._launcher_ws = ws
|
|
84
|
+
# Step 1: Register to Registry (may print log lines)
|
|
85
|
+
await self._register_to_registry()
|
|
86
|
+
# Step 2: Send module.ready (Launcher will close_stdio after receiving this)
|
|
87
|
+
msg = {
|
|
88
|
+
"type": "event",
|
|
89
|
+
"event_id": str(uuid.uuid4()),
|
|
90
|
+
"event": "module.ready",
|
|
91
|
+
"source": "event_hub",
|
|
92
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
93
|
+
"data": {
|
|
94
|
+
"module_id": "event_hub",
|
|
95
|
+
"ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
|
|
96
|
+
"graceful_shutdown": True,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
try:
|
|
100
|
+
await ws.send_text(json.dumps(msg))
|
|
101
|
+
print("[event_hub] Sent module.ready to Launcher")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"[event_hub] Failed to send module.ready: {e}")
|
|
104
|
+
|
|
105
|
+
async def _handle_shutdown(self, data: dict):
|
|
106
|
+
"""Handle module.shutdown event targeting event_hub."""
|
|
107
|
+
print("[event_hub] Received shutdown request")
|
|
108
|
+
# Send ack and ready via Launcher's WS connection
|
|
109
|
+
ws = self._launcher_ws
|
|
110
|
+
if ws:
|
|
111
|
+
try:
|
|
112
|
+
ack_msg = {
|
|
113
|
+
"type": "event",
|
|
114
|
+
"event_id": str(uuid.uuid4()),
|
|
115
|
+
"event": "module.shutdown.ack",
|
|
116
|
+
"source": "event_hub",
|
|
117
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
118
|
+
"data": {"module_id": "event_hub", "estimated_cleanup": 2},
|
|
119
|
+
}
|
|
120
|
+
await ws.send_text(json.dumps(ack_msg))
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"[event_hub] Failed to send shutdown ack: {e}")
|
|
123
|
+
# Cleanup
|
|
124
|
+
if self._timer_task:
|
|
125
|
+
self._timer_task.cancel()
|
|
126
|
+
if self._http_client:
|
|
127
|
+
await self._http_client.aclose()
|
|
128
|
+
# Send ready
|
|
129
|
+
if ws:
|
|
130
|
+
try:
|
|
131
|
+
ready_msg = {
|
|
132
|
+
"type": "event",
|
|
133
|
+
"event_id": str(uuid.uuid4()),
|
|
134
|
+
"event": "module.shutdown.ready",
|
|
135
|
+
"source": "event_hub",
|
|
136
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
137
|
+
"data": {"module_id": "event_hub"},
|
|
138
|
+
}
|
|
139
|
+
await ws.send_text(json.dumps(ready_msg))
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"[event_hub] Failed to send shutdown ready: {e}")
|
|
142
|
+
print("[event_hub] Shutdown ready, exiting")
|
|
143
|
+
# Trigger uvicorn exit
|
|
144
|
+
if self._uvicorn_server:
|
|
145
|
+
self._uvicorn_server.should_exit = True
|
|
146
|
+
|
|
147
|
+
async def _register_to_registry(self):
|
|
148
|
+
"""Register to Registry. Triggered after Launcher connects."""
|
|
149
|
+
if self._registered_to_registry:
|
|
150
|
+
return
|
|
151
|
+
payload = {
|
|
152
|
+
"action": "register",
|
|
153
|
+
"module_id": "event_hub",
|
|
154
|
+
"module_type": "infrastructure",
|
|
155
|
+
"name": "Event Hub",
|
|
156
|
+
"api_endpoint": f"http://{self.advertise_ip}:{self.port}",
|
|
157
|
+
"health_endpoint": "/health",
|
|
158
|
+
"metadata": {
|
|
159
|
+
"ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
headers = {"Authorization": f"Bearer {self.own_token}"}
|
|
31
163
|
try:
|
|
32
164
|
async with httpx.AsyncClient() as client:
|
|
33
165
|
resp = await client.post(
|
|
34
|
-
f"{self.registry_url}/
|
|
35
|
-
json=
|
|
36
|
-
headers=
|
|
166
|
+
f"{self.registry_url}/modules",
|
|
167
|
+
json=payload,
|
|
168
|
+
headers=headers,
|
|
37
169
|
timeout=5,
|
|
38
170
|
)
|
|
39
171
|
if resp.status_code == 200:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
172
|
+
self._registered_to_registry = True
|
|
173
|
+
print("[event_hub] Registered to Registry")
|
|
174
|
+
else:
|
|
175
|
+
print(f"[event_hub] WARNING: Registry returned {resp.status_code}")
|
|
43
176
|
except Exception as e:
|
|
44
|
-
print(f"[event_hub]
|
|
45
|
-
return None
|
|
177
|
+
print(f"[event_hub] WARNING: Failed to register to Registry: {e}")
|
|
46
178
|
|
|
47
179
|
# ── App factory ──
|
|
48
180
|
|
|
@@ -52,15 +184,14 @@ class EventHubServer:
|
|
|
52
184
|
|
|
53
185
|
@app.on_event("startup")
|
|
54
186
|
async def _startup():
|
|
187
|
+
# Initialize HTTP client early to avoid blocking first token verification
|
|
188
|
+
server._http_client = httpx.AsyncClient()
|
|
55
189
|
server._timer_task = asyncio.create_task(server._timer_loop())
|
|
56
|
-
server._test_task = asyncio.create_task(server._test_event_loop())
|
|
57
190
|
|
|
58
191
|
@app.on_event("shutdown")
|
|
59
192
|
async def _shutdown():
|
|
60
193
|
if server._timer_task:
|
|
61
194
|
server._timer_task.cancel()
|
|
62
|
-
if hasattr(server, '_test_task') and server._test_task:
|
|
63
|
-
server._test_task.cancel()
|
|
64
195
|
|
|
65
196
|
# ── WebSocket endpoint ──
|
|
66
197
|
|
|
@@ -70,12 +201,26 @@ class EventHubServer:
|
|
|
70
201
|
mid_hint = ws.query_params.get("id", "")
|
|
71
202
|
module_id = await server._verify_token(token, mid_hint)
|
|
72
203
|
if not module_id:
|
|
73
|
-
|
|
204
|
+
# Must accept before close — Starlette drops TCP without close frame otherwise,
|
|
205
|
+
# causing websockets 15.x clients to get "no close frame received or sent" errors.
|
|
206
|
+
await ws.accept()
|
|
207
|
+
print(f"[event_hub] 认证失败: token={token[:8]}... hint={mid_hint}")
|
|
208
|
+
try:
|
|
209
|
+
await ws.close(code=4001, reason="Authentication failed")
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
74
212
|
return
|
|
75
213
|
|
|
76
214
|
await ws.accept()
|
|
77
215
|
server.hub.add_connection(module_id, ws)
|
|
78
216
|
|
|
217
|
+
# Launcher bootstrap: first connection triggers module.ready + Registry registration
|
|
218
|
+
if (module_id == "launcher"
|
|
219
|
+
and not server._launcher_connected
|
|
220
|
+
and server.launcher_ws_token
|
|
221
|
+
and token == server.launcher_ws_token):
|
|
222
|
+
await server._on_launcher_connected(ws)
|
|
223
|
+
|
|
79
224
|
try:
|
|
80
225
|
while True:
|
|
81
226
|
raw = await ws.receive_text()
|
|
@@ -83,7 +228,10 @@ class EventHubServer:
|
|
|
83
228
|
except WebSocketDisconnect:
|
|
84
229
|
pass
|
|
85
230
|
except Exception as e:
|
|
86
|
-
|
|
231
|
+
# Connection-state errors are normal during shutdown
|
|
232
|
+
err = str(e).lower()
|
|
233
|
+
if "not connected" not in err and "closed" not in err:
|
|
234
|
+
print(f"[event_hub] WebSocket error for {module_id}: {e}")
|
|
87
235
|
finally:
|
|
88
236
|
server.hub.remove_connection(module_id)
|
|
89
237
|
|
|
@@ -104,10 +252,9 @@ class EventHubServer:
|
|
|
104
252
|
async def _timer_loop(self):
|
|
105
253
|
while True:
|
|
106
254
|
await asyncio.sleep(30)
|
|
107
|
-
# Dedup cleanup (offload to thread to avoid blocking event loop)
|
|
108
255
|
await asyncio.get_event_loop().run_in_executor(None, self.hub.dedup.cleanup)
|
|
109
|
-
|
|
110
|
-
|
|
256
|
+
if self._registered_to_registry:
|
|
257
|
+
await self._heartbeat()
|
|
111
258
|
|
|
112
259
|
async def _heartbeat(self):
|
|
113
260
|
try:
|
|
@@ -120,19 +267,3 @@ class EventHubServer:
|
|
|
120
267
|
)
|
|
121
268
|
except Exception:
|
|
122
269
|
pass
|
|
123
|
-
|
|
124
|
-
async def _test_event_loop(self):
|
|
125
|
-
"""Publish a test event every 5 seconds via internal routing."""
|
|
126
|
-
import uuid
|
|
127
|
-
from datetime import datetime, timezone
|
|
128
|
-
while True:
|
|
129
|
-
await asyncio.sleep(5)
|
|
130
|
-
msg = {
|
|
131
|
-
"type": "event",
|
|
132
|
-
"event_id": str(uuid.uuid4()),
|
|
133
|
-
"event": "event_hub.test",
|
|
134
|
-
"source": "event_hub",
|
|
135
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
136
|
-
"data": {"message": "heartbeat from event_hub"},
|
|
137
|
-
}
|
|
138
|
-
await self.hub._route_event("event_hub", msg)
|
package/core/kite_log.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kite module logging utilities.
|
|
3
|
+
Provides latest.log + crashes.jsonl infrastructure per the 异常处理规范.
|
|
4
|
+
|
|
5
|
+
Each module calls init_module_log() and setup_exception_hooks() at startup.
|
|
6
|
+
All paths derived from KITE_MODULE_DATA environment variable.
|
|
7
|
+
|
|
8
|
+
Note: This is a convenience library for Python modules. Node.js/binary modules
|
|
9
|
+
implement the same spec independently (paths + formats are language-agnostic).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import traceback
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Module-level state (initialized by init_module_log) ──
|
|
22
|
+
|
|
23
|
+
_module_name = ""
|
|
24
|
+
_log_dir = None
|
|
25
|
+
_log_latest_path = None
|
|
26
|
+
_log_daily_path = None
|
|
27
|
+
_log_daily_date = ""
|
|
28
|
+
_crash_log_path = None
|
|
29
|
+
_log_lock = threading.Lock()
|
|
30
|
+
|
|
31
|
+
# Strip ANSI escape sequences for plain-text log files
|
|
32
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _strip_ansi(s: str) -> str:
|
|
36
|
+
return _ANSI_RE.sub("", s)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_daily_log_path():
|
|
40
|
+
"""Resolve daily log path based on current date."""
|
|
41
|
+
global _log_daily_path, _log_daily_date
|
|
42
|
+
if not _log_dir:
|
|
43
|
+
return
|
|
44
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
45
|
+
if today == _log_daily_date and _log_daily_path:
|
|
46
|
+
return
|
|
47
|
+
month_dir = os.path.join(_log_dir, today[:7])
|
|
48
|
+
os.makedirs(month_dir, exist_ok=True)
|
|
49
|
+
_log_daily_path = os.path.join(month_dir, f"{today}.log")
|
|
50
|
+
_log_daily_date = today
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def init_module_log(module_name: str):
|
|
54
|
+
"""Initialize log files for a module. Call once at startup.
|
|
55
|
+
|
|
56
|
+
Requires KITE_MODULE_DATA environment variable to be set.
|
|
57
|
+
Creates: {KITE_MODULE_DATA}/log/latest.log (truncated)
|
|
58
|
+
{KITE_MODULE_DATA}/log/crashes.jsonl (truncated)
|
|
59
|
+
{KITE_MODULE_DATA}/log/{YYYY-MM}/ (daily archive directory)
|
|
60
|
+
"""
|
|
61
|
+
global _module_name, _log_dir, _log_latest_path, _crash_log_path
|
|
62
|
+
|
|
63
|
+
_module_name = module_name
|
|
64
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
65
|
+
if not module_data:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
_log_dir = os.path.join(module_data, "log")
|
|
69
|
+
os.makedirs(_log_dir, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
|
|
72
|
+
|
|
73
|
+
# latest.log — truncate on each startup
|
|
74
|
+
_log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
|
|
75
|
+
try:
|
|
76
|
+
with open(_log_latest_path, "w", encoding="utf-8") as f:
|
|
77
|
+
pass
|
|
78
|
+
except Exception:
|
|
79
|
+
_log_latest_path = None
|
|
80
|
+
|
|
81
|
+
# crashes.jsonl — truncate on each startup
|
|
82
|
+
_crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
|
|
83
|
+
try:
|
|
84
|
+
with open(_crash_log_path, "w", encoding="utf-8") as f:
|
|
85
|
+
pass
|
|
86
|
+
except Exception:
|
|
87
|
+
_crash_log_path = None
|
|
88
|
+
|
|
89
|
+
# Ensure daily archive directory exists
|
|
90
|
+
_resolve_daily_log_path()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_log(plain_line: str):
|
|
94
|
+
"""Write a plain-text line to both latest.log and daily log (open-write-close)."""
|
|
95
|
+
with _log_lock:
|
|
96
|
+
if _log_latest_path:
|
|
97
|
+
try:
|
|
98
|
+
with open(_log_latest_path, "a", encoding="utf-8") as f:
|
|
99
|
+
f.write(plain_line)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
_resolve_daily_log_path()
|
|
103
|
+
if _log_daily_path:
|
|
104
|
+
try:
|
|
105
|
+
with open(_log_daily_path, "a", encoding="utf-8") as f:
|
|
106
|
+
f.write(plain_line)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def write_crash(exc_type, exc_value, exc_tb,
|
|
112
|
+
thread_name=None, severity="critical", handled=False):
|
|
113
|
+
"""Write crash record to crashes.jsonl + daily crash archive."""
|
|
114
|
+
record = {
|
|
115
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
116
|
+
"module": _module_name,
|
|
117
|
+
"thread": thread_name or threading.current_thread().name,
|
|
118
|
+
"exception_type": exc_type.__name__ if exc_type else "Unknown",
|
|
119
|
+
"exception_message": str(exc_value),
|
|
120
|
+
"traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
121
|
+
"severity": severity,
|
|
122
|
+
"handled": handled,
|
|
123
|
+
"process_id": os.getpid(),
|
|
124
|
+
"platform": sys.platform,
|
|
125
|
+
"runtime_version": f"Python {sys.version.split()[0]}",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if exc_tb:
|
|
129
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
130
|
+
if tb_entries:
|
|
131
|
+
last = tb_entries[-1]
|
|
132
|
+
record["context"] = {
|
|
133
|
+
"function": last.name,
|
|
134
|
+
"file": os.path.basename(last.filename),
|
|
135
|
+
"line": last.lineno,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
line = json.dumps(record, ensure_ascii=False) + "\n"
|
|
139
|
+
|
|
140
|
+
# 1. Write to crashes.jsonl (current run)
|
|
141
|
+
if _crash_log_path:
|
|
142
|
+
try:
|
|
143
|
+
with open(_crash_log_path, "a", encoding="utf-8") as f:
|
|
144
|
+
f.write(line)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
# 2. Write to daily crash archive
|
|
149
|
+
if _log_dir:
|
|
150
|
+
try:
|
|
151
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
152
|
+
archive_dir = os.path.join(_log_dir, "crashes", today[:7])
|
|
153
|
+
os.makedirs(archive_dir, exist_ok=True)
|
|
154
|
+
archive_path = os.path.join(archive_dir, f"{today}.jsonl")
|
|
155
|
+
with open(archive_path, "a", encoding="utf-8") as f:
|
|
156
|
+
f.write(line)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def print_crash_summary(exc_type, exc_tb, thread_name=None):
|
|
162
|
+
"""Print crash summary to console (red highlight)."""
|
|
163
|
+
RED = "\033[91m"
|
|
164
|
+
RESET = "\033[0m"
|
|
165
|
+
|
|
166
|
+
if exc_tb:
|
|
167
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
168
|
+
if tb_entries:
|
|
169
|
+
last = tb_entries[-1]
|
|
170
|
+
location = f"{os.path.basename(last.filename)}:{last.lineno}"
|
|
171
|
+
else:
|
|
172
|
+
location = "unknown"
|
|
173
|
+
else:
|
|
174
|
+
location = "unknown"
|
|
175
|
+
|
|
176
|
+
prefix = f"[{_module_name}]"
|
|
177
|
+
if thread_name:
|
|
178
|
+
print(f"{prefix} {RED}线程 {thread_name} 崩溃: "
|
|
179
|
+
f"{exc_type.__name__} in {location}{RESET}")
|
|
180
|
+
else:
|
|
181
|
+
print(f"{prefix} {RED}崩溃: {exc_type.__name__} in {location}{RESET}")
|
|
182
|
+
if _crash_log_path:
|
|
183
|
+
print(f"{prefix} 崩溃日志: {_crash_log_path}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def setup_exception_hooks(module_name: str = ""):
|
|
187
|
+
"""Set up global exception hooks (sys.excepthook + threading.excepthook).
|
|
188
|
+
Call once at startup after init_module_log().
|
|
189
|
+
"""
|
|
190
|
+
if module_name:
|
|
191
|
+
global _module_name
|
|
192
|
+
_module_name = module_name
|
|
193
|
+
|
|
194
|
+
_orig_excepthook = sys.excepthook
|
|
195
|
+
|
|
196
|
+
def _excepthook(exc_type, exc_value, exc_tb):
|
|
197
|
+
write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
|
|
198
|
+
print_crash_summary(exc_type, exc_tb)
|
|
199
|
+
_orig_excepthook(exc_type, exc_value, exc_tb)
|
|
200
|
+
|
|
201
|
+
sys.excepthook = _excepthook
|
|
202
|
+
|
|
203
|
+
if hasattr(threading, "excepthook"):
|
|
204
|
+
def _thread_excepthook(args):
|
|
205
|
+
write_crash(args.exc_type, args.exc_value, args.exc_traceback,
|
|
206
|
+
thread_name=args.thread.name if args.thread else "unknown",
|
|
207
|
+
severity="error", handled=False)
|
|
208
|
+
print_crash_summary(args.exc_type, args.exc_traceback,
|
|
209
|
+
thread_name=args.thread.name if args.thread else None)
|
|
210
|
+
|
|
211
|
+
threading.excepthook = _thread_excepthook
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def install_log_hook():
|
|
215
|
+
"""Install a hook on the _SafeWriter or stdout to capture all print output
|
|
216
|
+
to latest.log + daily log. Call after init_module_log().
|
|
217
|
+
|
|
218
|
+
Wraps the current sys.stdout so that every write() also goes to log files.
|
|
219
|
+
"""
|
|
220
|
+
if not _log_latest_path:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
original_stdout = sys.stdout
|
|
224
|
+
|
|
225
|
+
class _LoggingWriter:
|
|
226
|
+
"""Wraps stdout to tee all output to log files."""
|
|
227
|
+
def __init__(self, stream):
|
|
228
|
+
self._stream = stream
|
|
229
|
+
|
|
230
|
+
def write(self, s):
|
|
231
|
+
self._stream.write(s)
|
|
232
|
+
if s and s.strip():
|
|
233
|
+
write_log(_strip_ansi(s) + ("\n" if not s.endswith("\n") else ""))
|
|
234
|
+
|
|
235
|
+
def flush(self):
|
|
236
|
+
self._stream.flush()
|
|
237
|
+
|
|
238
|
+
def __getattr__(self, name):
|
|
239
|
+
return getattr(self._stream, name)
|
|
240
|
+
|
|
241
|
+
sys.stdout = _LoggingWriter(original_stdout)
|