aicodestat 0.0.1__py3-none-any.whl
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.
- aicodestat-0.0.1.dist-info/METADATA +110 -0
- aicodestat-0.0.1.dist-info/RECORD +34 -0
- aicodestat-0.0.1.dist-info/WHEEL +5 -0
- aicodestat-0.0.1.dist-info/entry_points.txt +5 -0
- aicodestat-0.0.1.dist-info/top_level.txt +10 -0
- cli/__init__.py +2 -0
- cli/exporter.py +111 -0
- cli/main.py +213 -0
- cli/menus.py +540 -0
- cli/views.py +277 -0
- compute/__init__.py +2 -0
- compute/cache.py +90 -0
- compute/diff_engine.py +69 -0
- compute/lcs_engine.py +73 -0
- compute/metrics_service.py +362 -0
- config.py +120 -0
- local_mcp_server.py +260 -0
- logging_config.py +68 -0
- main.py +164 -0
- mcp/__init__.py +2 -0
- mcp/agent_adapter.py +69 -0
- mcp/api_schemas.py +26 -0
- mcp/routes_after.py +121 -0
- mcp/routes_before.py +68 -0
- mcp/routes_tools.py +100 -0
- service_manager.py +221 -0
- storage/__init__.py +2 -0
- storage/backup.py +185 -0
- storage/db.py +156 -0
- storage/models.py +338 -0
- storage/scheduler.py +111 -0
- utils/__init__.py +2 -0
- utils/port_utils.py +59 -0
- utils/time_utils.py +37 -0
storage/db.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""数据库连接管理、建表/迁移初始化逻辑"""
|
|
2
|
+
import sqlite3
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from config import get_database_config
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Database:
|
|
12
|
+
"""数据库连接管理类"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
15
|
+
"""
|
|
16
|
+
初始化数据库连接
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
db_path: 数据库文件路径,如果为None则从配置读取
|
|
20
|
+
"""
|
|
21
|
+
if db_path is None:
|
|
22
|
+
config = get_database_config()
|
|
23
|
+
db_path = config["path"]
|
|
24
|
+
|
|
25
|
+
self.db_path = Path(db_path)
|
|
26
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
self._connection: Optional[sqlite3.Connection] = None
|
|
28
|
+
|
|
29
|
+
def connect(self) -> sqlite3.Connection:
|
|
30
|
+
"""获取数据库连接(单例模式)"""
|
|
31
|
+
if self._connection is None:
|
|
32
|
+
self._connection = sqlite3.connect(
|
|
33
|
+
str(self.db_path),
|
|
34
|
+
check_same_thread=False
|
|
35
|
+
)
|
|
36
|
+
self._connection.row_factory = sqlite3.Row
|
|
37
|
+
# 启用外键约束
|
|
38
|
+
self._connection.execute("PRAGMA foreign_keys = ON")
|
|
39
|
+
return self._connection
|
|
40
|
+
|
|
41
|
+
def close(self):
|
|
42
|
+
"""关闭数据库连接"""
|
|
43
|
+
if self._connection:
|
|
44
|
+
self._connection.close()
|
|
45
|
+
self._connection = None
|
|
46
|
+
|
|
47
|
+
def initialize(self):
|
|
48
|
+
"""初始化数据库,创建所有表"""
|
|
49
|
+
conn = self.connect()
|
|
50
|
+
cursor = conn.cursor()
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# 1. 临时表:存储编辑前的完整代码
|
|
54
|
+
cursor.execute("""
|
|
55
|
+
CREATE TABLE IF NOT EXISTS temp_before_edit (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
session_id TEXT NOT NULL,
|
|
58
|
+
file_path TEXT NOT NULL,
|
|
59
|
+
code_before TEXT NOT NULL,
|
|
60
|
+
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
61
|
+
UNIQUE(session_id, file_path)
|
|
62
|
+
)
|
|
63
|
+
""")
|
|
64
|
+
|
|
65
|
+
# 2. 会话汇总表
|
|
66
|
+
cursor.execute("""
|
|
67
|
+
CREATE TABLE IF NOT EXISTS session_summary (
|
|
68
|
+
session_id TEXT NOT NULL,
|
|
69
|
+
file_path TEXT NOT NULL,
|
|
70
|
+
add_lines_count INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
modify_lines_count INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
total_lines_after INTEGER NOT NULL,
|
|
73
|
+
session_info TEXT,
|
|
74
|
+
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
75
|
+
PRIMARY KEY (session_id, file_path)
|
|
76
|
+
)
|
|
77
|
+
""")
|
|
78
|
+
|
|
79
|
+
# 创建索引
|
|
80
|
+
cursor.execute("""
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_session_id
|
|
82
|
+
ON session_summary(session_id)
|
|
83
|
+
""")
|
|
84
|
+
cursor.execute("""
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_file_path
|
|
86
|
+
ON session_summary(file_path)
|
|
87
|
+
""")
|
|
88
|
+
|
|
89
|
+
# 3. 差异行明细表
|
|
90
|
+
cursor.execute("""
|
|
91
|
+
CREATE TABLE IF NOT EXISTS code_diff_lines (
|
|
92
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
93
|
+
session_id TEXT NOT NULL,
|
|
94
|
+
file_path TEXT NOT NULL,
|
|
95
|
+
diff_type TEXT NOT NULL CHECK (diff_type IN ('add', 'modify')),
|
|
96
|
+
line_content TEXT NOT NULL,
|
|
97
|
+
line_number INTEGER NOT NULL,
|
|
98
|
+
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
99
|
+
FOREIGN KEY (session_id, file_path)
|
|
100
|
+
REFERENCES session_summary(session_id, file_path)
|
|
101
|
+
ON DELETE CASCADE
|
|
102
|
+
)
|
|
103
|
+
""")
|
|
104
|
+
|
|
105
|
+
# 创建索引
|
|
106
|
+
cursor.execute("""
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_diff_session_file
|
|
108
|
+
ON code_diff_lines(session_id, file_path)
|
|
109
|
+
""")
|
|
110
|
+
|
|
111
|
+
# 4. 数据备份记录表
|
|
112
|
+
cursor.execute("""
|
|
113
|
+
CREATE TABLE IF NOT EXISTS backup_record (
|
|
114
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
|
+
backup_path TEXT NOT NULL,
|
|
116
|
+
backup_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
117
|
+
backup_size INTEGER NOT NULL
|
|
118
|
+
)
|
|
119
|
+
""")
|
|
120
|
+
|
|
121
|
+
conn.commit()
|
|
122
|
+
logger.info(f"Database initialized successfully at {self.db_path}")
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
conn.rollback()
|
|
126
|
+
logger.error(f"Failed to initialize database: {e}")
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
def execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor:
|
|
130
|
+
"""执行SQL语句"""
|
|
131
|
+
conn = self.connect()
|
|
132
|
+
return conn.execute(sql, params)
|
|
133
|
+
|
|
134
|
+
def commit(self):
|
|
135
|
+
"""提交事务"""
|
|
136
|
+
if self._connection:
|
|
137
|
+
self._connection.commit()
|
|
138
|
+
|
|
139
|
+
def rollback(self):
|
|
140
|
+
"""回滚事务"""
|
|
141
|
+
if self._connection:
|
|
142
|
+
self._connection.rollback()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# 全局数据库实例
|
|
146
|
+
_db_instance: Optional[Database] = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_db() -> Database:
|
|
150
|
+
"""获取全局数据库实例(单例模式)"""
|
|
151
|
+
global _db_instance
|
|
152
|
+
if _db_instance is None:
|
|
153
|
+
_db_instance = Database()
|
|
154
|
+
_db_instance.initialize()
|
|
155
|
+
return _db_instance
|
|
156
|
+
|
storage/models.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""面向业务的DAO函数"""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Optional, Dict, Any
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from storage.db import get_db
|
|
6
|
+
from utils.time_utils import get_current_time
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def save_before_edit(session_id: str, file_path: str, code_before: str) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
保存编辑前的代码到临时表
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
session_id: 会话ID
|
|
17
|
+
file_path: 文件路径
|
|
18
|
+
code_before: 编辑前的代码内容
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
是否成功
|
|
22
|
+
"""
|
|
23
|
+
db = get_db()
|
|
24
|
+
try:
|
|
25
|
+
cursor = db.execute("""
|
|
26
|
+
INSERT OR REPLACE INTO temp_before_edit
|
|
27
|
+
(session_id, file_path, code_before, create_time)
|
|
28
|
+
VALUES (?, ?, ?, ?)
|
|
29
|
+
""", (session_id, file_path, code_before, get_current_time()))
|
|
30
|
+
db.commit()
|
|
31
|
+
logger.debug(f"Saved before_edit for session={session_id}, file={file_path}")
|
|
32
|
+
return True
|
|
33
|
+
except Exception as e:
|
|
34
|
+
logger.error(f"Failed to save before_edit: {e}")
|
|
35
|
+
db.rollback()
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_before_edit(session_id: str, file_path: str) -> Optional[str]:
|
|
40
|
+
"""
|
|
41
|
+
获取编辑前的代码
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
session_id: 会话ID
|
|
45
|
+
file_path: 文件路径
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
编辑前的代码内容,如果不存在则返回None
|
|
49
|
+
"""
|
|
50
|
+
db = get_db()
|
|
51
|
+
try:
|
|
52
|
+
cursor = db.execute("""
|
|
53
|
+
SELECT code_before FROM temp_before_edit
|
|
54
|
+
WHERE session_id = ? AND file_path = ?
|
|
55
|
+
""", (session_id, file_path))
|
|
56
|
+
row = cursor.fetchone()
|
|
57
|
+
if row:
|
|
58
|
+
return row[0]
|
|
59
|
+
return None
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"Failed to get before_edit: {e}")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def delete_before_edit(session_id: str, file_path: str) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
删除编辑前的代码记录
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
session_id: 会话ID
|
|
71
|
+
file_path: 文件路径
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
是否成功
|
|
75
|
+
"""
|
|
76
|
+
db = get_db()
|
|
77
|
+
try:
|
|
78
|
+
db.execute("""
|
|
79
|
+
DELETE FROM temp_before_edit
|
|
80
|
+
WHERE session_id = ? AND file_path = ?
|
|
81
|
+
""", (session_id, file_path))
|
|
82
|
+
db.commit()
|
|
83
|
+
logger.debug(f"Deleted before_edit for session={session_id}, file={file_path}")
|
|
84
|
+
return True
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Failed to delete before_edit: {e}")
|
|
87
|
+
db.rollback()
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def save_session_summary(
|
|
92
|
+
session_id: str,
|
|
93
|
+
file_path: str,
|
|
94
|
+
add_lines_count: int,
|
|
95
|
+
modify_lines_count: int,
|
|
96
|
+
total_lines_after: int,
|
|
97
|
+
session_info: Optional[str] = None
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
保存会话汇总信息
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
session_id: 会话ID
|
|
104
|
+
file_path: 文件路径
|
|
105
|
+
add_lines_count: 新增行数
|
|
106
|
+
modify_lines_count: 修改行数
|
|
107
|
+
total_lines_after: 编辑后文件总行数
|
|
108
|
+
session_info: 会话补充信息(可选)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
是否成功
|
|
112
|
+
"""
|
|
113
|
+
db = get_db()
|
|
114
|
+
try:
|
|
115
|
+
db.execute("""
|
|
116
|
+
INSERT OR REPLACE INTO session_summary
|
|
117
|
+
(session_id, file_path, add_lines_count, modify_lines_count,
|
|
118
|
+
total_lines_after, session_info, create_time)
|
|
119
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
120
|
+
""", (
|
|
121
|
+
session_id, file_path, add_lines_count, modify_lines_count,
|
|
122
|
+
total_lines_after, session_info, get_current_time()
|
|
123
|
+
))
|
|
124
|
+
db.commit()
|
|
125
|
+
logger.debug(f"Saved session_summary for session={session_id}, file={file_path}")
|
|
126
|
+
return True
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to save session_summary: {e}")
|
|
129
|
+
db.rollback()
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def save_code_diff_lines(
|
|
134
|
+
session_id: str,
|
|
135
|
+
file_path: str,
|
|
136
|
+
diff_lines: List[Dict[str, Any]]
|
|
137
|
+
) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
批量保存差异行明细
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
session_id: 会话ID
|
|
143
|
+
file_path: 文件路径
|
|
144
|
+
diff_lines: 差异行列表,每个元素包含 diff_type, line_content, line_number
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
是否成功
|
|
148
|
+
"""
|
|
149
|
+
if not diff_lines:
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
db = get_db()
|
|
153
|
+
try:
|
|
154
|
+
cursor = db.connect().cursor()
|
|
155
|
+
cursor.executemany("""
|
|
156
|
+
INSERT INTO code_diff_lines
|
|
157
|
+
(session_id, file_path, diff_type, line_content, line_number, create_time)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
159
|
+
""", [
|
|
160
|
+
(
|
|
161
|
+
session_id,
|
|
162
|
+
file_path,
|
|
163
|
+
diff_line["diff_type"],
|
|
164
|
+
diff_line["line_content"],
|
|
165
|
+
diff_line["line_number"],
|
|
166
|
+
get_current_time()
|
|
167
|
+
)
|
|
168
|
+
for diff_line in diff_lines
|
|
169
|
+
])
|
|
170
|
+
db.commit()
|
|
171
|
+
logger.debug(f"Saved {len(diff_lines)} diff_lines for session={session_id}, file={file_path}")
|
|
172
|
+
return True
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Failed to save code_diff_lines: {e}")
|
|
175
|
+
db.rollback()
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_session_summaries(
|
|
180
|
+
session_id: Optional[str] = None,
|
|
181
|
+
file_path: Optional[str] = None,
|
|
182
|
+
start_time: Optional[datetime] = None,
|
|
183
|
+
end_time: Optional[datetime] = None
|
|
184
|
+
) -> List[Dict[str, Any]]:
|
|
185
|
+
"""
|
|
186
|
+
查询会话汇总信息
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
session_id: 会话ID(可选,用于过滤)
|
|
190
|
+
file_path: 文件路径(可选,用于过滤)
|
|
191
|
+
start_time: 开始时间(可选)
|
|
192
|
+
end_time: 结束时间(可选)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
会话汇总列表
|
|
196
|
+
"""
|
|
197
|
+
db = get_db()
|
|
198
|
+
try:
|
|
199
|
+
conditions = []
|
|
200
|
+
params = []
|
|
201
|
+
|
|
202
|
+
if session_id:
|
|
203
|
+
conditions.append("session_id = ?")
|
|
204
|
+
params.append(session_id)
|
|
205
|
+
|
|
206
|
+
if file_path:
|
|
207
|
+
conditions.append("file_path = ?")
|
|
208
|
+
params.append(file_path)
|
|
209
|
+
|
|
210
|
+
if start_time:
|
|
211
|
+
conditions.append("create_time >= ?")
|
|
212
|
+
params.append(start_time)
|
|
213
|
+
|
|
214
|
+
if end_time:
|
|
215
|
+
conditions.append("create_time <= ?")
|
|
216
|
+
params.append(end_time)
|
|
217
|
+
|
|
218
|
+
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
|
|
219
|
+
|
|
220
|
+
cursor = db.execute(f"""
|
|
221
|
+
SELECT session_id, file_path, add_lines_count, modify_lines_count,
|
|
222
|
+
total_lines_after, session_info, create_time
|
|
223
|
+
FROM session_summary
|
|
224
|
+
{where_clause}
|
|
225
|
+
ORDER BY create_time DESC
|
|
226
|
+
""", tuple(params))
|
|
227
|
+
|
|
228
|
+
rows = cursor.fetchall()
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
"session_id": row[0],
|
|
232
|
+
"file_path": row[1],
|
|
233
|
+
"add_lines_count": row[2],
|
|
234
|
+
"modify_lines_count": row[3],
|
|
235
|
+
"total_lines_after": row[4],
|
|
236
|
+
"session_info": row[5],
|
|
237
|
+
"create_time": row[6]
|
|
238
|
+
}
|
|
239
|
+
for row in rows
|
|
240
|
+
]
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Failed to get session_summaries: {e}")
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_code_diff_lines(
|
|
247
|
+
session_id: Optional[str] = None,
|
|
248
|
+
file_path: Optional[str] = None
|
|
249
|
+
) -> List[Dict[str, Any]]:
|
|
250
|
+
"""
|
|
251
|
+
查询差异行明细
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
session_id: 会话ID(可选)
|
|
255
|
+
file_path: 文件路径(可选)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
差异行列表
|
|
259
|
+
"""
|
|
260
|
+
db = get_db()
|
|
261
|
+
try:
|
|
262
|
+
conditions = []
|
|
263
|
+
params = []
|
|
264
|
+
|
|
265
|
+
if session_id:
|
|
266
|
+
conditions.append("session_id = ?")
|
|
267
|
+
params.append(session_id)
|
|
268
|
+
|
|
269
|
+
if file_path:
|
|
270
|
+
conditions.append("file_path = ?")
|
|
271
|
+
params.append(file_path)
|
|
272
|
+
|
|
273
|
+
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
|
|
274
|
+
|
|
275
|
+
cursor = db.execute(f"""
|
|
276
|
+
SELECT id, session_id, file_path, diff_type, line_content, line_number, create_time
|
|
277
|
+
FROM code_diff_lines
|
|
278
|
+
{where_clause}
|
|
279
|
+
ORDER BY line_number ASC
|
|
280
|
+
""", tuple(params))
|
|
281
|
+
|
|
282
|
+
rows = cursor.fetchall()
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
"id": row[0],
|
|
286
|
+
"session_id": row[1],
|
|
287
|
+
"file_path": row[2],
|
|
288
|
+
"diff_type": row[3],
|
|
289
|
+
"line_content": row[4],
|
|
290
|
+
"line_number": row[5],
|
|
291
|
+
"create_time": row[6]
|
|
292
|
+
}
|
|
293
|
+
for row in rows
|
|
294
|
+
]
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Failed to get code_diff_lines: {e}")
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def delete_sessions(
|
|
301
|
+
session_ids: Optional[List[str]] = None,
|
|
302
|
+
before_time: Optional[datetime] = None
|
|
303
|
+
) -> int:
|
|
304
|
+
"""
|
|
305
|
+
删除会话数据(级联删除差异行)
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
session_ids: 会话ID列表(可选)
|
|
309
|
+
before_time: 删除指定时间之前的数据(可选)
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
删除的记录数
|
|
313
|
+
"""
|
|
314
|
+
db = get_db()
|
|
315
|
+
try:
|
|
316
|
+
if session_ids:
|
|
317
|
+
placeholders = ",".join("?" * len(session_ids))
|
|
318
|
+
cursor = db.execute(f"""
|
|
319
|
+
DELETE FROM session_summary
|
|
320
|
+
WHERE session_id IN ({placeholders})
|
|
321
|
+
""", tuple(session_ids))
|
|
322
|
+
elif before_time:
|
|
323
|
+
cursor = db.execute("""
|
|
324
|
+
DELETE FROM session_summary
|
|
325
|
+
WHERE create_time < ?
|
|
326
|
+
""", (before_time,))
|
|
327
|
+
else:
|
|
328
|
+
return 0
|
|
329
|
+
|
|
330
|
+
deleted_count = cursor.rowcount
|
|
331
|
+
db.commit()
|
|
332
|
+
logger.info(f"Deleted {deleted_count} session records")
|
|
333
|
+
return deleted_count
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Failed to delete sessions: {e}")
|
|
336
|
+
db.rollback()
|
|
337
|
+
return 0
|
|
338
|
+
|
storage/scheduler.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""定时任务:自动备份和清理"""
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from storage.backup import backup_database
|
|
8
|
+
from storage.models import delete_sessions
|
|
9
|
+
from config import get_database_config
|
|
10
|
+
from utils.time_utils import get_time_range_days
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Scheduler:
|
|
16
|
+
"""定时任务调度器"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._running = False
|
|
20
|
+
self._thread: Optional[threading.Thread] = None
|
|
21
|
+
self._stop_event = threading.Event()
|
|
22
|
+
|
|
23
|
+
def start(self):
|
|
24
|
+
"""启动定时任务"""
|
|
25
|
+
if self._running:
|
|
26
|
+
logger.warning("Scheduler is already running")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
self._running = True
|
|
30
|
+
self._stop_event.clear()
|
|
31
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
32
|
+
self._thread.start()
|
|
33
|
+
logger.info("Scheduler started")
|
|
34
|
+
|
|
35
|
+
def stop(self):
|
|
36
|
+
"""停止定时任务"""
|
|
37
|
+
if not self._running:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
self._running = False
|
|
41
|
+
self._stop_event.set()
|
|
42
|
+
if self._thread:
|
|
43
|
+
self._thread.join(timeout=5)
|
|
44
|
+
logger.info("Scheduler stopped")
|
|
45
|
+
|
|
46
|
+
def _run(self):
|
|
47
|
+
"""定时任务主循环"""
|
|
48
|
+
# 计算到下一个凌晨的时间
|
|
49
|
+
now = datetime.now()
|
|
50
|
+
next_backup_time = (now + timedelta(days=1)).replace(hour=2, minute=0, second=0, microsecond=0)
|
|
51
|
+
wait_seconds = (next_backup_time - now).total_seconds()
|
|
52
|
+
|
|
53
|
+
logger.info(f"Next backup scheduled at {next_backup_time}, waiting {wait_seconds:.0f} seconds")
|
|
54
|
+
|
|
55
|
+
while self._running and not self._stop_event.is_set():
|
|
56
|
+
# 等待到下一个备份时间或停止信号
|
|
57
|
+
if self._stop_event.wait(timeout=min(wait_seconds, 3600)): # 最多等待1小时检查一次
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
now = datetime.now()
|
|
61
|
+
if now >= next_backup_time:
|
|
62
|
+
try:
|
|
63
|
+
# 执行备份
|
|
64
|
+
logger.info("Starting scheduled backup...")
|
|
65
|
+
backup_path = backup_database()
|
|
66
|
+
if backup_path:
|
|
67
|
+
logger.info(f"Scheduled backup completed: {backup_path}")
|
|
68
|
+
else:
|
|
69
|
+
logger.error("Scheduled backup failed")
|
|
70
|
+
|
|
71
|
+
# 执行清理
|
|
72
|
+
config = get_database_config()
|
|
73
|
+
clean_cycle = config.get("clean_cycle", 30)
|
|
74
|
+
logger.info(f"Starting scheduled cleanup (removing data older than {clean_cycle} days)...")
|
|
75
|
+
_, before_time = get_time_range_days(clean_cycle)
|
|
76
|
+
deleted = delete_sessions(before_time=before_time)
|
|
77
|
+
logger.info(f"Scheduled cleanup completed: {deleted} records deleted")
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Scheduled task error: {e}", exc_info=True)
|
|
81
|
+
|
|
82
|
+
# 计算下一个备份时间(明天凌晨2点)
|
|
83
|
+
next_backup_time = (now + timedelta(days=1)).replace(hour=2, minute=0, second=0, microsecond=0)
|
|
84
|
+
wait_seconds = (next_backup_time - now).total_seconds()
|
|
85
|
+
logger.info(f"Next backup scheduled at {next_backup_time}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# 全局调度器实例
|
|
89
|
+
_scheduler: Optional[Scheduler] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_scheduler() -> Scheduler:
|
|
93
|
+
"""获取全局调度器实例"""
|
|
94
|
+
global _scheduler
|
|
95
|
+
if _scheduler is None:
|
|
96
|
+
_scheduler = Scheduler()
|
|
97
|
+
return _scheduler
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def start_scheduler():
|
|
101
|
+
"""启动定时任务"""
|
|
102
|
+
scheduler = get_scheduler()
|
|
103
|
+
scheduler.start()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def stop_scheduler():
|
|
107
|
+
"""停止定时任务"""
|
|
108
|
+
global _scheduler
|
|
109
|
+
if _scheduler:
|
|
110
|
+
_scheduler.stop()
|
|
111
|
+
|
utils/__init__.py
ADDED
utils/port_utils.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Port utility functions for checking port availability"""
|
|
2
|
+
import socket
|
|
3
|
+
import random
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_port_available(host: str, port: int) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check if a port is available
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
host: Host address
|
|
15
|
+
port: Port number
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
True if port is available, False otherwise
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
22
|
+
s.settimeout(1)
|
|
23
|
+
result = s.connect_ex((host, port))
|
|
24
|
+
return result != 0 # 0 means port is in use
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.warning(f"Error checking port {port}: {e}")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_available_port(host: str, preferred_port: int, max_attempts: int = 100) -> int:
|
|
31
|
+
"""
|
|
32
|
+
Find an available port, starting with preferred port
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
host: Host address
|
|
36
|
+
preferred_port: Preferred port number
|
|
37
|
+
max_attempts: Maximum attempts to find available port
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Available port number
|
|
41
|
+
"""
|
|
42
|
+
# First try preferred port
|
|
43
|
+
if is_port_available(host, preferred_port):
|
|
44
|
+
logger.info(f"Preferred port {preferred_port} is available")
|
|
45
|
+
return preferred_port
|
|
46
|
+
|
|
47
|
+
logger.warning(f"Preferred port {preferred_port} is in use, searching for alternative...")
|
|
48
|
+
|
|
49
|
+
# Try random ports in a reasonable range (49152-65535 for dynamic/private ports)
|
|
50
|
+
for _ in range(max_attempts):
|
|
51
|
+
# Use a range that's less likely to conflict
|
|
52
|
+
random_port = random.randint(49152, 65535)
|
|
53
|
+
if is_port_available(host, random_port):
|
|
54
|
+
logger.info(f"Found available port: {random_port}")
|
|
55
|
+
return random_port
|
|
56
|
+
|
|
57
|
+
# If no port found, raise exception
|
|
58
|
+
raise RuntimeError(f"Could not find an available port after {max_attempts} attempts")
|
|
59
|
+
|
utils/time_utils.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""时间、日期、格式化工具"""
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_current_time() -> datetime:
|
|
7
|
+
"""获取当前时间"""
|
|
8
|
+
return datetime.now()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_datetime(dt: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
12
|
+
"""格式化datetime为字符串"""
|
|
13
|
+
return dt.strftime(fmt)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_datetime(dt_str: str, fmt: str = "%Y-%m-%d %H:%M:%S") -> Optional[datetime]:
|
|
17
|
+
"""解析字符串为datetime"""
|
|
18
|
+
try:
|
|
19
|
+
return datetime.strptime(dt_str, fmt)
|
|
20
|
+
except ValueError:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_time_range_days(days: int) -> Tuple[datetime, datetime]:
|
|
25
|
+
"""获取指定天数前到现在的时间范围"""
|
|
26
|
+
end_time = get_current_time()
|
|
27
|
+
start_time = end_time - timedelta(days=days)
|
|
28
|
+
return start_time, end_time
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_expired(create_time: datetime, days: int) -> bool:
|
|
32
|
+
"""判断创建时间是否已过期(超过指定天数)"""
|
|
33
|
+
if create_time is None:
|
|
34
|
+
return True
|
|
35
|
+
expire_time = get_current_time() - timedelta(days=days)
|
|
36
|
+
return create_time < expire_time
|
|
37
|
+
|