""" Neural Sync 2.0 - Project Domain Service 项目域服务 - 管理动态项目域 每个域有独立的信号源、知识库和配置 """ import asyncio import logging from typing import Optional, Dict, List, Any from pydantic import BaseModel, Field from datetime import datetime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ..utils.timezone import now_cn_naive logger = logging.getLogger(__name__) class DomainConfig(BaseModel): """项目域配置""" signal_sources: List[str] = Field(default_factory=lambda: ["hackernews"]) ai_config: Dict[str, Any] = Field(default_factory=dict) audit_threshold: float = 98.0 auto_merge_enabled: bool = False webhook_urls: Dict[str, str] = Field(default_factory=dict) class ProjectDomainCreate(BaseModel): """创建项目域请求""" name: str slug: str description: Optional[str] = None config: Optional[DomainConfig] = None is_default: bool = False class ProjectDomainUpdate(BaseModel): """更新项目域请求""" name: Optional[str] = None description: Optional[str] = None config: Optional[DomainConfig] = None is_active: Optional[bool] = None is_default: Optional[bool] = None class ProjectDomainResponse(BaseModel): """项目域响应""" id: str name: str slug: str description: Optional[str] config: DomainConfig is_active: bool is_default: bool signal_count: int context_node_count: int experiment_count: int created_at: datetime updated_at: datetime last_synced_at: Optional[datetime] class DomainSyncProgress(BaseModel): """域同步进度""" domain_id: str phase: str # connecting, loading_knowledge, syncing_signals, complete progress: float # 0.0 - 1.0 message: str class ProjectDomainService: """ 项目域服务 功能: - CRUD 操作 - 域切换和知识同步 - 统计信息更新 - 默认域管理 """ def __init__(self, db_session: AsyncSession, event_bus=None): self.db = db_session self.event_bus = event_bus self._active_domain_id: Optional[str] = None async def create_domain(self, request: ProjectDomainCreate) -> ProjectDomainResponse: """创建新项目域""" from ..models import ProjectDomain import uuid # 检查 slug 唯一性 existing = await self.db.execute( select(ProjectDomain).where(ProjectDomain.slug == request.slug) ) if existing.scalar_one_or_none(): raise ValueError(f"Slug '{request.slug}' already exists") # 如果设置为默认域,取消其他域的默认状态 if request.is_default: await self._clear_default_domains() # 序列化配置 config_dict = request.config.model_dump() if request.config else {} domain = ProjectDomain( name=request.name, slug=request.slug, description=request.description, config=config_dict, is_default=request.is_default, ) self.db.add(domain) await self.db.commit() await self.db.refresh(domain) # 发布域创建事件 if self.event_bus: from ..events import EventType, Event await self.event_bus.publish( Event( type=EventType.DOMAIN_CREATED, data={ "domain_id": str(domain.id), "name": domain.name, "slug": domain.slug, }, ) ) return self._to_response(domain) async def get_domain(self, domain_id: str) -> Optional[ProjectDomainResponse]: """获取单个域""" from ..models import ProjectDomain import uuid try: domain_uuid = uuid.UUID(domain_id) except ValueError: return None result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.id == domain_uuid) ) domain = result.scalar_one_or_none() if not domain: return None return self._to_response(domain) async def get_domain_by_slug(self, slug: str) -> Optional[ProjectDomainResponse]: """通过 slug 获取域""" from ..models import ProjectDomain result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.slug == slug) ) domain = result.scalar_one_or_none() if not domain: return None return self._to_response(domain) async def list_domains( self, include_inactive: bool = False ) -> List[ProjectDomainResponse]: """列出所有域""" from ..models import ProjectDomain query = select(ProjectDomain) if not include_inactive: query = query.where(ProjectDomain.is_active == True) query = query.order_by(ProjectDomain.is_default.desc(), ProjectDomain.created_at.asc()) result = await self.db.execute(query) domains = result.scalars().all() return [self._to_response(d) for d in domains] async def update_domain( self, domain_id: str, request: ProjectDomainUpdate ) -> Optional[ProjectDomainResponse]: """更新域""" from ..models import ProjectDomain import uuid try: domain_uuid = uuid.UUID(domain_id) except ValueError: return None result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.id == domain_uuid) ) domain = result.scalar_one_or_none() if not domain: return None # 更新字段 if request.name is not None: domain.name = request.name if request.description is not None: domain.description = request.description if request.config is not None: domain.config = request.config.model_dump() if request.is_active is not None: domain.is_active = request.is_active if request.is_default is not None and request.is_default != domain.is_default: if request.is_default: await self._clear_default_domains() domain.is_default = request.is_default domain.updated_at = now_cn_naive() await self.db.commit() await self.db.refresh(domain) # 发布域更新事件 if self.event_bus: from ..events import EventType, Event await self.event_bus.publish( Event( type=EventType.DOMAIN_UPDATED, data={ "domain_id": str(domain.id), "name": domain.name, "slug": domain.slug, }, ) ) return self._to_response(domain) async def delete_domain(self, domain_id: str) -> bool: """删除域""" from ..models import ProjectDomain import uuid try: domain_uuid = uuid.UUID(domain_id) except ValueError: return False result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.id == domain_uuid) ) domain = result.scalar_one_or_none() if not domain: return False # 不允许删除默认域 if domain.is_default: raise ValueError("Cannot delete default domain") await self.db.delete(domain) await self.db.commit() # 发布域删除事件 if self.event_bus: from ..events import EventType, Event await self.event_bus.publish( Event( type=EventType.DOMAIN_DELETED, data={ "domain_id": str(domain.id), "name": domain.name, "slug": domain.slug, }, ) ) return True async def switch_domain(self, domain_id: str) -> DomainSyncProgress: """ 切换活动域 流程: 1. 验证域存在且激活 2. 开始知识同步(1.5s 动画) 3. 更新活动域 ID 4. 返回同步进度 """ from ..models import ProjectDomain import uuid try: domain_uuid = uuid.UUID(domain_id) except ValueError: raise ValueError("Invalid domain ID") result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.id == domain_uuid) ) domain = result.scalar_one_or_none() if not domain: raise ValueError("Domain not found") if not domain.is_active: raise ValueError("Domain is not active") # 更新最后同步时间 domain.last_synced_at = now_cn_naive() await self.db.commit() # 设置活动域 self._active_domain_id = str(domain.id) # 发布域切换事件 if self.event_bus: from ..events import EventType, Event await self.event_bus.publish( Event( type=EventType.DOMAIN_SWITCHED, data={ "domain_id": str(domain.id), "name": domain.name, "slug": domain.slug, }, ) ) # 模拟同步进度(1.5s) return DomainSyncProgress( domain_id=str(domain.id), phase="complete", progress=1.0, message=f"已切换到 {domain.name}", ) async def get_default_domain(self) -> Optional[ProjectDomainResponse]: """获取默认域""" from ..models import ProjectDomain result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.is_default == True) ) domain = result.scalar_one_or_none() if not domain: # 如果没有默认域,返回第一个激活域 result = await self.db.execute( select(ProjectDomain) .where(ProjectDomain.is_active == True) .order_by(ProjectDomain.created_at.asc()) .limit(1) ) domain = result.scalar_one_or_none() if not domain: return None return self._to_response(domain) async def update_domain_stats(self, domain_id: str) -> None: """更新域统计信息""" from ..models import ProjectDomain, Signal, ContextNode, Experiment import uuid try: domain_uuid = uuid.UUID(domain_id) except ValueError: return result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.id == domain_uuid) ) domain = result.scalar_one_or_none() if not domain: return # TODO: 需要添加域关联到 Signal, ContextNode, Experiment # 当前使用全局计数 signal_count = await self.db.execute(select(Signal)) domain.signal_count = len(signal_count.scalars().all()) context_count = await self.db.execute(select(ContextNode)) domain.context_node_count = len(context_count.scalars().all()) experiment_count = await self.db.execute(select(Experiment)) domain.experiment_count = len(experiment_count.scalars().all()) await self.db.commit() async def get_active_domain_id(self) -> Optional[str]: """获取当前活动域 ID""" if self._active_domain_id: return self._active_domain_id # 如果没有设置,返回默认域 default = await self.get_default_domain() if default: self._active_domain_id = default.id return default.id return None async def _clear_default_domains(self) -> None: """清除所有域的默认状态""" from ..models import ProjectDomain result = await self.db.execute( select(ProjectDomain).where(ProjectDomain.is_default == True) ) domains = result.scalars().all() for domain in domains: domain.is_default = False await self.db.commit() def _to_response(self, domain) -> ProjectDomainResponse: """转换为响应模型""" config = DomainConfig(**domain.config) if domain.config else DomainConfig() return ProjectDomainResponse( id=str(domain.id), name=domain.name, slug=domain.slug, description=domain.description, config=config, is_active=domain.is_active, is_default=domain.is_default, signal_count=domain.signal_count, context_node_count=domain.context_node_count, experiment_count=domain.experiment_count, created_at=domain.created_at, updated_at=domain.updated_at, last_synced_at=getattr(domain, 'last_synced_at', None), ) # 全局服务实例 _domain_service = None def get_domain_service(db_session: AsyncSession, event_bus=None) -> ProjectDomainService: """获取项目域服务实例""" return ProjectDomainService(db_session, event_bus)