@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,267 @@
1
+ """
2
+ Kernel unified server.
3
+ FastAPI app with WebSocket endpoint (RPC + Event) and minimal HTTP endpoints (/health, /stats).
4
+ Merges Registry + Event Hub into a single process.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+
11
+ from fastapi import FastAPI
12
+ from starlette.websockets import WebSocket, WebSocketDisconnect
13
+
14
+ from .registry_store import RegistryStore
15
+ from .event_hub import EventHub
16
+ from .rpc_router import RpcRouter
17
+
18
+ try:
19
+ import orjson
20
+ def _loads(raw: str):
21
+ return orjson.loads(raw)
22
+ except ImportError:
23
+ def _loads(raw: str):
24
+ return json.loads(raw)
25
+
26
+
27
+ class KernelServer:
28
+ """Merged Registry + Event Hub server.
29
+
30
+ Single WebSocket endpoint handles:
31
+ - JSON-RPC 2.0 requests (builtin + cross-module forward)
32
+ - JSON-RPC 2.0 responses (from forwarded calls)
33
+ - Event notifications (delivered to subscribers)
34
+ """
35
+
36
+ def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1"):
37
+ self.advertise_ip = advertise_ip
38
+ self.port: int = 0 # set by entry.py before uvicorn.run
39
+
40
+ # Core components
41
+ self.registry = RegistryStore(launcher_token) # Can be None
42
+ self.event_hub = EventHub()
43
+
44
+ # Shared connection table (module_id -> WebSocket)
45
+ # RpcRouter and EventHub both reference this
46
+ self.connections: dict[str, WebSocket] = {}
47
+
48
+ # RPC router (pass self reference)
49
+ self.rpc_router = RpcRouter(
50
+ self.registry,
51
+ self.event_hub,
52
+ self.connections,
53
+ kernel_server=self
54
+ )
55
+
56
+ # Background tasks
57
+ self._ttl_task: asyncio.Task | None = None
58
+ self._dedup_task: asyncio.Task | None = None
59
+ self._uvicorn_server = None # set by entry.py for graceful shutdown
60
+ self._shutting_down = False
61
+
62
+ # Launcher connection tracking
63
+ self._launcher_connected = False
64
+ self._launcher_subscribed = False
65
+ self._ready_published = False
66
+
67
+ # Build FastAPI app
68
+ self.app = self._create_app()
69
+
70
+ # ── App factory ──
71
+
72
+ def _create_app(self) -> FastAPI:
73
+ app = FastAPI(title="Kite Kernel", docs_url=None, redoc_url=None)
74
+ server = self
75
+
76
+ @app.on_event("startup")
77
+ async def _startup():
78
+ server._ttl_task = asyncio.create_task(server._ttl_loop())
79
+ server._dedup_task = asyncio.create_task(server._dedup_loop())
80
+
81
+ @app.on_event("shutdown")
82
+ async def _shutdown():
83
+ if server._ttl_task:
84
+ server._ttl_task.cancel()
85
+ if server._dedup_task:
86
+ server._dedup_task.cancel()
87
+
88
+ # ── WebSocket endpoint ──
89
+
90
+ @app.websocket("/ws")
91
+ async def ws_endpoint(ws: WebSocket):
92
+ token = ws.query_params.get("token", "")
93
+ mid_hint = ws.query_params.get("id", "")
94
+
95
+ # Token verification (all modules including Launcher need token)
96
+ module_id = server.registry.verify_token(token)
97
+ if module_id is None:
98
+ # Must accept before close (Starlette requirement)
99
+ await ws.accept()
100
+ print(f"[kernel] Auth failed: token={token[:8]}... hint={mid_hint}")
101
+ try:
102
+ await ws.close(code=4001, reason="Authentication failed")
103
+ except Exception:
104
+ pass
105
+ return
106
+
107
+ # Use id hint for debug mode
108
+ if module_id == "debug" and mid_hint:
109
+ module_id = mid_hint
110
+
111
+ await ws.accept()
112
+
113
+ # Register connection in both EventHub and shared connections table
114
+ server.event_hub.add_connection(module_id, ws)
115
+ server.connections[module_id] = ws
116
+
117
+ # Track Launcher connection
118
+ if module_id == "launcher":
119
+ server._launcher_connected = True
120
+ print(f"[kernel] launcher connected")
121
+
122
+ # Renew heartbeat on connect
123
+ if module_id in server.registry.modules:
124
+ server.registry.heartbeat(module_id)
125
+
126
+ try:
127
+ while True:
128
+ raw = await ws.receive_text()
129
+ try:
130
+ msg = _loads(raw)
131
+ except Exception:
132
+ # JSON parse error — send error if there's an id
133
+ try:
134
+ await ws.send_text(json.dumps({
135
+ "jsonrpc": "2.0", "id": None,
136
+ "error": {"code": -32700, "message": "Parse error"}
137
+ }))
138
+ except Exception:
139
+ pass
140
+ continue
141
+
142
+ if not isinstance(msg, dict):
143
+ continue
144
+
145
+ # Classify message type:
146
+ has_method = "method" in msg
147
+ has_id = "id" in msg
148
+ has_result = "result" in msg
149
+ has_error = "error" in msg
150
+
151
+ if has_method and has_id:
152
+ # RPC Request → dispatch to handler
153
+ await server.rpc_router.dispatch(module_id, ws, msg)
154
+ elif has_id and (has_result or has_error):
155
+ # RPC Response → match to pending forward
156
+ await server.rpc_router.handle_response(module_id, msg)
157
+ # else: notification or unknown — ignore
158
+
159
+ except WebSocketDisconnect:
160
+ pass
161
+ except Exception as e:
162
+ err = str(e).lower()
163
+ if "not connected" not in err and "closed" not in err:
164
+ print(f"[kernel] WebSocket error for {module_id}: {e}")
165
+ finally:
166
+ # Cleanup
167
+ server.event_hub.remove_connection(module_id)
168
+ server.connections.pop(module_id, None)
169
+ server.registry.set_offline(module_id)
170
+ server.event_hub.publish_internal(
171
+ "module.offline", {"module_id": module_id})
172
+
173
+ # ── HTTP endpoints (debug only) ──
174
+
175
+ @app.get("/health")
176
+ async def health():
177
+ eh_health = server.event_hub.get_health()
178
+ return {
179
+ "status": "healthy",
180
+ "module_count": len(server.registry.modules),
181
+ "online_count": sum(
182
+ 1 for m in server.registry.modules.values()
183
+ if m.get("status") == "online"
184
+ ),
185
+ "event_stats": eh_health.get("details", {}),
186
+ }
187
+
188
+ @app.get("/stats")
189
+ async def stats():
190
+ return {
191
+ "registry": {
192
+ "modules": {
193
+ mid: {
194
+ "status": data.get("status"),
195
+ "module_type": data.get("module_type"),
196
+ "registered_at": data.get("registered_at"),
197
+ }
198
+ for mid, data in server.registry.modules.items()
199
+ },
200
+ },
201
+ "event_hub": server.event_hub.get_stats(),
202
+ }
203
+
204
+ return app
205
+
206
+ # ── Background loops ──
207
+
208
+ async def _ttl_loop(self):
209
+ """Check heartbeat TTL every 10s and publish offline events."""
210
+ while True:
211
+ await asyncio.sleep(10)
212
+ try:
213
+ expired = self.registry.check_ttl()
214
+ for mid in expired:
215
+ self.event_hub.publish_internal(
216
+ "module.offline", {"module_id": mid})
217
+ except Exception as e:
218
+ print(f"[kernel] TTL loop error: {e}")
219
+
220
+ async def _dedup_loop(self):
221
+ """Clean up dedup table every 30s."""
222
+ while True:
223
+ await asyncio.sleep(30)
224
+ try:
225
+ await asyncio.get_event_loop().run_in_executor(
226
+ None, self.event_hub.dedup.cleanup)
227
+ except Exception as e:
228
+ print(f"[kernel] Dedup cleanup error: {e}")
229
+
230
+ # ── Self-registration ──
231
+
232
+ def self_register(self):
233
+ """Register Kernel itself in the registry (in-memory, no RPC needed)."""
234
+ self.registry.register_module({
235
+ "module_id": "kernel",
236
+ "module_type": "infrastructure",
237
+ "api_endpoint": f"http://{self.advertise_ip}:{self.port}",
238
+ "health_endpoint": "/health",
239
+ "metadata": {
240
+ "ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
241
+ },
242
+ })
243
+
244
+ def publish_ready(self):
245
+ """Publish module.ready event for Kernel (internal, no WS needed)."""
246
+ self.event_hub.publish_internal("module.ready", {
247
+ "module_id": "kernel",
248
+ "ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
249
+ "graceful_shutdown": True,
250
+ })
251
+
252
+ async def shutdown(self):
253
+ """Shutdown Kernel gracefully. Called by Launcher via RPC."""
254
+ if self._shutting_down:
255
+ return
256
+
257
+ self._shutting_down = True
258
+ print("[kernel] Shutting down...")
259
+
260
+ # Brief delay to ensure RPC response is sent
261
+ await asyncio.sleep(0.1)
262
+
263
+ # Trigger uvicorn shutdown
264
+ if self._uvicorn_server:
265
+ self._uvicorn_server.should_exit = True
266
+ else:
267
+ print("[kernel] Warning: uvicorn server reference not set")
@@ -0,0 +1,10 @@
1
+ """
2
+ Launcher package — Kite system entry point.
3
+
4
+ Can be started in two ways:
5
+ 1. Via main.py (with code stats): python main.py
6
+ 2. Directly (no stats): python -m launcher
7
+ """
8
+ from .entry import start_launcher
9
+
10
+ __all__ = ["start_launcher"]
@@ -0,0 +1,6 @@
1
+ """
2
+ Direct launcher entry point: python -m launcher
3
+ """
4
+ if __name__ == "__main__":
5
+ from .entry import start_launcher
6
+ start_launcher()
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 代码行数统计工具
4
+ 统计 Kite 项目的代码行数并记录到 JSONL 文件
5
+ """
6
+ import json
7
+ import sys
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ # Enable ANSI colors on Windows
12
+ if sys.platform == "win32":
13
+ try:
14
+ import ctypes
15
+ kernel32 = ctypes.windll.kernel32
16
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
17
+ except Exception:
18
+ pass
19
+
20
+
21
+ def count_lines_in_file(file_path: Path) -> int:
22
+ """统计单个文件的行数"""
23
+ try:
24
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
25
+ return sum(1 for _ in f)
26
+ except Exception:
27
+ return 0
28
+
29
+
30
+ def count_lines(root_dir: Path, pattern: str, exclude_dirs: set[str]) -> int:
31
+ """统计指定模式的文件行数"""
32
+ total = 0
33
+ for file_path in root_dir.rglob(pattern):
34
+ # 检查是否在排除目录中
35
+ if any(excluded in file_path.parts for excluded in exclude_dirs):
36
+ continue
37
+ total += count_lines_in_file(file_path)
38
+ return total
39
+
40
+
41
+ def count_all(root_dir: Path) -> dict:
42
+ """统计所有类型的代码行数"""
43
+ stats = {}
44
+
45
+ # 排除的目录
46
+ exclude_dirs = {
47
+ "__pycache__",
48
+ "node_modules",
49
+ ".git",
50
+ ".venv",
51
+ "venv",
52
+ ".idea",
53
+ ".vscode",
54
+ ".dev", # 开发相关(变更日志、发布备份)
55
+ ".claude", # Claude 工作目录
56
+ }
57
+
58
+ # Python 代码
59
+ stats["python"] = count_lines(root_dir, "*.py", exclude_dirs)
60
+
61
+ # JavaScript 代码
62
+ stats["javascript"] = count_lines(root_dir, "*.js", exclude_dirs)
63
+
64
+ # Markdown 文档
65
+ stats["markdown"] = count_lines(root_dir, "*.md", exclude_dirs)
66
+
67
+ # YAML 配置
68
+ stats["yaml"] = count_lines(root_dir, "*.yaml", exclude_dirs) + \
69
+ count_lines(root_dir, "*.yml", exclude_dirs)
70
+
71
+ # JSON 配置(排除 package-lock.json)
72
+ json_total = 0
73
+ for file_path in root_dir.rglob("*.json"):
74
+ if any(excluded in file_path.parts for excluded in exclude_dirs):
75
+ continue
76
+ if file_path.name == "package-lock.json":
77
+ continue
78
+ json_total += count_lines_in_file(file_path)
79
+ stats["json"] = json_total
80
+
81
+ # 总计
82
+ stats["total"] = sum(stats.values())
83
+
84
+ return stats
85
+
86
+
87
+ def save_record(stats: dict, record_file: Path):
88
+ """保存统计记录到 JSONL 文件(仅在有变化时)"""
89
+ # 读取最后一条记录
90
+ last_stats = None
91
+ if record_file.exists():
92
+ try:
93
+ with open(record_file, "r", encoding="utf-8") as f:
94
+ lines = f.readlines()
95
+ if lines:
96
+ last_record = json.loads(lines[-1])
97
+ last_stats = last_record.get("stats")
98
+ except (json.JSONDecodeError, IndexError):
99
+ pass
100
+
101
+ # 如果统计结果与上次完全相同,跳过保存
102
+ if last_stats == stats:
103
+ return
104
+
105
+ record = {
106
+ "timestamp": datetime.now().isoformat(),
107
+ "stats": stats,
108
+ }
109
+
110
+ # 追加到 JSONL 文件
111
+ record_file.parent.mkdir(parents=True, exist_ok=True)
112
+ with open(record_file, "a", encoding="utf-8") as f:
113
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
114
+
115
+
116
+ def print_stats(stats: dict):
117
+ """打印统计结果"""
118
+ # ANSI 颜色代码
119
+ BOLD = "\033[1m"
120
+ RESET = "\033[0m"
121
+
122
+ print("\n" + "=" * 50)
123
+ print("Kite 项目代码统计")
124
+ print("=" * 50)
125
+ print(f"Python 代码: {BOLD}{stats['python']:>8,} 行{RESET}")
126
+ print(f"JavaScript 代码: {BOLD}{stats['javascript']:>8,} 行{RESET}")
127
+ print(f"Markdown 文档: {BOLD}{stats['markdown']:>8,} 行{RESET}")
128
+ print(f"YAML 配置: {BOLD}{stats['yaml']:>8,} 行{RESET}")
129
+ print(f"JSON 配置: {BOLD}{stats['json']:>8,} 行{RESET}")
130
+ print("-" * 50)
131
+ print(f"总计: {BOLD}{stats['total']:>8,} 行{RESET}")
132
+ print("=" * 50 + "\n")
133
+
134
+
135
+ def show_history(record_file: Path, limit: int = 10):
136
+ """显示历史记录"""
137
+ # ANSI 颜色代码
138
+ GREEN = "\033[32m"
139
+ RED = "\033[31m"
140
+ BOLD = "\033[1m"
141
+ RESET = "\033[0m"
142
+
143
+ if not record_file.exists():
144
+ print("暂无历史记录")
145
+ return
146
+
147
+ records = []
148
+ with open(record_file, "r", encoding="utf-8") as f:
149
+ for line in f:
150
+ try:
151
+ records.append(json.loads(line))
152
+ except json.JSONDecodeError:
153
+ continue
154
+
155
+ if not records:
156
+ print("暂无历史记录")
157
+ return
158
+
159
+ print("\n" + "=" * 80)
160
+ print("历史记录(最近 {} 次)".format(min(limit, len(records))))
161
+ print("=" * 80)
162
+ print(f"{'时间':<25} {'Python':>10} {'JS':>10} {'MD':>10} {'总计':>10} {'变化':>10}")
163
+ print("-" * 80)
164
+
165
+ recent = records[-limit:]
166
+ for i, record in enumerate(recent):
167
+ ts = record["timestamp"][:19].replace("T", " ")
168
+ stats = record["stats"]
169
+
170
+ # 计算与上一次的变化
171
+ if i > 0:
172
+ prev_total = recent[i-1]["stats"]["total"]
173
+ diff = stats["total"] - prev_total
174
+ if diff > 0:
175
+ diff_str = f"{GREEN}+{diff:,}{RESET}"
176
+ elif diff < 0:
177
+ diff_str = f"{RED}{diff:,}{RESET}"
178
+ else:
179
+ diff_str = "0"
180
+ else:
181
+ diff_str = "-"
182
+
183
+ print(f"{ts:<25} {stats['python']:>10,} {stats['javascript']:>10,} "
184
+ f"{stats['markdown']:>10,} {BOLD}{stats['total']:>10,}{RESET} {diff_str:>10}")
185
+
186
+ print("=" * 80 + "\n")
187
+
188
+
189
+ def run_stats():
190
+ """Run code stats from main.py entry point (simplified output)."""
191
+ script_dir = Path(__file__).parent
192
+ root_dir = script_dir.parent
193
+ record_file = root_dir / "data" / "stats" / "lines.jsonl"
194
+
195
+ print("[launcher] 正在统计代码行数...")
196
+ try:
197
+ stats = count_all(root_dir)
198
+ save_record(stats, record_file)
199
+
200
+ # Print stats
201
+ BRIGHT_GREEN = "\033[92m"
202
+ BOLD = "\033[1m"
203
+ RESET = "\033[0m"
204
+
205
+ print("")
206
+ print("=" * 50)
207
+ print("Kite 项目代码统计")
208
+ print("=" * 50)
209
+ print(f"Python 代码: {BRIGHT_GREEN}{stats['python']:>8,}{RESET} 行")
210
+ print(f"JavaScript 代码: {BRIGHT_GREEN}{stats['javascript']:>8,}{RESET} 行")
211
+ print(f"Markdown 文档: {BRIGHT_GREEN}{stats['markdown']:>8,}{RESET} 行")
212
+ print(f"YAML 配置: {BRIGHT_GREEN}{stats['yaml']:>8,}{RESET} 行")
213
+ print(f"JSON 配置: {BRIGHT_GREEN}{stats['json']:>8,}{RESET} 行")
214
+ print("-" * 50)
215
+ print(f"总计: {BOLD}{BRIGHT_GREEN}{stats['total']:>8,}{RESET} 行")
216
+ print("=" * 50)
217
+ print("")
218
+
219
+ # Show history trend (last 8 records)
220
+ show_history(record_file, limit=8)
221
+ except Exception as e:
222
+ print(f"[launcher] 统计异常: {e}")
223
+
224
+
225
+ def main():
226
+ # 获取项目根目录
227
+ script_dir = Path(__file__).parent
228
+ root_dir = script_dir.parent
229
+
230
+ # 记录文件路径
231
+ record_file = root_dir / "data" / "stats" / "lines.jsonl"
232
+
233
+ # 解析命令行参数
234
+ show_hist = "--history" in sys.argv or "-h" in sys.argv
235
+ quiet = "--quiet" in sys.argv or "-q" in sys.argv
236
+
237
+ if show_hist:
238
+ # 只显示历史记录
239
+ show_history(record_file)
240
+ return
241
+
242
+ # 统计代码行数
243
+ if not quiet:
244
+ print("正在统计代码行数...")
245
+ stats = count_all(root_dir)
246
+
247
+ # 保存记录
248
+ save_record(stats, record_file)
249
+
250
+ # 打印结果
251
+ if not quiet:
252
+ print_stats(stats)
253
+ # 显示最近 8 次记录
254
+ show_history(record_file, limit=8)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ main()