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
mcp/routes_after.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""RecordAfterEdit 相关 FastAPI 路由,调用差异提取与存储层"""
|
|
2
|
+
import logging
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from mcp.api_schemas import RecordAfterEditRequest, MCPResponse
|
|
5
|
+
from storage.models import (
|
|
6
|
+
get_before_edit,
|
|
7
|
+
delete_before_edit,
|
|
8
|
+
save_session_summary,
|
|
9
|
+
save_code_diff_lines
|
|
10
|
+
)
|
|
11
|
+
from compute.diff_engine import extract_diff_lines
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/mcp", tags=["MCP Tools"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("/record_after", response_model=MCPResponse)
|
|
19
|
+
async def record_after_edit(request: RecordAfterEditRequest):
|
|
20
|
+
"""
|
|
21
|
+
RecordAfterEdit Tool: 记录文件编辑后的代码,提取差异行并存储
|
|
22
|
+
|
|
23
|
+
该Tool由Agent在编辑文件后调用,会:
|
|
24
|
+
1. 从临时表读取编辑前的代码
|
|
25
|
+
2. 计算差异行(新增/修改)
|
|
26
|
+
3. 存储到会话汇总表和差异行明细表
|
|
27
|
+
4. 清理临时数据
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
# 参数校验
|
|
31
|
+
if not request.session_id or not request.session_id.strip():
|
|
32
|
+
raise HTTPException(
|
|
33
|
+
status_code=400,
|
|
34
|
+
detail="session_id不能为空"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if not request.file_path or not request.file_path.strip():
|
|
38
|
+
raise HTTPException(
|
|
39
|
+
status_code=400,
|
|
40
|
+
detail="file_path不能为空"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not request.code_after:
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=400,
|
|
46
|
+
detail="code_after不能为空"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 获取编辑前的代码
|
|
50
|
+
code_before = get_before_edit(request.session_id, request.file_path)
|
|
51
|
+
if code_before is None:
|
|
52
|
+
raise HTTPException(
|
|
53
|
+
status_code=404,
|
|
54
|
+
detail=f"未找到编辑前的代码记录,请先调用RecordBeforeEdit Tool (session_id={request.session_id}, file_path={request.file_path})"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 提取差异行
|
|
58
|
+
diff_lines = extract_diff_lines(code_before, request.code_after)
|
|
59
|
+
|
|
60
|
+
# 统计新增和修改行数
|
|
61
|
+
add_lines_count = sum(1 for d in diff_lines if d["diff_type"] == "add")
|
|
62
|
+
modify_lines_count = sum(1 for d in diff_lines if d["diff_type"] == "modify")
|
|
63
|
+
|
|
64
|
+
# 计算编辑后文件总行数
|
|
65
|
+
total_lines_after = len(request.code_after.split("\n"))
|
|
66
|
+
|
|
67
|
+
# 保存会话汇总
|
|
68
|
+
success = save_session_summary(
|
|
69
|
+
session_id=request.session_id,
|
|
70
|
+
file_path=request.file_path,
|
|
71
|
+
add_lines_count=add_lines_count,
|
|
72
|
+
modify_lines_count=modify_lines_count,
|
|
73
|
+
total_lines_after=total_lines_after,
|
|
74
|
+
session_info=request.session_info
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if not success:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=500,
|
|
80
|
+
detail="保存会话汇总失败"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# 保存差异行明细
|
|
84
|
+
if diff_lines:
|
|
85
|
+
success = save_code_diff_lines(
|
|
86
|
+
session_id=request.session_id,
|
|
87
|
+
file_path=request.file_path,
|
|
88
|
+
diff_lines=diff_lines
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not success:
|
|
92
|
+
logger.warning("保存差异行明细失败,但会话汇总已保存")
|
|
93
|
+
|
|
94
|
+
# 清理临时数据
|
|
95
|
+
delete_before_edit(request.session_id, request.file_path)
|
|
96
|
+
|
|
97
|
+
logger.info(
|
|
98
|
+
f"RecordAfterEdit: session={request.session_id}, file={request.file_path}, "
|
|
99
|
+
f"add={add_lines_count}, modify={modify_lines_count}, total_diff={len(diff_lines)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return MCPResponse(
|
|
103
|
+
status="success",
|
|
104
|
+
message="编辑后代码记录成功,差异行已提取并存储",
|
|
105
|
+
data={
|
|
106
|
+
"add_lines_count": add_lines_count,
|
|
107
|
+
"modify_lines_count": modify_lines_count,
|
|
108
|
+
"total_diff_lines": len(diff_lines),
|
|
109
|
+
"total_lines_after": total_lines_after
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except HTTPException:
|
|
114
|
+
raise
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error(f"RecordAfterEdit error: {e}", exc_info=True)
|
|
117
|
+
raise HTTPException(
|
|
118
|
+
status_code=500,
|
|
119
|
+
detail=f"内部错误: {str(e)}"
|
|
120
|
+
)
|
|
121
|
+
|
mcp/routes_before.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""RecordBeforeEdit 相关 FastAPI 路由与业务逻辑"""
|
|
2
|
+
import logging
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from mcp.api_schemas import RecordBeforeEditRequest, MCPResponse
|
|
5
|
+
from storage.models import save_before_edit
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/mcp", tags=["MCP Tools"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.post("/record_before", response_model=MCPResponse)
|
|
13
|
+
async def record_before_edit(request: RecordBeforeEditRequest):
|
|
14
|
+
"""
|
|
15
|
+
RecordBeforeEdit Tool: 记录文件编辑前的代码
|
|
16
|
+
|
|
17
|
+
该Tool由Agent在编辑文件前调用,用于临时存储编辑前的代码内容,
|
|
18
|
+
后续RecordAfterEdit Tool会使用此数据进行差异计算。
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# 参数校验
|
|
22
|
+
if not request.session_id or not request.session_id.strip():
|
|
23
|
+
raise HTTPException(
|
|
24
|
+
status_code=400,
|
|
25
|
+
detail="session_id不能为空"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if not request.file_path or not request.file_path.strip():
|
|
29
|
+
raise HTTPException(
|
|
30
|
+
status_code=400,
|
|
31
|
+
detail="file_path不能为空"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if not request.code_before:
|
|
35
|
+
raise HTTPException(
|
|
36
|
+
status_code=400,
|
|
37
|
+
detail="code_before不能为空"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# 保存编辑前代码
|
|
41
|
+
success = save_before_edit(
|
|
42
|
+
session_id=request.session_id,
|
|
43
|
+
file_path=request.file_path,
|
|
44
|
+
code_before=request.code_before
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if not success:
|
|
48
|
+
raise HTTPException(
|
|
49
|
+
status_code=500,
|
|
50
|
+
detail="保存编辑前代码失败"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger.info(f"RecordBeforeEdit: session={request.session_id}, file={request.file_path}")
|
|
54
|
+
|
|
55
|
+
return MCPResponse(
|
|
56
|
+
status="success",
|
|
57
|
+
message="编辑前代码记录成功"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
except HTTPException:
|
|
61
|
+
raise
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"RecordBeforeEdit error: {e}", exc_info=True)
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=500,
|
|
66
|
+
detail=f"内部错误: {str(e)}"
|
|
67
|
+
)
|
|
68
|
+
|
mcp/routes_tools.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""MCP Tool发现端点,返回可用工具列表"""
|
|
2
|
+
from fastapi import APIRouter
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
|
|
5
|
+
router = APIRouter(prefix="/mcp", tags=["MCP Tools"])
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@router.get("/tools", response_model=Dict[str, Any])
|
|
9
|
+
async def list_tools():
|
|
10
|
+
"""
|
|
11
|
+
MCP Tool发现端点
|
|
12
|
+
|
|
13
|
+
返回所有可用的MCP Tools列表,供Agent自动发现和注册
|
|
14
|
+
"""
|
|
15
|
+
tools = [
|
|
16
|
+
{
|
|
17
|
+
"name": "RecordBeforeEdit",
|
|
18
|
+
"description": "记录文件编辑前的完整代码内容(仅本地临时存储,后续自动清理,不冗余保留)",
|
|
19
|
+
"schema_version": "v1",
|
|
20
|
+
"capabilities": ["read"],
|
|
21
|
+
"endpoint": "/mcp/record_before",
|
|
22
|
+
"method": "POST",
|
|
23
|
+
"actions": [
|
|
24
|
+
{
|
|
25
|
+
"name": "record_before",
|
|
26
|
+
"description": "记录文件编辑前的代码,用于后续对比差异",
|
|
27
|
+
"parameters": [
|
|
28
|
+
{
|
|
29
|
+
"name": "session_id",
|
|
30
|
+
"type": "string",
|
|
31
|
+
"required": True,
|
|
32
|
+
"description": "当前会话ID,由Agent生成,需与RecordAfterEdit的session_id一致,用于关联同一编辑操作"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "file_path",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"required": True,
|
|
38
|
+
"description": "目标文件的绝对路径(如/Users/xxx/project/test.py),用于关联文件"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "code_before",
|
|
42
|
+
"type": "string",
|
|
43
|
+
"required": True,
|
|
44
|
+
"description": "文件编辑前的完整代码内容(保留原始格式、空行,确保行号精准)"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "RecordAfterEdit",
|
|
52
|
+
"description": "记录文件编辑后的完整代码,提取具体差异行(新增/修改)及行号,清理临时数据,仅保留差异信息",
|
|
53
|
+
"schema_version": "v1",
|
|
54
|
+
"capabilities": ["write"],
|
|
55
|
+
"endpoint": "/mcp/record_after",
|
|
56
|
+
"method": "POST",
|
|
57
|
+
"actions": [
|
|
58
|
+
{
|
|
59
|
+
"name": "record_after_and_calc_diff",
|
|
60
|
+
"description": "记录编辑后代码,计算并存储具体差异行,清理编辑前的完整代码",
|
|
61
|
+
"parameters": [
|
|
62
|
+
{
|
|
63
|
+
"name": "session_id",
|
|
64
|
+
"type": "string",
|
|
65
|
+
"required": True,
|
|
66
|
+
"description": "当前会话ID,需与RecordBeforeEdit的session_id一致,用于关联同一编辑操作"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "file_path",
|
|
70
|
+
"type": "string",
|
|
71
|
+
"required": True,
|
|
72
|
+
"description": "目标文件的绝对路径,需与RecordBeforeEdit的file_path一致"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "code_after",
|
|
76
|
+
"type": "string",
|
|
77
|
+
"required": True,
|
|
78
|
+
"description": "文件编辑后的完整代码内容(保留原始格式、空行,确保行号精准)"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "session_info",
|
|
82
|
+
"type": "string",
|
|
83
|
+
"required": False,
|
|
84
|
+
"description": "会话补充信息(如用户指令、Agent类型、操作时间),用于后续统计筛选"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"version": "1.0.0",
|
|
94
|
+
"tools": tools,
|
|
95
|
+
"server_info": {
|
|
96
|
+
"name": "本地MCP Server AI代码统计系统",
|
|
97
|
+
"description": "基于MCP协议的AI代码统计工具"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
service_manager.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""MCP服务进程管理器"""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
import signal
|
|
6
|
+
import time
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
# 添加项目根目录到Python路径
|
|
13
|
+
_project_root = Path(__file__).parent
|
|
14
|
+
if str(_project_root) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_project_root))
|
|
16
|
+
|
|
17
|
+
from config import get_server_config
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ServiceManager:
|
|
23
|
+
"""MCP服务进程管理器"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, pid_file: Optional[str] = None):
|
|
26
|
+
"""
|
|
27
|
+
初始化服务管理器
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
pid_file: PID文件路径,用于存储进程ID
|
|
31
|
+
"""
|
|
32
|
+
if pid_file is None:
|
|
33
|
+
# 默认PID文件路径
|
|
34
|
+
pid_file = Path.home() / ".local-mcp-server" / "server.pid"
|
|
35
|
+
|
|
36
|
+
self.pid_file = Path(pid_file)
|
|
37
|
+
self.pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
self._process: Optional[subprocess.Popen] = None
|
|
39
|
+
|
|
40
|
+
def get_pid(self) -> Optional[int]:
|
|
41
|
+
"""从PID文件读取进程ID"""
|
|
42
|
+
if not self.pid_file.exists():
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
with open(self.pid_file, 'r') as f:
|
|
47
|
+
pid = int(f.read().strip())
|
|
48
|
+
return pid
|
|
49
|
+
except (ValueError, IOError) as e:
|
|
50
|
+
logger.warning(f"Failed to read PID file: {e}")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def is_running(self) -> bool:
|
|
54
|
+
"""检查服务是否正在运行"""
|
|
55
|
+
pid = self.get_pid()
|
|
56
|
+
if pid is None:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# 检查进程是否存在
|
|
61
|
+
os.kill(pid, 0)
|
|
62
|
+
return True
|
|
63
|
+
except (OSError, ProcessLookupError):
|
|
64
|
+
# 进程不存在,清理PID文件
|
|
65
|
+
if self.pid_file.exists():
|
|
66
|
+
self.pid_file.unlink()
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def get_status(self) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
获取服务状态
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
状态字典,包含 running, pid, port, host 等信息
|
|
75
|
+
"""
|
|
76
|
+
server_config = get_server_config()
|
|
77
|
+
status = {
|
|
78
|
+
"running": False,
|
|
79
|
+
"pid": None,
|
|
80
|
+
"host": server_config["host"],
|
|
81
|
+
"port": server_config["port"],
|
|
82
|
+
"pid_file": str(self.pid_file)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if self.is_running():
|
|
86
|
+
status["running"] = True
|
|
87
|
+
status["pid"] = self.get_pid()
|
|
88
|
+
|
|
89
|
+
return status
|
|
90
|
+
|
|
91
|
+
def start(self, background: bool = True) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
启动MCP服务
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
background: 是否后台运行
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
是否成功启动
|
|
100
|
+
"""
|
|
101
|
+
if self.is_running():
|
|
102
|
+
logger.warning("Service is already running")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
# 获取服务器配置
|
|
107
|
+
server_config = get_server_config()
|
|
108
|
+
host = server_config["host"]
|
|
109
|
+
port = server_config["port"]
|
|
110
|
+
|
|
111
|
+
# 构建启动命令
|
|
112
|
+
script_path = Path(__file__).parent / "local_mcp_server.py"
|
|
113
|
+
cmd = [sys.executable, str(script_path), "start", "--host", host, "--port", str(port)]
|
|
114
|
+
|
|
115
|
+
if background:
|
|
116
|
+
# 后台运行
|
|
117
|
+
if sys.platform == "win32":
|
|
118
|
+
# Windows: 使用CREATE_NEW_PROCESS_GROUP和DETACHED_PROCESS
|
|
119
|
+
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
|
120
|
+
process = subprocess.Popen(
|
|
121
|
+
cmd,
|
|
122
|
+
stdout=subprocess.DEVNULL,
|
|
123
|
+
stderr=subprocess.DEVNULL,
|
|
124
|
+
creationflags=creationflags,
|
|
125
|
+
start_new_session=True
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
# Unix: 使用nohup和后台运行
|
|
129
|
+
process = subprocess.Popen(
|
|
130
|
+
cmd,
|
|
131
|
+
stdout=subprocess.DEVNULL,
|
|
132
|
+
stderr=subprocess.DEVNULL,
|
|
133
|
+
start_new_session=True
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 等待一下确保进程启动
|
|
137
|
+
time.sleep(0.5)
|
|
138
|
+
|
|
139
|
+
if process.poll() is None:
|
|
140
|
+
# 进程仍在运行,保存PID
|
|
141
|
+
pid = process.pid
|
|
142
|
+
with open(self.pid_file, 'w') as f:
|
|
143
|
+
f.write(str(pid))
|
|
144
|
+
logger.info(f"MCP Server started in background (PID: {pid})")
|
|
145
|
+
return True
|
|
146
|
+
else:
|
|
147
|
+
logger.error("Failed to start MCP Server (process exited immediately)")
|
|
148
|
+
return False
|
|
149
|
+
else:
|
|
150
|
+
# 前台运行(用于测试)
|
|
151
|
+
self._process = subprocess.Popen(cmd)
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"Failed to start MCP Server: {e}", exc_info=True)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
def stop(self) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
停止MCP服务
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
是否成功停止
|
|
164
|
+
"""
|
|
165
|
+
pid = self.get_pid()
|
|
166
|
+
if pid is None:
|
|
167
|
+
logger.warning("Service is not running (no PID file)")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
if sys.platform == "win32":
|
|
172
|
+
# Windows: 使用taskkill
|
|
173
|
+
subprocess.run(
|
|
174
|
+
["taskkill", "/F", "/T", "/PID", str(pid)],
|
|
175
|
+
stdout=subprocess.DEVNULL,
|
|
176
|
+
stderr=subprocess.DEVNULL,
|
|
177
|
+
check=False
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
# Unix: 发送SIGTERM
|
|
181
|
+
os.kill(pid, signal.SIGTERM)
|
|
182
|
+
# 等待进程结束
|
|
183
|
+
try:
|
|
184
|
+
os.waitpid(pid, 0)
|
|
185
|
+
except ChildProcessError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
# 清理PID文件
|
|
189
|
+
if self.pid_file.exists():
|
|
190
|
+
self.pid_file.unlink()
|
|
191
|
+
|
|
192
|
+
logger.info(f"MCP Server stopped (PID: {pid})")
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
except (OSError, ProcessLookupError) as e:
|
|
196
|
+
logger.warning(f"Process {pid} not found: {e}")
|
|
197
|
+
# 清理PID文件
|
|
198
|
+
if self.pid_file.exists():
|
|
199
|
+
self.pid_file.unlink()
|
|
200
|
+
return False
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f"Failed to stop MCP Server: {e}", exc_info=True)
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
def restart(self) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
重启MCP服务
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
是否成功重启
|
|
211
|
+
"""
|
|
212
|
+
if self.is_running():
|
|
213
|
+
self.stop()
|
|
214
|
+
time.sleep(1)
|
|
215
|
+
return self.start()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_service_manager() -> ServiceManager:
|
|
219
|
+
"""获取全局服务管理器实例"""
|
|
220
|
+
return ServiceManager()
|
|
221
|
+
|
storage/__init__.py
ADDED
storage/backup.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""备份记录读写、导出/导入JSON实现"""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict, Any, Optional
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from storage.db import get_db
|
|
9
|
+
from storage.models import (
|
|
10
|
+
get_session_summaries,
|
|
11
|
+
get_code_diff_lines,
|
|
12
|
+
save_session_summary,
|
|
13
|
+
save_code_diff_lines
|
|
14
|
+
)
|
|
15
|
+
from utils.time_utils import get_current_time, format_datetime
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def backup_database(backup_path: Optional[str] = None) -> Optional[str]:
|
|
21
|
+
"""
|
|
22
|
+
备份数据库到JSON文件
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
backup_path: 备份文件路径(可选,如果为None则从配置读取)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
备份文件路径,如果失败则返回None
|
|
29
|
+
"""
|
|
30
|
+
from config import get_database_config
|
|
31
|
+
|
|
32
|
+
db = get_db()
|
|
33
|
+
config = get_database_config()
|
|
34
|
+
|
|
35
|
+
if backup_path is None:
|
|
36
|
+
backup_dir = Path(config["backup_path"])
|
|
37
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
timestamp = format_datetime(get_current_time(), "%Y%m%d_%H%M%S")
|
|
39
|
+
backup_path = str(backup_dir / f"backup_{timestamp}.json")
|
|
40
|
+
|
|
41
|
+
backup_file = Path(backup_path)
|
|
42
|
+
backup_file.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# 导出所有数据
|
|
46
|
+
summaries = get_session_summaries()
|
|
47
|
+
all_diff_lines = get_code_diff_lines()
|
|
48
|
+
|
|
49
|
+
# 组织数据
|
|
50
|
+
backup_data = {
|
|
51
|
+
"backup_time": format_datetime(get_current_time()),
|
|
52
|
+
"version": "1.0.0",
|
|
53
|
+
"summaries": summaries,
|
|
54
|
+
"diff_lines": all_diff_lines
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# 写入JSON文件
|
|
58
|
+
with open(backup_file, 'w', encoding='utf-8') as f:
|
|
59
|
+
json.dump(backup_data, f, ensure_ascii=False, indent=2)
|
|
60
|
+
|
|
61
|
+
# 记录备份信息
|
|
62
|
+
backup_size = backup_file.stat().st_size // 1024 # KB
|
|
63
|
+
record_backup(str(backup_file), backup_size)
|
|
64
|
+
|
|
65
|
+
logger.info(f"Database backed up to {backup_file} ({backup_size} KB)")
|
|
66
|
+
return str(backup_file)
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Failed to backup database: {e}")
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def restore_database(backup_path: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
从JSON文件恢复数据库
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
backup_path: 备份文件路径
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
是否成功
|
|
82
|
+
"""
|
|
83
|
+
backup_file = Path(backup_path)
|
|
84
|
+
|
|
85
|
+
if not backup_file.exists():
|
|
86
|
+
logger.error(f"Backup file not found: {backup_path}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# 读取备份数据
|
|
91
|
+
with open(backup_file, 'r', encoding='utf-8') as f:
|
|
92
|
+
backup_data = json.load(f)
|
|
93
|
+
|
|
94
|
+
# 清空现有数据(可选,根据需求决定)
|
|
95
|
+
# 这里我们选择追加模式,不删除现有数据
|
|
96
|
+
|
|
97
|
+
# 恢复会话汇总
|
|
98
|
+
summaries = backup_data.get("summaries", [])
|
|
99
|
+
for summary in summaries:
|
|
100
|
+
save_session_summary(
|
|
101
|
+
session_id=summary["session_id"],
|
|
102
|
+
file_path=summary["file_path"],
|
|
103
|
+
add_lines_count=summary["add_lines_count"],
|
|
104
|
+
modify_lines_count=summary["modify_lines_count"],
|
|
105
|
+
total_lines_after=summary["total_lines_after"],
|
|
106
|
+
session_info=summary.get("session_info")
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 恢复差异行
|
|
110
|
+
diff_lines = backup_data.get("diff_lines", [])
|
|
111
|
+
# 按session_id和file_path分组
|
|
112
|
+
grouped = {}
|
|
113
|
+
for diff_line in diff_lines:
|
|
114
|
+
key = (diff_line["session_id"], diff_line["file_path"])
|
|
115
|
+
if key not in grouped:
|
|
116
|
+
grouped[key] = []
|
|
117
|
+
grouped[key].append({
|
|
118
|
+
"diff_type": diff_line["diff_type"],
|
|
119
|
+
"line_content": diff_line["line_content"],
|
|
120
|
+
"line_number": diff_line["line_number"]
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
for (session_id, file_path), lines in grouped.items():
|
|
124
|
+
save_code_diff_lines(session_id, file_path, lines)
|
|
125
|
+
|
|
126
|
+
logger.info(f"Database restored from {backup_path}")
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Failed to restore database: {e}")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def record_backup(backup_path: str, backup_size: int):
|
|
135
|
+
"""
|
|
136
|
+
记录备份操作到数据库
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
backup_path: 备份文件路径
|
|
140
|
+
backup_size: 备份文件大小(KB)
|
|
141
|
+
"""
|
|
142
|
+
db = get_db()
|
|
143
|
+
try:
|
|
144
|
+
db.execute("""
|
|
145
|
+
INSERT INTO backup_record (backup_path, backup_time, backup_size)
|
|
146
|
+
VALUES (?, ?, ?)
|
|
147
|
+
""", (backup_path, get_current_time(), backup_size))
|
|
148
|
+
db.commit()
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Failed to record backup: {e}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_backup_records(limit: int = 10) -> List[Dict[str, Any]]:
|
|
154
|
+
"""
|
|
155
|
+
获取备份记录列表
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
limit: 返回记录数限制
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
备份记录列表
|
|
162
|
+
"""
|
|
163
|
+
db = get_db()
|
|
164
|
+
try:
|
|
165
|
+
cursor = db.execute("""
|
|
166
|
+
SELECT id, backup_path, backup_time, backup_size
|
|
167
|
+
FROM backup_record
|
|
168
|
+
ORDER BY backup_time DESC
|
|
169
|
+
LIMIT ?
|
|
170
|
+
""", (limit,))
|
|
171
|
+
|
|
172
|
+
rows = cursor.fetchall()
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
"id": row[0],
|
|
176
|
+
"backup_path": row[1],
|
|
177
|
+
"backup_time": row[2],
|
|
178
|
+
"backup_size": row[3]
|
|
179
|
+
}
|
|
180
|
+
for row in rows
|
|
181
|
+
]
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Failed to get backup records: {e}")
|
|
184
|
+
return []
|
|
185
|
+
|