@agentunion/kite 1.3.1 → 1.4.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 +287 -1
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -197
- package/extensions/channels/acp_channel/entry.py +111 -1
- package/extensions/channels/acp_channel/module.md +23 -22
- package/extensions/channels/acp_channel/server.py +263 -197
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +408 -72
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +255 -71
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +344 -90
- package/extensions/services/watchdog/monitor.py +237 -21
- package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
- package/extensions/services/web/config_example.py +35 -0
- package/extensions/services/web/config_loader.py +110 -0
- package/extensions/services/web/entry.py +114 -26
- package/extensions/services/web/module.md +35 -24
- package/extensions/services/web/pairing.py +250 -0
- package/extensions/services/web/pairing_codes.jsonl +16 -0
- package/extensions/services/web/relay.py +643 -0
- package/extensions/services/web/relay_config.json5 +67 -0
- package/extensions/services/web/routes/routes_management_ws.py +127 -0
- package/extensions/services/web/routes/routes_rpc.py +89 -0
- package/extensions/services/web/routes/routes_test.py +61 -0
- package/extensions/services/web/server.py +445 -99
- package/extensions/services/web/static/css/style.css +138 -2
- package/extensions/services/web/static/index.html +295 -2
- package/extensions/services/web/static/js/app.js +1579 -5
- package/extensions/services/web/static/js/kernel-client-example.js +161 -0
- package/extensions/services/web/static/js/kernel-client.js +383 -0
- package/extensions/services/web/static/js/registry-tests.js +558 -0
- package/extensions/services/web/static/js/token-manager.js +175 -0
- package/extensions/services/web/static/pairing.html +248 -0
- package/extensions/services/web/static/test_registry.html +262 -0
- package/extensions/services/web/web_config.json5 +29 -0
- package/kernel/entry.py +120 -32
- package/kernel/event_hub.py +159 -16
- package/kernel/module.md +36 -33
- package/kernel/registry_store.py +70 -20
- package/kernel/rpc_router.py +134 -57
- package/kernel/server.py +292 -15
- package/kite_cli/__init__.py +3 -0
- package/kite_cli/__main__.py +5 -0
- package/kite_cli/commands/__init__.py +1 -0
- package/kite_cli/commands/clean.py +101 -0
- package/kite_cli/commands/doctor.py +35 -0
- package/kite_cli/commands/history.py +111 -0
- package/kite_cli/commands/info.py +96 -0
- package/kite_cli/commands/install.py +313 -0
- package/kite_cli/commands/list.py +143 -0
- package/kite_cli/commands/log.py +81 -0
- package/kite_cli/commands/rollback.py +88 -0
- package/kite_cli/commands/search.py +73 -0
- package/kite_cli/commands/uninstall.py +85 -0
- package/kite_cli/commands/update.py +118 -0
- package/kite_cli/core/__init__.py +1 -0
- package/kite_cli/core/checker.py +142 -0
- package/kite_cli/core/dependency.py +229 -0
- package/kite_cli/core/downloader.py +209 -0
- package/kite_cli/core/install_info.py +40 -0
- package/kite_cli/core/tool_installer.py +397 -0
- package/kite_cli/core/validator.py +78 -0
- package/kite_cli/main.py +289 -0
- package/kite_cli/utils/__init__.py +1 -0
- package/kite_cli/utils/i18n.py +252 -0
- package/kite_cli/utils/interactive.py +63 -0
- package/kite_cli/utils/operation_log.py +77 -0
- package/kite_cli/utils/paths.py +34 -0
- package/kite_cli/utils/version.py +308 -0
- package/launcher/count_lines.py +34 -0
- package/launcher/entry.py +905 -166
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/launcher/process_manager.py +12 -1
- package/package.json +2 -1
- package/scripts/plan_manager.py +315 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""操作日志记录"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_log_file() -> Path:
|
|
9
|
+
"""获取日志文件路径"""
|
|
10
|
+
# 使用 KITE_DATA 环境变量,如果没有则使用 ~/.kite/data
|
|
11
|
+
kite_data = os.environ.get("KITE_DATA")
|
|
12
|
+
if kite_data:
|
|
13
|
+
log_dir = Path(kite_data) / "cli_logs"
|
|
14
|
+
else:
|
|
15
|
+
home = Path.home()
|
|
16
|
+
log_dir = home / ".kite" / "data" / "cli_logs"
|
|
17
|
+
|
|
18
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
return log_dir / "operations.jsonl"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def log_operation(operation: str, details: dict, success: bool = True, error: str = None):
|
|
23
|
+
"""记录操作日志"""
|
|
24
|
+
log_entry = {
|
|
25
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
26
|
+
"operation": operation,
|
|
27
|
+
"details": details,
|
|
28
|
+
"success": success,
|
|
29
|
+
"error": error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
log_file = get_log_file()
|
|
34
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
35
|
+
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
# 日志记录失败不应该影响主流程
|
|
38
|
+
print(f"[Warning] 记录操作日志失败: {e}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_operations(limit: int = 50) -> list:
|
|
42
|
+
"""读取最近的操作日志"""
|
|
43
|
+
log_file = get_log_file()
|
|
44
|
+
if not log_file.exists():
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with open(log_file, "r", encoding="utf-8") as f:
|
|
49
|
+
lines = f.readlines()
|
|
50
|
+
|
|
51
|
+
# 返回最后 N 条
|
|
52
|
+
operations = []
|
|
53
|
+
for line in lines[-limit:]:
|
|
54
|
+
try:
|
|
55
|
+
operations.append(json.loads(line.strip()))
|
|
56
|
+
except json.JSONDecodeError:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
return operations
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"[Error] 读取操作日志失败: {e}")
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_last_operation(operation_type: str = None) -> dict:
|
|
66
|
+
"""获取最后一次操作"""
|
|
67
|
+
operations = read_operations(limit=100)
|
|
68
|
+
|
|
69
|
+
if operation_type:
|
|
70
|
+
# 查找指定类型的最后一次操作
|
|
71
|
+
for op in reversed(operations):
|
|
72
|
+
if op.get("operation") == operation_type:
|
|
73
|
+
return op
|
|
74
|
+
return None
|
|
75
|
+
else:
|
|
76
|
+
# 返回最后一次操作
|
|
77
|
+
return operations[-1] if operations else None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""路径处理工具"""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_install_path(location: str, module_name: str) -> Path:
|
|
7
|
+
"""获取安装路径"""
|
|
8
|
+
if location == "dev":
|
|
9
|
+
# 开发环境: {KITE_PROJECT}/extensions/
|
|
10
|
+
kite_project = os.environ.get("KITE_PROJECT")
|
|
11
|
+
if not kite_project:
|
|
12
|
+
# 如果没有环境变量,使用当前目录
|
|
13
|
+
kite_project = Path.cwd()
|
|
14
|
+
return Path(kite_project) / "extensions" / module_name
|
|
15
|
+
|
|
16
|
+
elif location == "local":
|
|
17
|
+
# 本地实例: {KITE_INSTANCE_DIR}/extensions/
|
|
18
|
+
kite_instance = os.environ.get("KITE_INSTANCE_DIR")
|
|
19
|
+
if not kite_instance:
|
|
20
|
+
# 默认路径
|
|
21
|
+
home = Path.home()
|
|
22
|
+
kite_instance = home / ".kite" / "workspace" / "Kite"
|
|
23
|
+
return Path(kite_instance) / "extensions" / module_name
|
|
24
|
+
|
|
25
|
+
elif location == "global":
|
|
26
|
+
# 全局共享: {KITE_MODULES}
|
|
27
|
+
kite_modules = os.environ.get("KITE_MODULES")
|
|
28
|
+
if not kite_modules:
|
|
29
|
+
# 默认路径
|
|
30
|
+
home = Path.home()
|
|
31
|
+
kite_modules = home / ".kite" / "modules"
|
|
32
|
+
return Path(kite_modules) / module_name
|
|
33
|
+
|
|
34
|
+
raise ValueError(f"未知的安装位置: {location}")
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""版本号解析和比较工具
|
|
2
|
+
|
|
3
|
+
支持 semver 版本号解析、比较和匹配。
|
|
4
|
+
"""
|
|
5
|
+
import re
|
|
6
|
+
from typing import Tuple, Optional, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Version:
|
|
10
|
+
"""版本号类"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, version_string: str):
|
|
13
|
+
"""解析版本号字符串"""
|
|
14
|
+
self.original = version_string
|
|
15
|
+
self.major = 0
|
|
16
|
+
self.minor = 0
|
|
17
|
+
self.patch = 0
|
|
18
|
+
self.prerelease = None
|
|
19
|
+
self.build = None
|
|
20
|
+
|
|
21
|
+
self._parse(version_string)
|
|
22
|
+
|
|
23
|
+
def _parse(self, version_string: str):
|
|
24
|
+
"""解析版本号"""
|
|
25
|
+
# 移除 'v' 前缀
|
|
26
|
+
version_string = version_string.lstrip('v')
|
|
27
|
+
|
|
28
|
+
# 分离 prerelease 和 build
|
|
29
|
+
if '+' in version_string:
|
|
30
|
+
version_string, self.build = version_string.split('+', 1)
|
|
31
|
+
|
|
32
|
+
if '-' in version_string:
|
|
33
|
+
version_string, self.prerelease = version_string.split('-', 1)
|
|
34
|
+
|
|
35
|
+
# 解析主版本号
|
|
36
|
+
parts = version_string.split('.')
|
|
37
|
+
if len(parts) >= 1:
|
|
38
|
+
self.major = int(parts[0]) if parts[0].isdigit() else 0
|
|
39
|
+
if len(parts) >= 2:
|
|
40
|
+
self.minor = int(parts[1]) if parts[1].isdigit() else 0
|
|
41
|
+
if len(parts) >= 3:
|
|
42
|
+
self.patch = int(parts[2]) if parts[2].isdigit() else 0
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
"""转为字符串"""
|
|
46
|
+
version = f"{self.major}.{self.minor}.{self.patch}"
|
|
47
|
+
if self.prerelease:
|
|
48
|
+
version += f"-{self.prerelease}"
|
|
49
|
+
if self.build:
|
|
50
|
+
version += f"+{self.build}"
|
|
51
|
+
return version
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
return f"Version('{self}')"
|
|
55
|
+
|
|
56
|
+
def __eq__(self, other):
|
|
57
|
+
"""相等比较"""
|
|
58
|
+
if not isinstance(other, Version):
|
|
59
|
+
other = Version(str(other))
|
|
60
|
+
return (self.major, self.minor, self.patch, self.prerelease) == \
|
|
61
|
+
(other.major, other.minor, other.patch, other.prerelease)
|
|
62
|
+
|
|
63
|
+
def __lt__(self, other):
|
|
64
|
+
"""小于比较"""
|
|
65
|
+
if not isinstance(other, Version):
|
|
66
|
+
other = Version(str(other))
|
|
67
|
+
|
|
68
|
+
# 比较主版本号
|
|
69
|
+
if self.major != other.major:
|
|
70
|
+
return self.major < other.major
|
|
71
|
+
if self.minor != other.minor:
|
|
72
|
+
return self.minor < other.minor
|
|
73
|
+
if self.patch != other.patch:
|
|
74
|
+
return self.patch < other.patch
|
|
75
|
+
|
|
76
|
+
# 比较 prerelease
|
|
77
|
+
if self.prerelease is None and other.prerelease is None:
|
|
78
|
+
return False
|
|
79
|
+
if self.prerelease is None:
|
|
80
|
+
return False # 正式版 > 预发布版
|
|
81
|
+
if other.prerelease is None:
|
|
82
|
+
return True
|
|
83
|
+
return self.prerelease < other.prerelease
|
|
84
|
+
|
|
85
|
+
def __le__(self, other):
|
|
86
|
+
return self == other or self < other
|
|
87
|
+
|
|
88
|
+
def __gt__(self, other):
|
|
89
|
+
return not self <= other
|
|
90
|
+
|
|
91
|
+
def __ge__(self, other):
|
|
92
|
+
return not self < other
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class VersionSpec:
|
|
96
|
+
"""版本规范类"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, spec: str):
|
|
99
|
+
"""解析版本规范"""
|
|
100
|
+
self.original = spec
|
|
101
|
+
self.operator = None
|
|
102
|
+
self.version = None
|
|
103
|
+
self.range_start = None
|
|
104
|
+
self.range_end = None
|
|
105
|
+
|
|
106
|
+
self._parse(spec)
|
|
107
|
+
|
|
108
|
+
def _parse(self, spec: str):
|
|
109
|
+
"""解析版本规范"""
|
|
110
|
+
spec = spec.strip()
|
|
111
|
+
|
|
112
|
+
# latest
|
|
113
|
+
if spec == "latest":
|
|
114
|
+
self.operator = "latest"
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# 范围: 1.0.0 - 2.0.0
|
|
118
|
+
if ' - ' in spec:
|
|
119
|
+
parts = spec.split(' - ')
|
|
120
|
+
if len(parts) == 2:
|
|
121
|
+
self.operator = "range"
|
|
122
|
+
self.range_start = Version(parts[0].strip())
|
|
123
|
+
self.range_end = Version(parts[1].strip())
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# ^1.2.3 (兼容版本)
|
|
127
|
+
if spec.startswith('^'):
|
|
128
|
+
self.operator = "^"
|
|
129
|
+
self.version = Version(spec[1:])
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# ~1.2.3 (近似版本)
|
|
133
|
+
if spec.startswith('~'):
|
|
134
|
+
self.operator = "~"
|
|
135
|
+
self.version = Version(spec[1:])
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# >=1.2.3
|
|
139
|
+
if spec.startswith('>='):
|
|
140
|
+
self.operator = ">="
|
|
141
|
+
self.version = Version(spec[2:])
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# <=1.2.3
|
|
145
|
+
if spec.startswith('<='):
|
|
146
|
+
self.operator = "<="
|
|
147
|
+
self.version = Version(spec[2:])
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# >1.2.3
|
|
151
|
+
if spec.startswith('>'):
|
|
152
|
+
self.operator = ">"
|
|
153
|
+
self.version = Version(spec[1:])
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# <1.2.3
|
|
157
|
+
if spec.startswith('<'):
|
|
158
|
+
self.operator = "<"
|
|
159
|
+
self.version = Version(spec[1:])
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# =1.2.3 或 1.2.3 (精确版本)
|
|
163
|
+
if spec.startswith('='):
|
|
164
|
+
spec = spec[1:]
|
|
165
|
+
self.operator = "="
|
|
166
|
+
self.version = Version(spec)
|
|
167
|
+
|
|
168
|
+
def match(self, version: Version) -> bool:
|
|
169
|
+
"""检查版本是否匹配规范"""
|
|
170
|
+
if not isinstance(version, Version):
|
|
171
|
+
version = Version(str(version))
|
|
172
|
+
|
|
173
|
+
if self.operator == "latest":
|
|
174
|
+
return True # latest 匹配任何版本,由调用者选择最新
|
|
175
|
+
|
|
176
|
+
if self.operator == "=":
|
|
177
|
+
return version == self.version
|
|
178
|
+
|
|
179
|
+
if self.operator == ">":
|
|
180
|
+
return version > self.version
|
|
181
|
+
|
|
182
|
+
if self.operator == ">=":
|
|
183
|
+
return version >= self.version
|
|
184
|
+
|
|
185
|
+
if self.operator == "<":
|
|
186
|
+
return version < self.version
|
|
187
|
+
|
|
188
|
+
if self.operator == "<=":
|
|
189
|
+
return version <= self.version
|
|
190
|
+
|
|
191
|
+
if self.operator == "^":
|
|
192
|
+
# ^1.2.3 匹配 >=1.2.3 且 <2.0.0
|
|
193
|
+
if self.version.major == 0:
|
|
194
|
+
# ^0.x.y 匹配 >=0.x.y 且 <0.(x+1).0
|
|
195
|
+
return version >= self.version and \
|
|
196
|
+
version.major == 0 and \
|
|
197
|
+
version.minor == self.version.minor
|
|
198
|
+
else:
|
|
199
|
+
return version >= self.version and \
|
|
200
|
+
version.major == self.version.major
|
|
201
|
+
|
|
202
|
+
if self.operator == "~":
|
|
203
|
+
# ~1.2.3 匹配 >=1.2.3 且 <1.3.0
|
|
204
|
+
return version >= self.version and \
|
|
205
|
+
version.major == self.version.major and \
|
|
206
|
+
version.minor == self.version.minor
|
|
207
|
+
|
|
208
|
+
if self.operator == "range":
|
|
209
|
+
return self.range_start <= version <= self.range_end
|
|
210
|
+
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def parse_version_spec(spec: str) -> Tuple[Optional[str], Optional[str]]:
|
|
215
|
+
"""解析版本规范字符串
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
spec: 版本规范,如 "pkg@1.2.0", "pkg@^1.0.0", "pkg@latest", "pkg"
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
(package_name, version_spec) 元组
|
|
222
|
+
"""
|
|
223
|
+
if '@' not in spec:
|
|
224
|
+
return spec, None
|
|
225
|
+
|
|
226
|
+
parts = spec.rsplit('@', 1)
|
|
227
|
+
if len(parts) == 2:
|
|
228
|
+
return parts[0], parts[1]
|
|
229
|
+
|
|
230
|
+
return spec, None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def compare_versions(v1: str, v2: str) -> int:
|
|
234
|
+
"""比较两个版本号
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
v1: 版本号 1
|
|
238
|
+
v2: 版本号 2
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
-1 if v1 < v2
|
|
242
|
+
0 if v1 == v2
|
|
243
|
+
1 if v1 > v2
|
|
244
|
+
"""
|
|
245
|
+
version1 = Version(v1)
|
|
246
|
+
version2 = Version(v2)
|
|
247
|
+
|
|
248
|
+
if version1 < version2:
|
|
249
|
+
return -1
|
|
250
|
+
elif version1 > version2:
|
|
251
|
+
return 1
|
|
252
|
+
else:
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def match_version(version: str, spec: str) -> bool:
|
|
257
|
+
"""检查版本是否匹配规范
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
version: 版本号,如 "1.2.3"
|
|
261
|
+
spec: 版本规范,如 "^1.0.0", ">=1.2.0", "1.2.3"
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
是否匹配
|
|
265
|
+
"""
|
|
266
|
+
v = Version(version)
|
|
267
|
+
s = VersionSpec(spec)
|
|
268
|
+
return s.match(v)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_latest_version(versions: List[str]) -> Optional[str]:
|
|
272
|
+
"""从版本列表中获取最新版本
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
versions: 版本号列表
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
最新版本号,如果列表为空则返回 None
|
|
279
|
+
"""
|
|
280
|
+
if not versions:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
version_objects = [Version(v) for v in versions]
|
|
284
|
+
latest = max(version_objects)
|
|
285
|
+
return str(latest)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def filter_versions(versions: List[str], spec: str) -> List[str]:
|
|
289
|
+
"""根据版本规范过滤版本列表
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
versions: 版本号列表
|
|
293
|
+
spec: 版本规范
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
匹配的版本列表,按版本号降序排列
|
|
297
|
+
"""
|
|
298
|
+
version_spec = VersionSpec(spec)
|
|
299
|
+
matched = []
|
|
300
|
+
|
|
301
|
+
for v in versions:
|
|
302
|
+
version = Version(v)
|
|
303
|
+
if version_spec.match(version):
|
|
304
|
+
matched.append(v)
|
|
305
|
+
|
|
306
|
+
# 按版本号降序排列
|
|
307
|
+
matched.sort(key=lambda x: Version(x), reverse=True)
|
|
308
|
+
return matched
|
package/launcher/count_lines.py
CHANGED
|
@@ -185,6 +185,40 @@ def show_history(record_file: Path, limit: int = 10):
|
|
|
185
185
|
|
|
186
186
|
print("=" * 80 + "\n")
|
|
187
187
|
|
|
188
|
+
# 计算每日新增
|
|
189
|
+
daily_stats = {}
|
|
190
|
+
for record in records:
|
|
191
|
+
date = record["timestamp"][:10] # YYYY-MM-DD
|
|
192
|
+
total = record["stats"]["total"]
|
|
193
|
+
if date not in daily_stats:
|
|
194
|
+
daily_stats[date] = {"first": total, "last": total}
|
|
195
|
+
else:
|
|
196
|
+
daily_stats[date]["last"] = total
|
|
197
|
+
|
|
198
|
+
# 计算每日增量
|
|
199
|
+
daily_changes = []
|
|
200
|
+
for date in sorted(daily_stats.keys()):
|
|
201
|
+
day_data = daily_stats[date]
|
|
202
|
+
daily_change = day_data["last"] - day_data["first"]
|
|
203
|
+
if daily_change != 0: # 只显示有变化的日期
|
|
204
|
+
daily_changes.append((date, daily_change))
|
|
205
|
+
|
|
206
|
+
if daily_changes:
|
|
207
|
+
print("=" * 80)
|
|
208
|
+
print("每日新增代码行数")
|
|
209
|
+
print("=" * 80)
|
|
210
|
+
print(f"{'日期':<15} {'新增行数':>15}")
|
|
211
|
+
print("-" * 80)
|
|
212
|
+
for date, change in daily_changes[-10:]: # 最近 10 天
|
|
213
|
+
if change > 0:
|
|
214
|
+
change_str = f"{GREEN}+{change:,}{RESET}"
|
|
215
|
+
elif change < 0:
|
|
216
|
+
change_str = f"{RED}{change:,}{RESET}"
|
|
217
|
+
else:
|
|
218
|
+
change_str = "0"
|
|
219
|
+
print(f"{date:<15} {change_str:>15}")
|
|
220
|
+
print("=" * 80 + "\n")
|
|
221
|
+
|
|
188
222
|
|
|
189
223
|
def run_stats():
|
|
190
224
|
"""Run code stats from main.py entry point (simplified output)."""
|