450 lines
13 KiB
Python
450 lines
13 KiB
Python
"""
|
||
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)
|