307 lines
11 KiB
Python
307 lines
11 KiB
Python
"""
|
|
项目管理服务
|
|
管理项目信息,包括项目名、本地目录、分类文件夹初始化、商品信息等
|
|
"""
|
|
|
|
import json
|
|
import uuid
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional
|
|
from dataclasses import dataclass, asdict
|
|
from datetime import datetime
|
|
|
|
from python_core.config import settings
|
|
from python_core.utils.logger import logger
|
|
from python_core.utils.jsonrpc import create_response_handler
|
|
from python_core.services.resource_category_manager import resource_category_manager
|
|
|
|
|
|
@dataclass
|
|
class Project:
|
|
"""项目数据结构"""
|
|
id: str
|
|
name: str # 项目名
|
|
local_directory: str # 本地目录路径
|
|
product_name: str # 商品名
|
|
product_image: str # 商品图片路径
|
|
created_at: str
|
|
updated_at: str
|
|
is_active: bool = True
|
|
|
|
|
|
class ProjectManager:
|
|
"""项目管理器"""
|
|
|
|
def __init__(self):
|
|
self.cache_dir = settings.temp_dir / "cache"
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 项目数据文件
|
|
self.projects_file = self.cache_dir / "projects.json"
|
|
self.projects = self._load_projects()
|
|
|
|
def _load_projects(self) -> List[Project]:
|
|
"""加载项目数据"""
|
|
if self.projects_file.exists():
|
|
try:
|
|
with open(self.projects_file, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
return [Project(**item) for item in data]
|
|
except Exception as e:
|
|
logger.error(f"Failed to load projects: {e}")
|
|
return []
|
|
else:
|
|
return []
|
|
|
|
def _save_projects(self, projects: List[Project] = None):
|
|
"""保存项目数据"""
|
|
if projects is None:
|
|
projects = self.projects
|
|
|
|
try:
|
|
data = [asdict(project) for project in projects]
|
|
with open(self.projects_file, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
logger.info(f"Projects saved to {self.projects_file}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to save projects: {e}")
|
|
raise
|
|
|
|
def _initialize_category_folders(self, project_directory: str) -> bool:
|
|
"""为项目初始化分类文件夹"""
|
|
try:
|
|
project_path = Path(project_directory)
|
|
if not project_path.exists():
|
|
project_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 获取所有启用的分类
|
|
active_categories = resource_category_manager.get_all_categories()
|
|
|
|
for category in active_categories:
|
|
if category.get('is_active', True): # 只为启用的分类创建文件夹
|
|
category_folder = project_path / category['title']
|
|
category_folder.mkdir(exist_ok=True)
|
|
logger.info(f"Created category folder: {category_folder}")
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize category folders: {e}")
|
|
return False
|
|
|
|
def get_all_projects(self) -> List[Dict]:
|
|
"""获取所有项目"""
|
|
return [asdict(project) for project in self.projects if project.is_active]
|
|
|
|
def get_project_by_id(self, project_id: str) -> Optional[Dict]:
|
|
"""根据ID获取项目"""
|
|
for project in self.projects:
|
|
if project.id == project_id and project.is_active:
|
|
return asdict(project)
|
|
return None
|
|
|
|
def create_project(self, name: str, local_directory: str, product_name: str = "",
|
|
product_image: str = "") -> Dict:
|
|
"""创建新项目"""
|
|
now = datetime.now().isoformat()
|
|
|
|
# 验证目录路径
|
|
if not os.path.isabs(local_directory):
|
|
raise ValueError("Local directory must be an absolute path")
|
|
|
|
new_project = Project(
|
|
id=str(uuid.uuid4()),
|
|
name=name,
|
|
local_directory=local_directory,
|
|
product_name=product_name,
|
|
product_image=product_image,
|
|
created_at=now,
|
|
updated_at=now
|
|
)
|
|
|
|
# 初始化分类文件夹
|
|
if not self._initialize_category_folders(local_directory):
|
|
raise RuntimeError("Failed to initialize category folders")
|
|
|
|
self.projects.append(new_project)
|
|
self._save_projects()
|
|
|
|
logger.info(f"Created new project: {name}")
|
|
return asdict(new_project)
|
|
|
|
def update_project(self, project_id: str, name: str = None, local_directory: str = None,
|
|
product_name: str = None, product_image: str = None) -> Optional[Dict]:
|
|
"""更新项目"""
|
|
for i, project in enumerate(self.projects):
|
|
if project.id == project_id and project.is_active:
|
|
if name is not None:
|
|
project.name = name
|
|
if local_directory is not None:
|
|
# 验证新目录路径
|
|
if not os.path.isabs(local_directory):
|
|
raise ValueError("Local directory must be an absolute path")
|
|
project.local_directory = local_directory
|
|
if product_name is not None:
|
|
project.product_name = product_name
|
|
if product_image is not None:
|
|
project.product_image = product_image
|
|
project.updated_at = datetime.now().isoformat()
|
|
|
|
self.projects[i] = project
|
|
self._save_projects()
|
|
|
|
logger.info(f"Updated project: {project_id}")
|
|
return asdict(project)
|
|
return None
|
|
|
|
def delete_project(self, project_id: str) -> bool:
|
|
"""删除项目(软删除)"""
|
|
for i, project in enumerate(self.projects):
|
|
if project.id == project_id and project.is_active:
|
|
project.is_active = False
|
|
project.updated_at = datetime.now().isoformat()
|
|
|
|
self.projects[i] = project
|
|
self._save_projects()
|
|
|
|
logger.info(f"Deleted project: {project_id}")
|
|
return True
|
|
return False
|
|
|
|
def search_projects(self, keyword: str) -> List[Dict]:
|
|
"""搜索项目"""
|
|
keyword = keyword.lower()
|
|
results = []
|
|
|
|
for project in self.projects:
|
|
if (project.is_active and
|
|
(keyword in project.name.lower() or
|
|
keyword in project.product_name.lower())):
|
|
results.append(asdict(project))
|
|
|
|
return results
|
|
|
|
def open_project_directory(self, project_id: str) -> bool:
|
|
"""打开项目目录"""
|
|
project = self.get_project_by_id(project_id)
|
|
if project and os.path.exists(project['local_directory']):
|
|
try:
|
|
import subprocess
|
|
import platform
|
|
|
|
directory = project['local_directory']
|
|
system = platform.system()
|
|
|
|
if system == "Windows":
|
|
subprocess.run(['explorer', directory])
|
|
elif system == "Darwin": # macOS
|
|
subprocess.run(['open', directory])
|
|
else: # Linux
|
|
subprocess.run(['xdg-open', directory])
|
|
|
|
logger.info(f"Opened project directory: {directory}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Failed to open directory: {e}")
|
|
return False
|
|
return False
|
|
|
|
|
|
# 全局实例
|
|
project_manager = ProjectManager()
|
|
|
|
|
|
def main():
|
|
"""命令行接口 - 使用JSON-RPC协议"""
|
|
import sys
|
|
import json
|
|
|
|
# 创建响应处理器
|
|
rpc = create_response_handler()
|
|
|
|
if len(sys.argv) < 2:
|
|
rpc.error("INVALID_REQUEST", "No command specified")
|
|
return
|
|
|
|
command = sys.argv[1]
|
|
|
|
try:
|
|
if command == "get_all_projects":
|
|
projects = project_manager.get_all_projects()
|
|
rpc.success(projects)
|
|
|
|
elif command == "get_project_by_id":
|
|
if len(sys.argv) < 3:
|
|
rpc.error("INVALID_REQUEST", "Project ID required")
|
|
return
|
|
project_id = sys.argv[2]
|
|
project = project_manager.get_project_by_id(project_id)
|
|
if project:
|
|
rpc.success(project)
|
|
else:
|
|
rpc.error("NOT_FOUND", "Project not found")
|
|
|
|
elif command == "create_project":
|
|
if len(sys.argv) < 4:
|
|
rpc.error("INVALID_REQUEST", "Name and directory required")
|
|
return
|
|
name = sys.argv[2]
|
|
local_directory = sys.argv[3]
|
|
product_name = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
product_image = sys.argv[5] if len(sys.argv) > 5 else ""
|
|
result = project_manager.create_project(name, local_directory, product_name, product_image)
|
|
rpc.success(result)
|
|
|
|
elif command == "update_project":
|
|
if len(sys.argv) < 4:
|
|
rpc.error("INVALID_REQUEST", "Project ID and update data required")
|
|
return
|
|
project_id = sys.argv[2]
|
|
update_data = json.loads(sys.argv[3])
|
|
result = project_manager.update_project(
|
|
project_id,
|
|
update_data.get('name'),
|
|
update_data.get('local_directory'),
|
|
update_data.get('product_name'),
|
|
update_data.get('product_image')
|
|
)
|
|
if result:
|
|
rpc.success(result)
|
|
else:
|
|
rpc.error("NOT_FOUND", "Project not found")
|
|
|
|
elif command == "delete_project":
|
|
if len(sys.argv) < 3:
|
|
rpc.error("INVALID_REQUEST", "Project ID required")
|
|
return
|
|
project_id = sys.argv[2]
|
|
success = project_manager.delete_project(project_id)
|
|
rpc.success(success)
|
|
|
|
elif command == "search_projects":
|
|
if len(sys.argv) < 3:
|
|
rpc.error("INVALID_REQUEST", "Search keyword required")
|
|
return
|
|
keyword = sys.argv[2]
|
|
results = project_manager.search_projects(keyword)
|
|
rpc.success(results)
|
|
|
|
elif command == "open_project_directory":
|
|
if len(sys.argv) < 3:
|
|
rpc.error("INVALID_REQUEST", "Project ID required")
|
|
return
|
|
project_id = sys.argv[2]
|
|
success = project_manager.open_project_directory(project_id)
|
|
rpc.success(success)
|
|
|
|
else:
|
|
rpc.error("INVALID_REQUEST", f"Unknown command: {command}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Command execution failed: {e}")
|
|
rpc.error("INTERNAL_ERROR", str(e))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |