@agentunion/kite 1.2.0 → 1.3.1
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/assistant/entry.py +30 -81
- package/extensions/agents/assistant/module.md +1 -1
- package/extensions/agents/assistant/server.py +83 -122
- package/extensions/channels/acp_channel/entry.py +30 -81
- package/extensions/channels/acp_channel/module.md +1 -1
- package/extensions/channels/acp_channel/server.py +83 -122
- package/extensions/event_hub_bench/entry.py +81 -121
- package/extensions/services/backup/entry.py +213 -85
- package/extensions/services/model_service/entry.py +213 -85
- package/extensions/services/watchdog/entry.py +513 -460
- package/extensions/services/watchdog/monitor.py +55 -69
- package/extensions/services/web/entry.py +11 -108
- package/extensions/services/web/server.py +120 -77
- package/{core/registry → kernel}/entry.py +65 -37
- package/{core/event_hub/hub.py → kernel/event_hub.py} +61 -81
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +13 -4
- 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/{core/launcher → launcher}/entry.py +693 -767
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/main.py +11 -350
- 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/__init__.py +0 -0
- 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 -436
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -269
- package/core/kite_log.py +0 -241
- package/core/launcher/__init__.py +0 -0
- package/core/registry/__init__.py +0 -0
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -339
- package/extensions/services/backup/server.py +0 -244
- package/extensions/services/model_service/server.py +0 -236
- package/extensions/services/watchdog/server.py +0 -229
- /package/{core → kernel}/__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
- /package/{core/launcher → launcher}/process_manager.py +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Launcher logging setup: timestamped print, log files, crash logging, exception hooks.
|
|
3
|
+
"""
|
|
4
|
+
import builtins
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
# ── Timestamped print with delta + color ──
|
|
15
|
+
_builtin_print = builtins.print
|
|
16
|
+
_start_ts = time.monotonic()
|
|
17
|
+
_last_ts = time.monotonic()
|
|
18
|
+
_first_line = True
|
|
19
|
+
_module_last_ts: dict[str, float] = {}
|
|
20
|
+
|
|
21
|
+
_MODULE_PREFIX_RE = re.compile(r"^\[([a-z_]+)\]")
|
|
22
|
+
|
|
23
|
+
# ANSI escape codes
|
|
24
|
+
_DIM = "\033[90m"
|
|
25
|
+
_GREEN = "\033[32m"
|
|
26
|
+
_RED = "\033[91m"
|
|
27
|
+
_ORANGE = "\033[38;5;208m"
|
|
28
|
+
_RESET = "\033[0m"
|
|
29
|
+
|
|
30
|
+
# Log file paths
|
|
31
|
+
_log_lock = threading.Lock()
|
|
32
|
+
_log_latest_path = None
|
|
33
|
+
_log_daily_path = None
|
|
34
|
+
_log_daily_date = ""
|
|
35
|
+
_log_dir = None
|
|
36
|
+
_crash_log_path = None
|
|
37
|
+
|
|
38
|
+
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
39
|
+
|
|
40
|
+
def _strip_ansi(s: str) -> str:
|
|
41
|
+
return _ANSI_RE.sub("", s)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def init_log_files():
|
|
45
|
+
"""Initialize log file paths. Called after KITE_MODULE_DATA is set."""
|
|
46
|
+
global _log_latest_path, _log_dir, _crash_log_path
|
|
47
|
+
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
48
|
+
if not module_data:
|
|
49
|
+
return
|
|
50
|
+
_log_dir = os.path.join(module_data, "log")
|
|
51
|
+
os.makedirs(_log_dir, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
suffix = os.environ.get("KITE_INSTANCE_SUFFIX", "")
|
|
54
|
+
|
|
55
|
+
# latest.log — truncate on each startup
|
|
56
|
+
_log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
|
|
57
|
+
try:
|
|
58
|
+
with open(_log_latest_path, "w", encoding="utf-8") as f:
|
|
59
|
+
pass
|
|
60
|
+
except Exception as e:
|
|
61
|
+
_builtin_print(f"[launcher] 警告: 无法初始化 {os.path.basename(_log_latest_path)}: {e}")
|
|
62
|
+
_log_latest_path = None
|
|
63
|
+
|
|
64
|
+
# crashes.jsonl — truncate on each startup
|
|
65
|
+
_crash_log_path = os.path.join(_log_dir, f"crashes{suffix}.jsonl")
|
|
66
|
+
try:
|
|
67
|
+
with open(_crash_log_path, "w", encoding="utf-8") as f:
|
|
68
|
+
pass
|
|
69
|
+
except Exception:
|
|
70
|
+
_crash_log_path = None
|
|
71
|
+
|
|
72
|
+
# daily log — ensure directory exists
|
|
73
|
+
_resolve_daily_log_path()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_daily_log_path():
|
|
77
|
+
"""Resolve the daily log file path based on current date."""
|
|
78
|
+
global _log_daily_path, _log_daily_date
|
|
79
|
+
if not _log_dir:
|
|
80
|
+
return
|
|
81
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
82
|
+
if today == _log_daily_date and _log_daily_path:
|
|
83
|
+
return
|
|
84
|
+
month_dir = os.path.join(_log_dir, today[:7])
|
|
85
|
+
os.makedirs(month_dir, exist_ok=True)
|
|
86
|
+
_log_daily_path = os.path.join(month_dir, f"{today}.log")
|
|
87
|
+
_log_daily_date = today
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _write_log(plain_line: str):
|
|
91
|
+
"""Write a plain-text line to both latest.log and daily log."""
|
|
92
|
+
with _log_lock:
|
|
93
|
+
if _log_latest_path:
|
|
94
|
+
try:
|
|
95
|
+
with open(_log_latest_path, "a", encoding="utf-8") as f:
|
|
96
|
+
f.write(plain_line)
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
_resolve_daily_log_path()
|
|
100
|
+
if _log_daily_path:
|
|
101
|
+
try:
|
|
102
|
+
with open(_log_daily_path, "a", encoding="utf-8") as f:
|
|
103
|
+
f.write(plain_line)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _write_crash(exc_type, exc_value, exc_tb,
|
|
109
|
+
thread_name=None, severity="critical", handled=False):
|
|
110
|
+
"""Write crash record to crashes.jsonl + daily archive."""
|
|
111
|
+
record = {
|
|
112
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
113
|
+
"module": "launcher",
|
|
114
|
+
"thread": thread_name or threading.current_thread().name,
|
|
115
|
+
"exception_type": exc_type.__name__ if exc_type else "Unknown",
|
|
116
|
+
"exception_message": str(exc_value),
|
|
117
|
+
"traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
118
|
+
"severity": severity,
|
|
119
|
+
"handled": handled,
|
|
120
|
+
"process_id": os.getpid(),
|
|
121
|
+
"platform": sys.platform,
|
|
122
|
+
"runtime_version": f"Python {sys.version.split()[0]}",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if exc_tb:
|
|
126
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
127
|
+
if tb_entries:
|
|
128
|
+
last = tb_entries[-1]
|
|
129
|
+
record["context"] = {
|
|
130
|
+
"function": last.name,
|
|
131
|
+
"file": os.path.basename(last.filename),
|
|
132
|
+
"line": last.lineno,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
line = json.dumps(record, ensure_ascii=False) + "\n"
|
|
136
|
+
|
|
137
|
+
# Write to crashes.jsonl (current run)
|
|
138
|
+
if _crash_log_path:
|
|
139
|
+
try:
|
|
140
|
+
with open(_crash_log_path, "a", encoding="utf-8") as f:
|
|
141
|
+
f.write(line)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
# Write to daily archive
|
|
146
|
+
if _log_dir:
|
|
147
|
+
try:
|
|
148
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
149
|
+
archive_dir = os.path.join(_log_dir, "crashes", today[:7])
|
|
150
|
+
os.makedirs(archive_dir, exist_ok=True)
|
|
151
|
+
archive_path = os.path.join(archive_dir, f"{today}.jsonl")
|
|
152
|
+
with open(archive_path, "a", encoding="utf-8") as f:
|
|
153
|
+
f.write(line)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _print_crash_summary(exc_type, exc_tb, thread_name=None):
|
|
159
|
+
"""Print crash summary to console."""
|
|
160
|
+
if exc_tb:
|
|
161
|
+
tb_entries = traceback.extract_tb(exc_tb)
|
|
162
|
+
if tb_entries:
|
|
163
|
+
last = tb_entries[-1]
|
|
164
|
+
location = f"{os.path.basename(last.filename)}:{last.lineno}"
|
|
165
|
+
else:
|
|
166
|
+
location = "unknown"
|
|
167
|
+
else:
|
|
168
|
+
location = "unknown"
|
|
169
|
+
|
|
170
|
+
prefix = "[launcher]"
|
|
171
|
+
if thread_name:
|
|
172
|
+
print(f"{prefix} {_RED}线程 {thread_name} 崩溃: "
|
|
173
|
+
f"{exc_type.__name__} in {location}{_RESET}")
|
|
174
|
+
else:
|
|
175
|
+
print(f"{prefix} {_RED}崩溃: {exc_type.__name__} in {location}{_RESET}")
|
|
176
|
+
if _crash_log_path:
|
|
177
|
+
print(f"{prefix} 崩溃日志: {_crash_log_path}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def setup_exception_hooks():
|
|
181
|
+
"""Set up global exception hooks for launcher process."""
|
|
182
|
+
_orig_excepthook = sys.excepthook
|
|
183
|
+
|
|
184
|
+
def _excepthook(exc_type, exc_value, exc_tb):
|
|
185
|
+
_write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=False)
|
|
186
|
+
_print_crash_summary(exc_type, exc_tb)
|
|
187
|
+
_orig_excepthook(exc_type, exc_value, exc_tb)
|
|
188
|
+
|
|
189
|
+
sys.excepthook = _excepthook
|
|
190
|
+
|
|
191
|
+
if hasattr(threading, "excepthook"):
|
|
192
|
+
def _thread_excepthook(args):
|
|
193
|
+
_write_crash(args.exc_type, args.exc_value, args.exc_traceback,
|
|
194
|
+
thread_name=args.thread.name if args.thread else "unknown",
|
|
195
|
+
severity="error", handled=False)
|
|
196
|
+
_print_crash_summary(args.exc_type, args.exc_traceback,
|
|
197
|
+
thread_name=args.thread.name if args.thread else None)
|
|
198
|
+
|
|
199
|
+
threading.excepthook = _thread_excepthook
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _tprint(*args, **kwargs):
|
|
203
|
+
"""Timestamped print with delta tracking."""
|
|
204
|
+
global _last_ts, _first_line
|
|
205
|
+
now = time.monotonic()
|
|
206
|
+
elapsed = now - _start_ts
|
|
207
|
+
|
|
208
|
+
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
209
|
+
|
|
210
|
+
if elapsed < 1:
|
|
211
|
+
elapsed_str = f"{elapsed * 1000:.0f}ms"
|
|
212
|
+
elif elapsed < 100:
|
|
213
|
+
elapsed_str = f"{elapsed:.1f}s"
|
|
214
|
+
else:
|
|
215
|
+
elapsed_str = f"{elapsed:.0f}s"
|
|
216
|
+
|
|
217
|
+
text_for_module = " ".join(str(a) for a in args) if args else ""
|
|
218
|
+
plain_text = _ANSI_RE.sub("", text_for_module).strip()
|
|
219
|
+
m = _MODULE_PREFIX_RE.match(plain_text)
|
|
220
|
+
module_name = m.group(1) if m else None
|
|
221
|
+
|
|
222
|
+
if module_name:
|
|
223
|
+
if module_name in _module_last_ts:
|
|
224
|
+
delta = now - _module_last_ts[module_name]
|
|
225
|
+
else:
|
|
226
|
+
delta = None
|
|
227
|
+
_module_last_ts[module_name] = now
|
|
228
|
+
else:
|
|
229
|
+
delta = now - _last_ts
|
|
230
|
+
_last_ts = now
|
|
231
|
+
|
|
232
|
+
if delta is None:
|
|
233
|
+
delta_str = ""
|
|
234
|
+
delta_color = _DIM
|
|
235
|
+
elif delta < 1:
|
|
236
|
+
delta_str = f"+{delta * 1000:.0f}ms"
|
|
237
|
+
elif delta < 100:
|
|
238
|
+
delta_str = f"+{delta:.1f}s"
|
|
239
|
+
else:
|
|
240
|
+
delta_str = f"+{delta:.0f}s"
|
|
241
|
+
|
|
242
|
+
if delta is not None:
|
|
243
|
+
if delta >= 5:
|
|
244
|
+
delta_color = _RED
|
|
245
|
+
elif delta >= 1:
|
|
246
|
+
delta_color = _GREEN
|
|
247
|
+
else:
|
|
248
|
+
delta_color = _DIM
|
|
249
|
+
|
|
250
|
+
if _first_line:
|
|
251
|
+
_first_line = False
|
|
252
|
+
prefix = f"{_ORANGE}[{elapsed_str:>6}] {ts} {delta_str:>8} "
|
|
253
|
+
_builtin_print(prefix, end="")
|
|
254
|
+
_builtin_print(*args, end="", **{k: v for k, v in kwargs.items() if k != "end"})
|
|
255
|
+
_builtin_print(_RESET, end=kwargs.get("end", "\n"))
|
|
256
|
+
else:
|
|
257
|
+
prefix = f"[{elapsed_str:>6}] {ts} {delta_color}{delta_str:>8}{_RESET} "
|
|
258
|
+
_builtin_print(prefix, end="")
|
|
259
|
+
_builtin_print(*args, **kwargs)
|
|
260
|
+
|
|
261
|
+
if _log_latest_path or _log_daily_path:
|
|
262
|
+
sep = kwargs.get("sep", " ")
|
|
263
|
+
end = kwargs.get("end", "\n")
|
|
264
|
+
text = sep.join(str(a) for a in args)
|
|
265
|
+
plain_prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
|
|
266
|
+
_write_log(plain_prefix + _strip_ansi(text) + end)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def setup_timestamped_print():
|
|
270
|
+
"""Replace builtins.print with timestamped version."""
|
|
271
|
+
builtins.print = _tprint
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def reset_time_baseline():
|
|
275
|
+
"""Reset time baseline (used after code stats to exclude that time)."""
|
|
276
|
+
global _start_ts, _last_ts
|
|
277
|
+
_start_ts = time.monotonic()
|
|
278
|
+
_last_ts = time.monotonic()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_crash_log_path():
|
|
282
|
+
"""Get the crash log path for error reporting."""
|
|
283
|
+
return _crash_log_path
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def write_crash_handled(exc_type, exc_value, exc_tb):
|
|
287
|
+
"""Write a handled crash to the log."""
|
|
288
|
+
_write_crash(exc_type, exc_value, exc_tb, severity="critical", handled=True)
|
|
289
|
+
_print_crash_summary(exc_type, exc_tb)
|
|
@@ -35,9 +35,11 @@ class ModuleInfo:
|
|
|
35
35
|
launch: LaunchConfig = field(default_factory=LaunchConfig)
|
|
36
36
|
|
|
37
37
|
def is_core(self) -> bool:
|
|
38
|
-
"""Core modules live directly under {KITE_PROJECT}
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
"""Core modules (launcher, kernel) live directly under {KITE_PROJECT}/."""
|
|
39
|
+
project_root = os.environ["KITE_PROJECT"]
|
|
40
|
+
# Check if module_dir is launcher or kernel at project root
|
|
41
|
+
return self.name in ("launcher", "kernel") and \
|
|
42
|
+
os.path.normcase(self.module_dir) == os.path.normcase(os.path.join(project_root, self.name))
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def _parse_frontmatter(text: str) -> dict:
|
|
@@ -92,15 +94,18 @@ class ModuleScanner:
|
|
|
92
94
|
def __init__(self, discovery: dict = None, launcher_dir: str = ""):
|
|
93
95
|
self.discovery = discovery
|
|
94
96
|
project_root = os.environ["KITE_PROJECT"]
|
|
95
|
-
self.launcher_dir = launcher_dir or os.path.join(project_root, "
|
|
97
|
+
self.launcher_dir = launcher_dir or os.path.join(project_root, "launcher")
|
|
96
98
|
|
|
97
99
|
def scan(self) -> dict[str, ModuleInfo]:
|
|
98
100
|
"""Return dict of {module_name: ModuleInfo}. Duplicate names are skipped."""
|
|
99
101
|
modules = {}
|
|
100
102
|
project_root = os.environ["KITE_PROJECT"]
|
|
101
103
|
|
|
102
|
-
# Built-in:
|
|
103
|
-
|
|
104
|
+
# Built-in: scan kernel (depth 0) and extensions/ (depth 2)
|
|
105
|
+
# Note: launcher is not scanned (it's the scanner itself)
|
|
106
|
+
kernel_dir = os.path.join(project_root, "kernel")
|
|
107
|
+
if os.path.isdir(kernel_dir):
|
|
108
|
+
self._scan_dir(kernel_dir, 0, modules)
|
|
104
109
|
self._scan_dir(os.path.join(project_root, "extensions"), 2, modules)
|
|
105
110
|
|
|
106
111
|
# Extra sources from discovery config
|
package/main.py
CHANGED
|
@@ -1,357 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Kite development entry point.
|
|
3
|
-
|
|
3
|
+
1. Run code stats
|
|
4
|
+
2. Start launcher
|
|
4
5
|
"""
|
|
5
|
-
import builtins
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import re
|
|
9
|
-
import secrets
|
|
10
6
|
import sys
|
|
11
|
-
import
|
|
12
|
-
import time
|
|
13
|
-
import traceback
|
|
14
|
-
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
15
8
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
# separate PIDs — unaffected. Their stdout is relayed via ProcessManager._read_stdout
|
|
19
|
-
# → print(), so timestamps & deltas are added at the relay point automatically.
|
|
9
|
+
# Add project root to sys.path
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
_first_line = True
|
|
25
|
-
_module_last_ts: dict[str, float] = {} # module_name -> last print timestamp
|
|
12
|
+
# 1. Run code stats
|
|
13
|
+
from launcher.count_lines import run_stats
|
|
14
|
+
run_stats()
|
|
26
15
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
41
|
-
_log_lock = threading.Lock()
|
|
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)
|
|
49
|
-
|
|
50
|
-
# Strip ANSI escape sequences for plain-text log files
|
|
51
|
-
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
52
|
-
|
|
53
|
-
def _strip_ansi(s: str) -> str:
|
|
54
|
-
return _ANSI_RE.sub("", s)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _init_log_files():
|
|
58
|
-
"""Initialize log file paths. Called after KITE_MODULE_DATA is set."""
|
|
59
|
-
global _log_latest_path, _log_dir, _crash_log_path
|
|
60
|
-
module_data = os.environ.get("KITE_MODULE_DATA")
|
|
61
|
-
if not module_data:
|
|
62
|
-
return
|
|
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", "")
|
|
67
|
-
|
|
68
|
-
# latest.log — truncate on each startup (write empty to clear)
|
|
69
|
-
_log_latest_path = os.path.join(_log_dir, f"latest{suffix}.log")
|
|
70
|
-
try:
|
|
71
|
-
with open(_log_latest_path, "w", encoding="utf-8") as f:
|
|
72
|
-
pass # truncate
|
|
73
|
-
except Exception as e:
|
|
74
|
-
_builtin_print(f"[launcher] 警告: 无法初始化 {os.path.basename(_log_latest_path)}: {e}")
|
|
75
|
-
_log_latest_path = None
|
|
76
|
-
|
|
77
|
-
# crashes.jsonl — truncate 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()
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
94
|
-
today = datetime.now().strftime("%Y-%m-%d")
|
|
95
|
-
if today == _log_daily_date and _log_daily_path:
|
|
96
|
-
return
|
|
97
|
-
month_dir = os.path.join(_log_dir, today[:7]) # YYYY-MM
|
|
98
|
-
os.makedirs(month_dir, exist_ok=True)
|
|
99
|
-
_log_daily_path = os.path.join(month_dir, f"{today}.log")
|
|
100
|
-
_log_daily_date = today
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _write_log(plain_line: str):
|
|
104
|
-
"""Write a plain-text line to both latest.log and daily log (open-write-close)."""
|
|
105
|
-
with _log_lock:
|
|
106
|
-
if _log_latest_path:
|
|
107
|
-
try:
|
|
108
|
-
with open(_log_latest_path, "a", encoding="utf-8") as f:
|
|
109
|
-
f.write(plain_line)
|
|
110
|
-
except Exception:
|
|
111
|
-
pass
|
|
112
|
-
# Check daily rotation
|
|
113
|
-
_resolve_daily_log_path()
|
|
114
|
-
if _log_daily_path:
|
|
115
|
-
try:
|
|
116
|
-
with open(_log_daily_path, "a", encoding="utf-8") as f:
|
|
117
|
-
f.write(plain_line)
|
|
118
|
-
except Exception:
|
|
119
|
-
pass
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _tprint(*args, **kwargs):
|
|
218
|
-
global _last_ts, _first_line
|
|
219
|
-
now = time.monotonic()
|
|
220
|
-
elapsed = now - _start_ts
|
|
221
|
-
|
|
222
|
-
# Timestamp
|
|
223
|
-
ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
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
|
-
|
|
251
|
-
# Format delta string
|
|
252
|
-
if delta is None:
|
|
253
|
-
delta_str = ""
|
|
254
|
-
delta_color = _DIM
|
|
255
|
-
elif delta < 1:
|
|
256
|
-
delta_str = f"+{delta * 1000:.0f}ms"
|
|
257
|
-
elif delta < 100:
|
|
258
|
-
delta_str = f"+{delta:.1f}s"
|
|
259
|
-
else:
|
|
260
|
-
delta_str = f"+{delta:.0f}s"
|
|
261
|
-
|
|
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"))
|
|
278
|
-
else:
|
|
279
|
-
prefix = f"[{elapsed_str:>6}] {ts} {delta_color}{delta_str:>8}{_RESET} "
|
|
280
|
-
_builtin_print(prefix, end="")
|
|
281
|
-
_builtin_print(*args, **kwargs)
|
|
282
|
-
|
|
283
|
-
# Write to log files (plain text, no ANSI)
|
|
284
|
-
if _log_latest_path or _log_daily_path:
|
|
285
|
-
sep = kwargs.get("sep", " ")
|
|
286
|
-
end = kwargs.get("end", "\n")
|
|
287
|
-
text = sep.join(str(a) for a in args)
|
|
288
|
-
plain_prefix = f"[{elapsed_str:>6}] {ts} {delta_str:>8} "
|
|
289
|
-
_write_log(plain_prefix + _strip_ansi(text) + end)
|
|
290
|
-
|
|
291
|
-
builtins.print = _tprint
|
|
292
|
-
|
|
293
|
-
# Load .env (development convenience, not required in production)
|
|
294
|
-
try:
|
|
295
|
-
from dotenv import load_dotenv
|
|
296
|
-
load_dotenv()
|
|
297
|
-
except ImportError:
|
|
298
|
-
pass
|
|
299
|
-
|
|
300
|
-
# Resolve project root (directory containing this file)
|
|
301
|
-
_project_root = os.path.dirname(os.path.abspath(__file__))
|
|
302
|
-
|
|
303
|
-
# Home base for Kite data
|
|
304
|
-
_home = os.environ.get("HOME") or os.environ.get("USERPROFILE") or os.path.expanduser("~")
|
|
305
|
-
_kite_home = os.path.join(_home, ".kite")
|
|
306
|
-
|
|
307
|
-
# Set KITE_* defaults (only if not already set by cli.js or .env)
|
|
308
|
-
_defaults = {
|
|
309
|
-
"KITE_PROJECT": _project_root,
|
|
310
|
-
"KITE_CWD": os.getcwd(),
|
|
311
|
-
"KITE_WORKSPACE": os.path.join(_kite_home, "workspace"),
|
|
312
|
-
"KITE_DATA": os.path.join(_kite_home, "data"),
|
|
313
|
-
"KITE_MODULES": os.path.join(_kite_home, "modules"),
|
|
314
|
-
"KITE_REPO": os.path.join(_kite_home, "repo"),
|
|
315
|
-
"KITE_ENV": "development",
|
|
316
|
-
}
|
|
317
|
-
for key, value in _defaults.items():
|
|
318
|
-
if not os.environ.get(key):
|
|
319
|
-
os.environ[key] = value
|
|
320
|
-
|
|
321
|
-
# Parse --debug flag
|
|
322
|
-
if "--debug" in sys.argv:
|
|
323
|
-
os.environ["KITE_DEBUG"] = "1"
|
|
324
|
-
sys.argv.remove("--debug")
|
|
325
|
-
|
|
326
|
-
# Ensure project root is on sys.path
|
|
327
|
-
sys.path.insert(0, os.environ["KITE_PROJECT"])
|
|
328
|
-
|
|
329
|
-
_builtin_print("[launcher] 正在加载模块...")
|
|
330
|
-
from core.launcher.entry import Launcher
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
339
|
-
token = secrets.token_hex(32)
|
|
340
|
-
launcher = Launcher(kite_token=token)
|
|
341
|
-
# KITE_MODULE_DATA is now set by constructor — initialize log files
|
|
342
|
-
_init_log_files()
|
|
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)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if __name__ == "__main__":
|
|
357
|
-
main()
|
|
16
|
+
# 2. Start launcher
|
|
17
|
+
from launcher.entry import start_launcher
|
|
18
|
+
start_launcher()
|