@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
package/kernel/server.py
ADDED
|
@@ -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,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()
|