diff --git a/api/routers.py b/api/routers.py index 8242400..2110c17 100644 --- a/api/routers.py +++ b/api/routers.py @@ -15,7 +15,7 @@ from domain.agent import api as agent from domain.virtual_fs import api as virtual_fs from domain.virtual_fs.mapping import s3_api, webdav_api from domain.virtual_fs.search import search_api -from domain.audit import router as audit +from domain.audit import api as audit def include_routers(app: FastAPI): @@ -37,4 +37,4 @@ def include_routers(app: FastAPI): app.include_router(s3_api.router) app.include_router(offline_downloads.router) app.include_router(email.router) - app.include_router(audit) + app.include_router(audit.router) diff --git a/db/session.py b/db/session.py index aa68310..0ae06b3 100644 --- a/db/session.py +++ b/db/session.py @@ -1,6 +1,6 @@ from tortoise import Tortoise -from domain.adapters.registry import runtime_registry +from domain.adapters import runtime_registry TORTOISE_ORM = { "connections": {"default": "sqlite://data/db/db.sqlite3"}, diff --git a/domain/__init__.py b/domain/__init__.py new file mode 100644 index 0000000..4ed5259 --- /dev/null +++ b/domain/__init__.py @@ -0,0 +1,7 @@ +""" +domain:业务域层 + +约定:跨包只从各子包 `__init__.py` 导入公开 API。 +""" + +__all__: list[str] = [] diff --git a/domain/adapters/__init__.py b/domain/adapters/__init__.py index 8b13789..ccbf7e5 100644 --- a/domain/adapters/__init__.py +++ b/domain/adapters/__init__.py @@ -1 +1,24 @@ +from .providers import BaseAdapter +from .registry import ( + RuntimeRegistry, + discover_adapters, + get_config_schema, + get_config_schemas, + normalize_adapter_type, + runtime_registry, +) +from .service import AdapterService +from .types import AdapterCreate, AdapterOut +__all__ = [ + "BaseAdapter", + "RuntimeRegistry", + "discover_adapters", + "get_config_schema", + "get_config_schemas", + "normalize_adapter_type", + "runtime_registry", + "AdapterService", + "AdapterCreate", + "AdapterOut", +] diff --git a/domain/adapters/api.py b/domain/adapters/api.py index 8c7410f..5daf156 100644 --- a/domain/adapters/api.py +++ b/domain/adapters/api.py @@ -4,10 +4,9 @@ from fastapi import APIRouter, Depends, Request from api.response import success from domain.audit import AuditAction, audit -from domain.adapters.service import AdapterService -from domain.adapters.types import AdapterCreate -from domain.auth.service import get_current_active_user -from domain.auth.types import User +from domain.auth import User, get_current_active_user +from .service import AdapterService +from .types import AdapterCreate router = APIRouter(prefix="/api/adapters", tags=["adapters"]) diff --git a/domain/adapters/registry.py b/domain/adapters/registry.py index 3296e6d..a76c0e0 100644 --- a/domain/adapters/registry.py +++ b/domain/adapters/registry.py @@ -4,7 +4,7 @@ from importlib import import_module from typing import Callable, Dict from models import StorageAdapter -from domain.adapters.providers.base import BaseAdapter +from .providers.base import BaseAdapter AdapterFactory = Callable[[StorageAdapter], BaseAdapter] @@ -21,7 +21,7 @@ def normalize_adapter_type(value: str | None) -> str | None: def discover_adapters(): """扫描 domain.adapters.providers 包, 自动注册适配器类型、工厂与配置 schema。""" - from domain.adapters import providers as adapters_pkg + from . import providers as adapters_pkg TYPE_MAP.clear() CONFIG_SCHEMAS.clear() diff --git a/domain/adapters/service.py b/domain/adapters/service.py index 377abf9..3e70b31 100644 --- a/domain/adapters/service.py +++ b/domain/adapters/service.py @@ -2,13 +2,13 @@ from typing import Optional from fastapi import HTTPException -from domain.adapters.registry import ( +from domain.auth import User +from .registry import ( get_config_schemas, normalize_adapter_type, runtime_registry, ) -from domain.adapters.types import AdapterCreate, AdapterOut -from domain.auth.types import User +from .types import AdapterCreate, AdapterOut from models import StorageAdapter diff --git a/domain/agent/__init__.py b/domain/agent/__init__.py index 703a4df..dbdebaf 100644 --- a/domain/agent/__init__.py +++ b/domain/agent/__init__.py @@ -1,4 +1,9 @@ -from .api import router - -__all__ = ["router"] +from .service import AgentService +from .types import AgentChatContext, AgentChatRequest, PendingToolCall +__all__ = [ + "AgentService", + "AgentChatContext", + "AgentChatRequest", + "PendingToolCall", +] diff --git a/domain/agent/api.py b/domain/agent/api.py index b87dacd..e8ae5cd 100644 --- a/domain/agent/api.py +++ b/domain/agent/api.py @@ -4,11 +4,10 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import StreamingResponse from api.response import success -from domain.agent.service import AgentService -from domain.agent.types import AgentChatRequest from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.auth.types import User +from domain.auth import User, get_current_active_user +from .service import AgentService +from .types import AgentChatRequest router = APIRouter(prefix="/api/agent", tags=["agent"]) diff --git a/domain/agent/service.py b/domain/agent/service.py index 30b3ce5..922b2b8 100644 --- a/domain/agent/service.py +++ b/domain/agent/service.py @@ -6,11 +6,10 @@ from typing import Any, Dict, List, Optional, Tuple import httpx from fastapi import HTTPException -from domain.agent.tools import get_tool, openai_tools, tool_result_to_content -from domain.agent.types import AgentChatRequest, PendingToolCall -from domain.ai.inference import MissingModelError, chat_completion, chat_completion_stream -from domain.ai.service import AIProviderService -from domain.auth.types import User +from domain.ai import AIProviderService, MissingModelError, chat_completion, chat_completion_stream +from domain.auth import User +from .tools import get_tool, openai_tools, tool_result_to_content +from .types import AgentChatRequest, PendingToolCall def _normalize_path(p: Optional[str]) -> Optional[str]: @@ -136,6 +135,11 @@ def _sse(event: str, data: Any) -> bytes: return f"event: {event}\ndata: {payload}\n\n".encode("utf-8") +def _format_exc(exc: BaseException) -> str: + text = str(exc) + return text if text else exc.__class__.__name__ + + class AgentService: @classmethod async def chat(cls, req: AgentChatRequest, user: Optional[User]) -> Dict[str, Any]: @@ -376,11 +380,11 @@ class AgentService: if isinstance(msg, dict): assistant_message = msg except MissingModelError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc + raise HTTPException(status_code=400, detail=_format_exc(exc)) from exc except httpx.HTTPStatusError as exc: - raise HTTPException(status_code=502, detail=f"对话请求失败: {exc}") from exc + raise HTTPException(status_code=502, detail=f"对话请求失败: {_format_exc(exc)}") from exc except httpx.RequestError as exc: - raise HTTPException(status_code=502, detail=f"对话请求异常: {exc}") from exc + raise HTTPException(status_code=502, detail=f"对话请求异常: {_format_exc(exc)}") from exc if not assistant_message: assistant_message = {"role": "assistant", "content": ""} @@ -446,3 +450,21 @@ class AgentService: except asyncio.CancelledError: return + except HTTPException as exc: + detail = exc.detail + content = detail if isinstance(detail, str) else str(detail) + if not content.strip(): + content = f"请求失败({exc.status_code})" + new_messages.append({"role": "assistant", "content": content}) + payload: Dict[str, Any] = {"messages": new_messages} + if pending: + payload["pending_tool_calls"] = [p.model_dump() for p in pending] + yield _sse("done", payload) + return + except Exception as exc: # noqa: BLE001 + new_messages.append({"role": "assistant", "content": f"服务端异常: {_format_exc(exc)}"}) + payload: Dict[str, Any] = {"messages": new_messages} + if pending: + payload["pending_tool_calls"] = [p.model_dump() for p in pending] + yield _sse("done", payload) + return diff --git a/domain/agent/tools.py b/domain/agent/tools.py index 53f060c..6a8a14e 100644 --- a/domain/agent/tools.py +++ b/domain/agent/tools.py @@ -2,10 +2,9 @@ import json from dataclasses import dataclass from typing import Any, Awaitable, Callable, Dict, List, Optional -from domain.processors.service import ProcessorService -from domain.processors.types import ProcessDirectoryRequest, ProcessRequest -from domain.virtual_fs.service import VirtualFSService -from domain.virtual_fs.search.search_service import VirtualFSSearchService +from domain.processors import ProcessDirectoryRequest, ProcessRequest, ProcessorService +from domain.virtual_fs import VirtualFSService +from domain.virtual_fs.search import VirtualFSSearchService @dataclass(frozen=True) diff --git a/domain/ai/__init__.py b/domain/ai/__init__.py index 31a6279..dffdb77 100644 --- a/domain/ai/__init__.py +++ b/domain/ai/__init__.py @@ -1,28 +1,61 @@ -from .api import router_ai, router_vector_db +from .inference import ( + MissingModelError, + chat_completion, + chat_completion_stream, + describe_image_base64, + get_text_embedding, + provider_service, + rerank_texts, +) from .service import ( AIProviderService, + FILE_COLLECTION_NAME, + VECTOR_COLLECTION_NAME, + DEFAULT_VECTOR_DIMENSION, VectorDBConfigManager, VectorDBService, - DEFAULT_VECTOR_DIMENSION, - ABILITIES, - normalize_capabilities, ) from .types import ( + ABILITIES, AIDefaultsUpdate, AIModelCreate, AIModelUpdate, AIProviderCreate, AIProviderUpdate, VectorDBConfigPayload, + normalize_capabilities, +) +from .vector_providers import ( + BaseVectorProvider, + MilvusLiteProvider, + MilvusServerProvider, + QdrantProvider, + get_provider_class, + get_provider_entry, + list_providers, ) __all__ = [ - "router_ai", - "router_vector_db", + "MissingModelError", + "chat_completion", + "chat_completion_stream", + "describe_image_base64", + "get_text_embedding", + "provider_service", + "rerank_texts", "AIProviderService", "VectorDBService", "VectorDBConfigManager", "DEFAULT_VECTOR_DIMENSION", + "VECTOR_COLLECTION_NAME", + "FILE_COLLECTION_NAME", + "BaseVectorProvider", + "MilvusLiteProvider", + "MilvusServerProvider", + "QdrantProvider", + "list_providers", + "get_provider_entry", + "get_provider_class", "ABILITIES", "normalize_capabilities", "AIDefaultsUpdate", diff --git a/domain/ai/api.py b/domain/ai/api.py index 17cd8d9..ee4c846 100644 --- a/domain/ai/api.py +++ b/domain/ai/api.py @@ -5,8 +5,9 @@ from fastapi import APIRouter, Depends, HTTPException, Path, Request from api.response import success from domain.audit import AuditAction, audit -from domain.ai.service import AIProviderService, VectorDBConfigManager, VectorDBService -from domain.ai.types import ( +from domain.auth import User, get_current_active_user +from .service import AIProviderService, VectorDBConfigManager, VectorDBService +from .types import ( AIDefaultsUpdate, AIModelCreate, AIModelUpdate, @@ -14,9 +15,7 @@ from domain.ai.types import ( AIProviderUpdate, VectorDBConfigPayload, ) -from domain.ai.vector_providers import get_provider_class, get_provider_entry, list_providers -from domain.auth.service import get_current_active_user -from domain.auth.types import User +from .vector_providers import get_provider_class, get_provider_entry, list_providers router_ai = APIRouter(prefix="/api/ai", tags=["ai"]) router_vector_db = APIRouter(prefix="/api/vector-db", tags=["vector-db"]) diff --git a/domain/ai/inference.py b/domain/ai/inference.py index 4399eb9..fa301cf 100644 --- a/domain/ai/inference.py +++ b/domain/ai/inference.py @@ -4,7 +4,7 @@ import httpx from typing import Any, AsyncIterator, Dict, List, Sequence, Tuple from models.database import AIModel, AIProvider -from domain.ai.service import AIProviderService +from .service import AIProviderService provider_service = AIProviderService diff --git a/domain/ai/service.py b/domain/ai/service.py index 9157894..0956d4e 100644 --- a/domain/ai/service.py +++ b/domain/ai/service.py @@ -7,7 +7,7 @@ import httpx from tortoise.exceptions import DoesNotExist from tortoise.transactions import in_transaction -from domain.config.service import ConfigService +from domain.config import ConfigService from models.database import AIDefaultModel, AIModel, AIProvider from .types import ABILITIES, normalize_capabilities diff --git a/domain/audit/__init__.py b/domain/audit/__init__.py index f6b435c..ce582f8 100644 --- a/domain/audit/__init__.py +++ b/domain/audit/__init__.py @@ -1,5 +1,4 @@ -from domain.audit.decorator import audit -from domain.audit.types import AuditAction -from domain.audit.api import router +from .decorator import audit +from .types import AuditAction -__all__ = ["audit", "AuditAction", "router"] +__all__ = ["audit", "AuditAction"] diff --git a/domain/audit/api.py b/domain/audit/api.py index ba7bfa8..30ffaeb 100644 --- a/domain/audit/api.py +++ b/domain/audit/api.py @@ -4,10 +4,9 @@ from typing import Annotated, Optional from fastapi import APIRouter, Depends, HTTPException, Query from api import response -from domain.audit.service import AuditService -from domain.audit.types import AuditAction -from domain.auth.service import get_current_active_user -from domain.auth.types import User +from domain.auth import User, get_current_active_user +from .service import AuditService +from .types import AuditAction CurrentUser = Annotated[User, Depends(get_current_active_user)] diff --git a/domain/audit/decorator.py b/domain/audit/decorator.py index 5559b09..596571b 100644 --- a/domain/audit/decorator.py +++ b/domain/audit/decorator.py @@ -7,11 +7,11 @@ import jwt from fastapi import Request from jwt.exceptions import InvalidTokenError -from domain.audit.service import AuditService -from domain.audit.types import AuditAction -from domain.auth.service import ALGORITHM -from domain.config.service import ConfigService +from domain.auth import ALGORITHM +from domain.config import ConfigService from models.database import UserAccount +from .service import AuditService +from .types import AuditAction def _extract_request(bound_args: Mapping[str, Any]) -> Request | None: diff --git a/domain/audit/service.py b/domain/audit/service.py index 33e4bae..58be1ba 100644 --- a/domain/audit/service.py +++ b/domain/audit/service.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Optional from models.database import AuditLog -from domain.audit.types import AuditAction +from .types import AuditAction class AuditService: diff --git a/domain/auth/__init__.py b/domain/auth/__init__.py new file mode 100644 index 0000000..ef7b1f2 --- /dev/null +++ b/domain/auth/__init__.py @@ -0,0 +1,49 @@ +from .service import ( + ALGORITHM, + AuthService, + authenticate_user_db, + create_access_token, + get_current_active_user, + get_current_user, + get_password_hash, + has_users, + register_user, + request_password_reset, + reset_password_with_token, + verify_password, + verify_password_reset_token, +) +from .types import ( + PasswordResetConfirm, + PasswordResetRequest, + RegisterRequest, + Token, + TokenData, + UpdateMeRequest, + User, + UserInDB, +) + +__all__ = [ + "ALGORITHM", + "AuthService", + "authenticate_user_db", + "create_access_token", + "get_current_active_user", + "get_current_user", + "get_password_hash", + "has_users", + "register_user", + "request_password_reset", + "reset_password_with_token", + "verify_password", + "verify_password_reset_token", + "PasswordResetConfirm", + "PasswordResetRequest", + "RegisterRequest", + "Token", + "TokenData", + "UpdateMeRequest", + "User", + "UserInDB", +] diff --git a/domain/auth/api.py b/domain/auth/api.py index 2afae86..49ef77c 100644 --- a/domain/auth/api.py +++ b/domain/auth/api.py @@ -5,8 +5,8 @@ from fastapi.security import OAuth2PasswordRequestForm from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import AuthService, get_current_active_user -from domain.auth.types import ( +from .service import AuthService, get_current_active_user +from .types import ( PasswordResetConfirm, PasswordResetRequest, RegisterRequest, diff --git a/domain/auth/service.py b/domain/auth/service.py index 0f783cf..4edb658 100644 --- a/domain/auth/service.py +++ b/domain/auth/service.py @@ -11,7 +11,9 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jwt.exceptions import InvalidTokenError -from domain.auth.types import ( +from domain.config import ConfigService +from models.database import UserAccount +from .types import ( PasswordResetConfirm, PasswordResetRequest, RegisterRequest, @@ -21,8 +23,6 @@ from domain.auth.types import ( User, UserInDB, ) -from models.database import UserAccount -from domain.config.service import ConfigService ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 365 @@ -324,7 +324,7 @@ class AuthService: @classmethod async def _send_password_reset_email(cls, user: UserAccount, token: str) -> None: - from domain.email.service import EmailService + from domain.email import EmailService app_domain = await ConfigService.get("APP_DOMAIN", None) base_url = (app_domain or "http://localhost:5173").rstrip("/") diff --git a/domain/backup/__init__.py b/domain/backup/__init__.py index 8b13789..a873502 100644 --- a/domain/backup/__init__.py +++ b/domain/backup/__init__.py @@ -1 +1,7 @@ +from .service import BackupService +from .types import BackupData +__all__ = [ + "BackupService", + "BackupData", +] diff --git a/domain/backup/api.py b/domain/backup/api.py index 1a00f8c..0b0b31f 100644 --- a/domain/backup/api.py +++ b/domain/backup/api.py @@ -4,8 +4,8 @@ from fastapi import APIRouter, Depends, File, Request, UploadFile from fastapi.responses import JSONResponse from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.backup.service import BackupService +from domain.auth import get_current_active_user +from .service import BackupService router = APIRouter( prefix="/api/backup", diff --git a/domain/backup/service.py b/domain/backup/service.py index 03c38a5..a953c63 100644 --- a/domain/backup/service.py +++ b/domain/backup/service.py @@ -4,8 +4,8 @@ from datetime import datetime from fastapi import HTTPException from tortoise.transactions import in_transaction -from domain.backup.types import BackupData -from domain.config.service import VERSION +from domain.config import VERSION +from .types import BackupData from models.database import ( AIDefaultModel, AIModel, diff --git a/domain/config/__init__.py b/domain/config/__init__.py new file mode 100644 index 0000000..c8f0c0b --- /dev/null +++ b/domain/config/__init__.py @@ -0,0 +1,10 @@ +from .service import ConfigService, VERSION +from .types import ConfigItem, LatestVersionInfo, SystemStatus + +__all__ = [ + "ConfigService", + "VERSION", + "ConfigItem", + "LatestVersionInfo", + "SystemStatus", +] diff --git a/domain/config/api.py b/domain/config/api.py index 7af0650..ca3b8f1 100644 --- a/domain/config/api.py +++ b/domain/config/api.py @@ -4,10 +4,9 @@ from fastapi import APIRouter, Depends, Form, Request from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.config.service import ConfigService -from domain.config.types import ConfigItem +from domain.auth import User, get_current_active_user +from .service import ConfigService +from .types import ConfigItem router = APIRouter(prefix="/api/config", tags=["config"]) diff --git a/domain/config/service.py b/domain/config/service.py index cc9b0b4..bd8aa04 100644 --- a/domain/config/service.py +++ b/domain/config/service.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional import httpx from dotenv import load_dotenv -from domain.config.types import LatestVersionInfo, SystemStatus +from .types import LatestVersionInfo, SystemStatus from models.database import Configuration, UserAccount load_dotenv(dotenv_path=".env") diff --git a/domain/email/__init__.py b/domain/email/__init__.py new file mode 100644 index 0000000..778cfc7 --- /dev/null +++ b/domain/email/__init__.py @@ -0,0 +1,20 @@ +from .service import EmailService, EmailTemplateRenderer +from .types import ( + EmailConfig, + EmailSecurity, + EmailSendPayload, + EmailTemplatePreviewPayload, + EmailTemplateUpdate, + EmailTestRequest, +) + +__all__ = [ + "EmailService", + "EmailTemplateRenderer", + "EmailConfig", + "EmailSecurity", + "EmailSendPayload", + "EmailTemplatePreviewPayload", + "EmailTemplateUpdate", + "EmailTestRequest", +] diff --git a/domain/email/api.py b/domain/email/api.py index 798a824..e105cf5 100644 --- a/domain/email/api.py +++ b/domain/email/api.py @@ -2,10 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException, Request from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.email.service import EmailService, EmailTemplateRenderer -from domain.email.types import ( +from domain.auth import User, get_current_active_user +from .service import EmailService, EmailTemplateRenderer +from .types import ( EmailTemplatePreviewPayload, EmailTemplateUpdate, EmailTestRequest, diff --git a/domain/email/service.py b/domain/email/service.py index c4f0074..ef14544 100644 --- a/domain/email/service.py +++ b/domain/email/service.py @@ -7,8 +7,8 @@ from pathlib import Path from string import Template from typing import Any, Dict, List, Optional -from domain.config.service import ConfigService -from domain.email.types import EmailConfig, EmailSecurity, EmailSendPayload +from domain.config import ConfigService +from .types import EmailConfig, EmailSecurity, EmailSendPayload class EmailTemplateRenderer: @@ -104,7 +104,7 @@ class EmailService: template: str, context: Optional[Dict[str, Any]] = None, ): - from domain.tasks.task_queue import TaskProgress, task_queue_service + from domain.tasks import TaskProgress, task_queue_service payload = EmailSendPayload( recipients=recipients, @@ -126,7 +126,7 @@ class EmailService: @classmethod async def send_from_task(cls, task_id: str, data: Dict[str, Any]): - from domain.tasks.task_queue import TaskProgress, task_queue_service + from domain.tasks import TaskProgress, task_queue_service payload = EmailSendPayload(**data) diff --git a/domain/offline_downloads/__init__.py b/domain/offline_downloads/__init__.py new file mode 100644 index 0000000..be398ac --- /dev/null +++ b/domain/offline_downloads/__init__.py @@ -0,0 +1,7 @@ +from .service import OfflineDownloadService +from .types import OfflineDownloadCreate + +__all__ = [ + "OfflineDownloadService", + "OfflineDownloadCreate", +] diff --git a/domain/offline_downloads/api.py b/domain/offline_downloads/api.py index caa544e..f07afdd 100644 --- a/domain/offline_downloads/api.py +++ b/domain/offline_downloads/api.py @@ -4,10 +4,9 @@ from fastapi import APIRouter, Depends, Request from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.offline_downloads.service import OfflineDownloadService -from domain.offline_downloads.types import OfflineDownloadCreate +from domain.auth import User, get_current_active_user +from .service import OfflineDownloadService +from .types import OfflineDownloadCreate CurrentUser = Annotated[User, Depends(get_current_active_user)] diff --git a/domain/offline_downloads/service.py b/domain/offline_downloads/service.py index a973816..63842c6 100644 --- a/domain/offline_downloads/service.py +++ b/domain/offline_downloads/service.py @@ -7,11 +7,10 @@ import aiofiles import aiohttp from fastapi import Depends, HTTPException -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.offline_downloads.types import OfflineDownloadCreate -from domain.virtual_fs.service import VirtualFSService -from domain.tasks.task_queue import Task, TaskProgress, task_queue_service +from domain.auth import User, get_current_active_user +from domain.tasks import Task, TaskProgress, task_queue_service +from domain.virtual_fs import VirtualFSService +from .types import OfflineDownloadCreate class OfflineDownloadService: diff --git a/domain/plugins/__init__.py b/domain/plugins/__init__.py index 0233f41..96d0cb0 100644 --- a/domain/plugins/__init__.py +++ b/domain/plugins/__init__.py @@ -4,9 +4,9 @@ Foxel 插件系统 提供 .foxpkg 插件包的安装、管理和运行时加载功能。 """ -from domain.plugins.loader import PluginLoader, PluginLoadError -from domain.plugins.service import PluginService -from domain.plugins.startup import init_plugins, load_installed_plugins +from .loader import PluginLoadError, PluginLoader +from .service import PluginService +from .startup import init_plugins, load_installed_plugins __all__ = [ "PluginLoader", diff --git a/domain/plugins/api.py b/domain/plugins/api.py index c3b5434..b849213 100644 --- a/domain/plugins/api.py +++ b/domain/plugins/api.py @@ -8,8 +8,8 @@ from fastapi import APIRouter, File, Request, UploadFile from fastapi.responses import FileResponse from domain.audit import AuditAction, audit -from domain.plugins.service import PluginService -from domain.plugins.types import ( +from .service import PluginService +from .types import ( PluginInstallResult, PluginOut, ) diff --git a/domain/plugins/loader.py b/domain/plugins/loader.py index 366000c..425bbd5 100644 --- a/domain/plugins/loader.py +++ b/domain/plugins/loader.py @@ -20,7 +20,7 @@ from typing import Any, Dict, List, Optional, Tuple from fastapi import APIRouter -from domain.plugins.types import ( +from .types import ( ManifestProcessorConfig, ManifestRouteConfig, PluginManifest, @@ -344,7 +344,7 @@ class PluginLoader: supported_exts = getattr(module, "SUPPORTED_EXTS", []) # 注册到处理器注册表 - from domain.processors.registry import CONFIG_SCHEMAS, TYPE_MAP + from domain.processors import CONFIG_SCHEMAS, TYPE_MAP processor_type = processor_config.type TYPE_MAP[processor_type] = factory @@ -401,7 +401,7 @@ class PluginLoader: """ # 卸载处理器 if manifest and manifest.backend and manifest.backend.processors: - from domain.processors.registry import CONFIG_SCHEMAS, TYPE_MAP + from domain.processors import CONFIG_SCHEMAS, TYPE_MAP for proc_config in manifest.backend.processors: proc_type = proc_config.type diff --git a/domain/plugins/service.py b/domain/plugins/service.py index e8b242b..5bce591 100644 --- a/domain/plugins/service.py +++ b/domain/plugins/service.py @@ -12,8 +12,8 @@ from typing import List, Optional, Union from fastapi import HTTPException -from domain.plugins.loader import PluginLoadError, PluginLoader -from domain.plugins.types import ( +from .loader import PluginLoadError, PluginLoader +from .types import ( PluginInstallResult, PluginManifest, PluginOut, diff --git a/domain/plugins/startup.py b/domain/plugins/startup.py index 55192fa..5499e28 100644 --- a/domain/plugins/startup.py +++ b/domain/plugins/startup.py @@ -7,8 +7,8 @@ import logging from typing import TYPE_CHECKING, List, Tuple -from domain.plugins.loader import PluginLoadError, PluginLoader -from domain.plugins.types import PluginManifest +from .loader import PluginLoadError, PluginLoader +from .types import PluginManifest if TYPE_CHECKING: from fastapi import FastAPI @@ -113,4 +113,3 @@ async def init_plugins(app: "FastAPI") -> None: logger.warning(f" - {error}") else: logger.info(f"插件加载完成,共 {loaded_count} 个插件") - diff --git a/domain/processors/__init__.py b/domain/processors/__init__.py new file mode 100644 index 0000000..a960040 --- /dev/null +++ b/domain/processors/__init__.py @@ -0,0 +1,35 @@ +from .base import BaseProcessor +from .registry import ( + CONFIG_SCHEMAS, + TYPE_MAP, + get_config_schema, + get_config_schemas, + get_last_discovery_errors, + get_module_path, + reload_processors, +) +from .service import ( + ProcessorService, + get_processor, + list_processors, + reload_processor_modules, +) +from .types import ProcessDirectoryRequest, ProcessRequest, UpdateSourceRequest + +__all__ = [ + "BaseProcessor", + "CONFIG_SCHEMAS", + "TYPE_MAP", + "get_config_schema", + "get_config_schemas", + "get_last_discovery_errors", + "get_module_path", + "reload_processors", + "ProcessorService", + "get_processor", + "list_processors", + "reload_processor_modules", + "ProcessDirectoryRequest", + "ProcessRequest", + "UpdateSourceRequest", +] diff --git a/domain/processors/api.py b/domain/processors/api.py index 53e5869..6cafe8d 100644 --- a/domain/processors/api.py +++ b/domain/processors/api.py @@ -4,10 +4,9 @@ from fastapi import APIRouter, Body, Depends, Request from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.processors.service import ProcessorService -from domain.processors.types import ( +from domain.auth import User, get_current_active_user +from .service import ProcessorService +from .types import ( ProcessDirectoryRequest, ProcessRequest, UpdateSourceRequest, diff --git a/domain/processors/builtin/vector_index.py b/domain/processors/builtin/vector_index.py index f5ba2b1..edc9e0c 100644 --- a/domain/processors/builtin/vector_index.py +++ b/domain/processors/builtin/vector_index.py @@ -8,12 +8,14 @@ from fastapi.responses import Response from PIL import Image from ..base import BaseProcessor -from domain.ai.inference import describe_image_base64, get_text_embedding, provider_service -from domain.ai.service import ( - VectorDBService, +from domain.ai import ( DEFAULT_VECTOR_DIMENSION, - VECTOR_COLLECTION_NAME, FILE_COLLECTION_NAME, + VECTOR_COLLECTION_NAME, + VectorDBService, + describe_image_base64, + get_text_embedding, + provider_service, ) diff --git a/domain/processors/registry.py b/domain/processors/registry.py index 43e0aa1..d2812ac 100644 --- a/domain/processors/registry.py +++ b/domain/processors/registry.py @@ -5,7 +5,7 @@ from pathlib import Path from types import ModuleType from typing import Callable, Dict, Optional -from domain.processors.base import BaseProcessor +from .base import BaseProcessor ProcessorFactory = Callable[[], BaseProcessor] TYPE_MAP: Dict[str, ProcessorFactory] = {} @@ -16,7 +16,7 @@ LAST_DISCOVERY_ERRORS: list[str] = [] def discover_processors(force_reload: bool = False) -> list[str]: """扫描并缓存可用的处理器模块。""" - from domain.processors import builtin as processors_pkg + from . import builtin as processors_pkg TYPE_MAP.clear() CONFIG_SCHEMAS.clear() diff --git a/domain/processors/service.py b/domain/processors/service.py index 611d05f..2305496 100644 --- a/domain/processors/service.py +++ b/domain/processors/service.py @@ -3,20 +3,20 @@ from typing import List, Tuple from fastapi import HTTPException from fastapi.concurrency import run_in_threadpool -from domain.processors.registry import ( +from domain.tasks import task_queue_service +from domain.virtual_fs import VirtualFSService +from .registry import ( get, get_config_schema, get_config_schemas, get_module_path, reload_processors, ) -from domain.processors.types import ( +from .types import ( ProcessDirectoryRequest, ProcessRequest, UpdateSourceRequest, ) -from domain.virtual_fs.service import VirtualFSService -from domain.tasks.task_queue import task_queue_service class ProcessorService: diff --git a/domain/repositories/__init__.py b/domain/repositories/__init__.py new file mode 100644 index 0000000..c9c2ef6 --- /dev/null +++ b/domain/repositories/__init__.py @@ -0,0 +1 @@ +__all__: list[str] = [] diff --git a/domain/share/__init__.py b/domain/share/__init__.py new file mode 100644 index 0000000..0d9268c --- /dev/null +++ b/domain/share/__init__.py @@ -0,0 +1,10 @@ +from .service import ShareService +from .types import ShareCreate, ShareInfo, ShareInfoWithPassword, SharePassword + +__all__ = [ + "ShareService", + "ShareCreate", + "ShareInfo", + "ShareInfoWithPassword", + "SharePassword", +] diff --git a/domain/share/api.py b/domain/share/api.py index 116855d..0f80712 100644 --- a/domain/share/api.py +++ b/domain/share/api.py @@ -4,10 +4,9 @@ from fastapi import APIRouter, Depends, Request from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.share.service import ShareService -from domain.share.types import ( +from domain.auth import User, get_current_active_user +from .service import ShareService +from .types import ( ShareCreate, ShareInfo, ShareInfoWithPassword, diff --git a/domain/share/service.py b/domain/share/service.py index 05c7853..e0d8a5f 100644 --- a/domain/share/service.py +++ b/domain/share/service.py @@ -7,7 +7,7 @@ import bcrypt from fastapi import HTTPException, status from fastapi.responses import Response -from domain.virtual_fs.service import VirtualFSService +from domain.virtual_fs import VirtualFSService from models.database import ShareLink, UserAccount diff --git a/domain/tasks/__init__.py b/domain/tasks/__init__.py new file mode 100644 index 0000000..d8301f1 --- /dev/null +++ b/domain/tasks/__init__.py @@ -0,0 +1,24 @@ +from .service import TaskService +from .task_queue import Task, TaskProgress, TaskStatus, task_queue_service +from .types import ( + AutomationTaskBase, + AutomationTaskCreate, + AutomationTaskRead, + AutomationTaskUpdate, + TaskQueueSettings, + TaskQueueSettingsResponse, +) + +__all__ = [ + "TaskService", + "Task", + "TaskProgress", + "TaskStatus", + "task_queue_service", + "AutomationTaskBase", + "AutomationTaskCreate", + "AutomationTaskRead", + "AutomationTaskUpdate", + "TaskQueueSettings", + "TaskQueueSettingsResponse", +] diff --git a/domain/tasks/api.py b/domain/tasks/api.py index 5339f6b..a7fba9e 100644 --- a/domain/tasks/api.py +++ b/domain/tasks/api.py @@ -2,9 +2,9 @@ from fastapi import APIRouter, Depends, Request from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.tasks.service import TaskService -from domain.tasks.types import ( +from domain.auth import get_current_active_user +from .service import TaskService +from .types import ( AutomationTaskCreate, AutomationTaskUpdate, TaskQueueSettings, diff --git a/domain/tasks/service.py b/domain/tasks/service.py index a16aa8c..321b9ae 100644 --- a/domain/tasks/service.py +++ b/domain/tasks/service.py @@ -3,17 +3,16 @@ from typing import Annotated, Any, Dict, Optional from fastapi import Depends, HTTPException -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.config.service import ConfigService -from domain.tasks.types import ( +from domain.auth import User, get_current_active_user +from domain.config import ConfigService +from .task_queue import task_queue_service +from .types import ( AutomationTaskCreate, AutomationTaskUpdate, TaskQueueSettings, TaskQueueSettingsResponse, ) from models.database import AutomationTask -from domain.tasks.task_queue import task_queue_service class TaskService: diff --git a/domain/tasks/task_queue.py b/domain/tasks/task_queue.py index c735433..972c637 100644 --- a/domain/tasks/task_queue.py +++ b/domain/tasks/task_queue.py @@ -74,7 +74,7 @@ class TaskQueueService: try: # Local import to avoid circular dependency during module load. - from domain.virtual_fs.service import VirtualFSService + from domain.virtual_fs import VirtualFSService if task.name == "process_file": params = task.task_info @@ -88,7 +88,7 @@ class TaskQueueService: task.result = result elif task.name == "automation_task" or self._is_processor_task(task.name): from models.database import AutomationTask - from domain.processors.service import get_processor + from domain.processors import get_processor params = task.task_info auto_task = await AutomationTask.get(id=params["task_id"]) @@ -116,7 +116,7 @@ class TaskQueueService: await VirtualFSService.write_file(save_to, result) task.result = "Automation task completed" elif task.name == "offline_http_download": - from domain.offline_downloads.service import OfflineDownloadService + from domain.offline_downloads import OfflineDownloadService result_path = await OfflineDownloadService.run_http_download(task) task.result = {"path": result_path} @@ -124,7 +124,7 @@ class TaskQueueService: result = await VirtualFSService.run_cross_mount_transfer_task(task) task.result = result elif task.name == "send_email": - from domain.email.service import EmailService + from domain.email import EmailService await EmailService.send_from_task(task.id, task.task_info) task.result = "Email sent" else: @@ -141,7 +141,7 @@ class TaskQueueService: def _is_processor_task(self, task_name: str) -> bool: try: - from domain.processors.service import get_processor + from domain.processors import get_processor return get_processor(task_name) is not None except Exception: @@ -180,7 +180,7 @@ class TaskQueueService: async def start_worker(self, concurrency: int | None = None): if concurrency is None: - from domain.config.service import ConfigService + from domain.config import ConfigService stored_value = await ConfigService.get("TASK_QUEUE_CONCURRENCY", self._concurrency) try: diff --git a/domain/virtual_fs/__init__.py b/domain/virtual_fs/__init__.py new file mode 100644 index 0000000..3d5d6bb --- /dev/null +++ b/domain/virtual_fs/__init__.py @@ -0,0 +1,11 @@ +from .service import VirtualFSService +from .types import DirListing, MkdirRequest, MoveRequest, SearchResultItem, VfsEntry + +__all__ = [ + "VirtualFSService", + "DirListing", + "MkdirRequest", + "MoveRequest", + "SearchResultItem", + "VfsEntry", +] diff --git a/domain/virtual_fs/api.py b/domain/virtual_fs/api.py index 9d66bc3..407b1dc 100644 --- a/domain/virtual_fs/api.py +++ b/domain/virtual_fs/api.py @@ -4,10 +4,9 @@ from fastapi import APIRouter, Depends, File, Query, Request, UploadFile from api.response import success from domain.audit import AuditAction, audit -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.virtual_fs.service import VirtualFSService -from domain.virtual_fs.types import MkdirRequest, MoveRequest +from domain.auth import User, get_current_active_user +from .service import VirtualFSService +from .types import MkdirRequest, MoveRequest router = APIRouter(prefix="/api/fs", tags=["virtual-fs"]) diff --git a/domain/virtual_fs/file_ops.py b/domain/virtual_fs/file_ops.py index 218dc7a..d770b58 100644 --- a/domain/virtual_fs/file_ops.py +++ b/domain/virtual_fs/file_ops.py @@ -4,8 +4,8 @@ from typing import Any, AsyncIterator, Union from fastapi import HTTPException from fastapi.responses import Response -from domain.tasks.service import TaskService -from domain.virtual_fs.thumbnail import is_raw_filename, raw_bytes_to_jpeg +from domain.tasks import TaskService +from .thumbnail import is_raw_filename, raw_bytes_to_jpeg from .listing import VirtualFSListingMixin diff --git a/domain/virtual_fs/listing.py b/domain/virtual_fs/listing.py index f0b78eb..c352593 100644 --- a/domain/virtual_fs/listing.py +++ b/domain/virtual_fs/listing.py @@ -3,9 +3,9 @@ from typing import Any, Dict, List, Tuple from fastapi import HTTPException from api.response import page -from domain.adapters.registry import runtime_registry -from domain.ai.service import VectorDBService, VECTOR_COLLECTION_NAME, FILE_COLLECTION_NAME -from domain.virtual_fs.thumbnail import is_image_filename, is_video_filename +from domain.adapters import runtime_registry +from domain.ai import FILE_COLLECTION_NAME, VECTOR_COLLECTION_NAME, VectorDBService +from .thumbnail import is_image_filename, is_video_filename from models import StorageAdapter from .resolver import VirtualFSResolverMixin diff --git a/domain/virtual_fs/mapping/__init__.py b/domain/virtual_fs/mapping/__init__.py index e69de29..c9c2ef6 100644 --- a/domain/virtual_fs/mapping/__init__.py +++ b/domain/virtual_fs/mapping/__init__.py @@ -0,0 +1 @@ +__all__: list[str] = [] diff --git a/domain/virtual_fs/mapping/s3_api.py b/domain/virtual_fs/mapping/s3_api.py index 096a55f..ef9f153 100644 --- a/domain/virtual_fs/mapping/s3_api.py +++ b/domain/virtual_fs/mapping/s3_api.py @@ -15,8 +15,8 @@ from fastapi import APIRouter, Request, Response from fastapi import HTTPException from domain.audit import AuditAction, audit -from domain.config.service import ConfigService -from domain.virtual_fs.service import VirtualFSService +from domain.config import ConfigService +from domain.virtual_fs import VirtualFSService router = APIRouter(prefix="/s3", tags=["s3"]) diff --git a/domain/virtual_fs/mapping/webdav_api.py b/domain/virtual_fs/mapping/webdav_api.py index e40e08a..5d12c18 100644 --- a/domain/virtual_fs/mapping/webdav_api.py +++ b/domain/virtual_fs/mapping/webdav_api.py @@ -9,10 +9,9 @@ from fastapi import APIRouter, Request, Response, HTTPException, Depends import xml.etree.ElementTree as ET from domain.audit import AuditAction, audit -from domain.auth.service import AuthService -from domain.auth.types import User, UserInDB -from domain.virtual_fs.service import VirtualFSService -from domain.config.service import ConfigService +from domain.auth import AuthService, User, UserInDB +from domain.config import ConfigService +from domain.virtual_fs import VirtualFSService _WEBDAV_ENABLED_KEY = "WEBDAV_MAPPING_ENABLED" diff --git a/domain/virtual_fs/processing.py b/domain/virtual_fs/processing.py index d19024a..42fa41a 100644 --- a/domain/virtual_fs/processing.py +++ b/domain/virtual_fs/processing.py @@ -16,7 +16,7 @@ class VirtualFSProcessingMixin(VirtualFSTransferMixin): save_to: str | None = None, overwrite: bool = False, ) -> Any: - from domain.processors.service import get_processor + from domain.processors import get_processor processor = get_processor(processor_type) if not processor: diff --git a/domain/virtual_fs/resolver.py b/domain/virtual_fs/resolver.py index bb498dd..9290e12 100644 --- a/domain/virtual_fs/resolver.py +++ b/domain/virtual_fs/resolver.py @@ -3,7 +3,7 @@ from typing import Tuple from fastapi import HTTPException from fastapi.responses import Response -from domain.adapters.registry import runtime_registry +from domain.adapters import runtime_registry from models import StorageAdapter from .common import VirtualFSCommonMixin diff --git a/domain/virtual_fs/routes.py b/domain/virtual_fs/routes.py index dd3a9c5..8227b51 100644 --- a/domain/virtual_fs/routes.py +++ b/domain/virtual_fs/routes.py @@ -4,8 +4,8 @@ import re from fastapi import HTTPException, UploadFile from fastapi.responses import Response -from domain.config.service import ConfigService -from domain.virtual_fs.thumbnail import ( +from domain.config import ConfigService +from .thumbnail import ( get_or_create_thumb, is_image_filename, is_raw_filename, diff --git a/domain/virtual_fs/search/__init__.py b/domain/virtual_fs/search/__init__.py index e69de29..c0337ff 100644 --- a/domain/virtual_fs/search/__init__.py +++ b/domain/virtual_fs/search/__init__.py @@ -0,0 +1,3 @@ +from .search_service import VirtualFSSearchService + +__all__ = ["VirtualFSSearchService"] diff --git a/domain/virtual_fs/search/search_api.py b/domain/virtual_fs/search/search_api.py index c53ae8b..1be4cc5 100644 --- a/domain/virtual_fs/search/search_api.py +++ b/domain/virtual_fs/search/search_api.py @@ -1,9 +1,8 @@ from fastapi import APIRouter, Depends, Query from api.response import success -from domain.auth.service import get_current_active_user -from domain.auth.types import User -from domain.virtual_fs.search.search_service import VirtualFSSearchService +from domain.auth import User, get_current_active_user +from .search_service import VirtualFSSearchService router = APIRouter(prefix="/api/fs/search", tags=["search"]) diff --git a/domain/virtual_fs/search/search_service.py b/domain/virtual_fs/search/search_service.py index 53be215..cc39353 100644 --- a/domain/virtual_fs/search/search_service.py +++ b/domain/virtual_fs/search/search_service.py @@ -1,8 +1,7 @@ from typing import Any, Dict, List, Tuple -from domain.virtual_fs.types import SearchResultItem -from domain.ai.inference import get_text_embedding -from domain.ai.service import VectorDBService, VECTOR_COLLECTION_NAME, FILE_COLLECTION_NAME +from domain.ai import FILE_COLLECTION_NAME, VECTOR_COLLECTION_NAME, VectorDBService, get_text_embedding +from ..types import SearchResultItem def _normalize_result(raw: Dict[str, Any], source: str, fallback_score: float = 0.0) -> SearchResultItem: diff --git a/domain/virtual_fs/temp_link.py b/domain/virtual_fs/temp_link.py index f4d1d6b..82036ff 100644 --- a/domain/virtual_fs/temp_link.py +++ b/domain/virtual_fs/temp_link.py @@ -5,7 +5,7 @@ import time from fastapi import HTTPException -from domain.config.service import ConfigService +from domain.config import ConfigService from .processing import VirtualFSProcessingMixin diff --git a/domain/virtual_fs/transfer.py b/domain/virtual_fs/transfer.py index 84f3275..5d7cc85 100644 --- a/domain/virtual_fs/transfer.py +++ b/domain/virtual_fs/transfer.py @@ -273,7 +273,7 @@ class VirtualFSTransferMixin(VirtualFSFileOpsMixin): "overwrite": overwrite, } - from domain.tasks.task_queue import task_queue_service + from domain.tasks import task_queue_service task = await task_queue_service.add_task("cross_mount_transfer", payload) return { @@ -286,7 +286,7 @@ class VirtualFSTransferMixin(VirtualFSFileOpsMixin): @classmethod async def run_cross_mount_transfer_task(cls, task: "Task") -> Dict[str, Any]: - from domain.tasks.task_queue import task_queue_service + from domain.tasks import task_queue_service params = task.task_info or {} operation = params.get("operation") diff --git a/main.py b/main.py index 63c4499..662c786 100644 --- a/main.py +++ b/main.py @@ -2,8 +2,8 @@ import os from pathlib import Path from contextlib import asynccontextmanager -from domain.config.service import ConfigService, VERSION -from domain.adapters.registry import runtime_registry +from domain.adapters import runtime_registry +from domain.config import ConfigService, VERSION from db.session import close_db, init_db from api.routers import include_routers from fastapi.middleware.cors import CORSMiddleware @@ -19,7 +19,7 @@ from middleware.exception_handler import ( ) import httpx from dotenv import load_dotenv -from domain.tasks.task_queue import task_queue_service +from domain.tasks import task_queue_service load_dotenv() @@ -59,7 +59,7 @@ async def lifespan(app: FastAPI): await task_queue_service.start_worker() # 加载已安装的插件 - from domain.plugins.startup import init_plugins + from domain.plugins import init_plugins await init_plugins(app) # 在所有路由加载完成后,挂载静态文件服务(放在最后以避免覆盖 API 路由) diff --git a/setup/foxel_cli.py b/setup/foxel_cli.py index e059ce5..ff962fb 100644 --- a/setup/foxel_cli.py +++ b/setup/foxel_cli.py @@ -13,8 +13,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from domain.auth.service import get_password_hash -from domain.config.service import VERSION +from domain.config import VERSION +from domain.auth import get_password_hash def _project_root() -> Path: