diff --git a/api/routes/adapters.py b/api/routes/adapters.py index 0f72acb..130897d 100644 --- a/api/routes/adapters.py +++ b/api/routes/adapters.py @@ -5,7 +5,7 @@ from typing import Annotated from models import StorageAdapter from schemas import AdapterCreate, AdapterOut from services.auth import get_current_active_user, User -from services.adapters.registry import runtime_registry, get_config_schemas +from services.adapters.registry import runtime_registry, get_config_schemas, normalize_adapter_type from api.response import success from services.logging import LogService @@ -14,6 +14,9 @@ router = APIRouter(prefix="/api/adapters", tags=["adapters"]) def validate_and_normalize_config(adapter_type: str, cfg): schemas = get_config_schemas() + adapter_type = normalize_adapter_type(adapter_type) + if not adapter_type: + raise HTTPException(400, detail="不支持的适配器类型") if not isinstance(cfg, dict): raise HTTPException(400, detail="config 必须是对象") schema = schemas.get(adapter_type) @@ -77,17 +80,10 @@ async def list_adapters( async def available_adapter_types( current_user: Annotated[User, Depends(get_current_active_user)] ): - name_map = { - "local": "本地文件系统", - "webdav": "WebDAV", - "GoogleDrive": "Google Drive", - "OneDrive": "OneDrive", - } data = [] for t, fields in get_config_schemas().items(): data.append({ "type": t, - "name": name_map.get(t, t), "config_schema": fields, }) return success(data) diff --git a/schemas/adapters.py b/schemas/adapters.py index 578f860..d0856ff 100644 --- a/schemas/adapters.py +++ b/schemas/adapters.py @@ -1,15 +1,28 @@ +import re from typing import Dict, Optional from pydantic import BaseModel, Field, field_validator class AdapterBase(BaseModel): name: str - type: str = Field(pattern=r"^[a-zA-Z0-9_]+$") + type: str = Field(pattern=r"^[a-z0-9_]+$") config: Dict = Field(default_factory=dict) enabled: bool = True path: str = None sub_path: Optional[str] = None + @field_validator("type", mode="before") + @classmethod + def _normalize_type(cls, v: str): + if not isinstance(v, str): + raise ValueError("type required") + normalized = v.strip().lower() + if not normalized: + raise ValueError("type required") + if not re.fullmatch(r"[a-z0-9_]+", normalized): + raise ValueError("type must be lowercase alphanumeric or underscore") + return normalized + class AdapterCreate(AdapterBase): @staticmethod diff --git a/services/adapters/googledrive.py b/services/adapters/googledrive.py index 913c7be..c5bcb36 100644 --- a/services/adapters/googledrive.py +++ b/services/adapters/googledrive.py @@ -543,7 +543,7 @@ class GoogleDriveAdapter: return None -ADAPTER_TYPE = "GoogleDrive" +ADAPTER_TYPE = "googledrive" CONFIG_SCHEMA = [ {"key": "client_id", "label": "Client ID", "type": "string", "required": True}, diff --git a/services/adapters/onedrive.py b/services/adapters/onedrive.py index e23b3a4..36b4ead 100644 --- a/services/adapters/onedrive.py +++ b/services/adapters/onedrive.py @@ -445,7 +445,7 @@ class OneDriveAdapter: return self._format_item(resp.json()) -ADAPTER_TYPE = "OneDrive" +ADAPTER_TYPE = "onedrive" CONFIG_SCHEMA = [ {"key": "client_id", "label": "Client ID", "type": "string", "required": True}, diff --git a/services/adapters/quark.py b/services/adapters/quark.py index ad2c978..4d35d35 100644 --- a/services/adapters/quark.py +++ b/services/adapters/quark.py @@ -718,7 +718,7 @@ class QuarkAdapter: return it["fid"] -ADAPTER_TYPE = "Quark" +ADAPTER_TYPE = "quark" CONFIG_SCHEMA = [ {"key": "cookie", "label": "Cookie", "type": "password", "required": True, "placeholder": "从 pan.quark.cn 复制"}, diff --git a/services/adapters/registry.py b/services/adapters/registry.py index c17e945..f34a565 100644 --- a/services/adapters/registry.py +++ b/services/adapters/registry.py @@ -12,6 +12,13 @@ TYPE_MAP: Dict[str, AdapterFactory] = {} CONFIG_SCHEMAS: Dict[str, list] = {} +def normalize_adapter_type(value: str | None) -> str | None: + if value is None: + return None + normalized = str(value).strip().lower() + return normalized or None + + def discover_adapters(): """扫描 services.adapters 包, 自动注册适配器类型、工厂与配置 schema。""" from .. import adapters as adapters_pkg @@ -25,7 +32,7 @@ def discover_adapters(): module = import_module(full_name) except Exception: continue - adapter_type = getattr(module, "ADAPTER_TYPE", None) + adapter_type = normalize_adapter_type(getattr(module, "ADAPTER_TYPE", None)) schema = getattr(module, "CONFIG_SCHEMA", None) factory = getattr(module, "ADAPTER_FACTORY", None) @@ -64,7 +71,16 @@ class RuntimeRegistry: self._instances.clear() adapters = await StorageAdapter.filter(enabled=True) for rec in adapters: - factory = TYPE_MAP.get(rec.type) + normalized_type = normalize_adapter_type(rec.type) + if not normalized_type: + continue + if normalized_type != rec.type: + rec.type = normalized_type + try: + await rec.save(update_fields=["type"]) + except Exception: + continue + factory = TYPE_MAP.get(normalized_type) if not factory: continue try: @@ -89,10 +105,21 @@ class RuntimeRegistry: self.remove(rec.id) return - factory = TYPE_MAP.get(rec.type) + normalized_type = normalize_adapter_type(rec.type) + if not normalized_type: + self.remove(rec.id) + return + if normalized_type != rec.type: + rec.type = normalized_type + try: + await rec.save(update_fields=["type"]) + except Exception: + pass + + factory = TYPE_MAP.get(normalized_type) if not factory: discover_adapters() - factory = TYPE_MAP.get(rec.type) + factory = TYPE_MAP.get(normalized_type) if not factory: return diff --git a/services/adapters/s3.py b/services/adapters/s3.py index df3f3be..63edc98 100644 --- a/services/adapters/s3.py +++ b/services/adapters/s3.py @@ -359,7 +359,7 @@ class S3Adapter: return StreamingResponse(iterator(), status_code=status, headers=headers, media_type=content_type) -ADAPTER_TYPE = "S3" +ADAPTER_TYPE = "s3" CONFIG_SCHEMA = [ {"key": "bucket_name", "label": "Bucket 名称", diff --git a/services/adapters/telegram.py b/services/adapters/telegram.py index a53d744..c656d08 100644 --- a/services/adapters/telegram.py +++ b/services/adapters/telegram.py @@ -8,7 +8,7 @@ from telethon.sessions import StringSession import socks # 适配器类型标识 -ADAPTER_TYPE = "Telegram" +ADAPTER_TYPE = "telegram" # 适配器配置项定义 CONFIG_SCHEMA = [ diff --git a/web/src/api/adapters.ts b/web/src/api/adapters.ts index d1861d9..57dd9bb 100644 --- a/web/src/api/adapters.ts +++ b/web/src/api/adapters.ts @@ -21,7 +21,6 @@ export interface AdapterTypeField { export interface AdapterTypeMeta { type: string; - name: string; config_schema: AdapterTypeField[]; } diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 7feca25..c9b6c8b 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -525,6 +525,15 @@ export const en = { 'Select adapter type': 'Select adapter type', '/ or /drive': '/ or /drive', 'Adapter Config': 'Adapter Config', + 'adapter.type.local': 'Local Filesystem', + 'adapter.type.webdav': 'WebDAV', + 'adapter.type.googledrive': 'Google Drive', + 'adapter.type.onedrive': 'OneDrive', + 'adapter.type.s3': 'Amazon S3', + 'adapter.type.ftp': 'FTP', + 'adapter.type.sftp': 'SFTP', + 'adapter.type.telegram': 'Telegram', + 'adapter.type.quark': 'Quark Drive', // Tasks 'Automation Tasks': 'Automation Tasks', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index 451347d..2c51917 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -528,6 +528,15 @@ export const zh = { 'Select adapter type': '选择适配器类型', '/ or /drive': '/或/drive', 'Adapter Config': '适配器配置', + 'adapter.type.local': '本地文件系统', + 'adapter.type.webdav': 'WebDAV', + 'adapter.type.googledrive': 'Google Drive', + 'adapter.type.onedrive': 'OneDrive', + 'adapter.type.s3': 'Amazon S3', + 'adapter.type.ftp': 'FTP', + 'adapter.type.sftp': 'SFTP', + 'adapter.type.telegram': 'Telegram', + 'adapter.type.quark': '夸克网盘', // Tasks 'Automation Tasks': '自动化任务', diff --git a/web/src/pages/AdaptersPage.tsx b/web/src/pages/AdaptersPage.tsx index c273dd8..fe8031f 100644 --- a/web/src/pages/AdaptersPage.tsx +++ b/web/src/pages/AdaptersPage.tsx @@ -130,9 +130,16 @@ const AdaptersPage = memo(function AdaptersPage() { } }; + const renderTypeLabel = useCallback((type?: string) => { + if (!type) return '-'; + const key = `adapter.type.${type}`; + const label = t(key); + return label === key ? type : label; + }, [t]); + const columns = [ { title: t('Name'), dataIndex: 'name' }, - { title: t('Type'), dataIndex: 'type', width: 100 }, + { title: t('Type'), dataIndex: 'type', width: 140, render: (value: string) => renderTypeLabel(value) }, { title: t('Mount Path'), dataIndex: 'path', width: 140, render: (v: string) => v || '-' }, { title: t('Sub Path'), dataIndex: 'sub_path', width: 140, render: (v: string) => v || '-' }, { @@ -233,9 +240,9 @@ const AdaptersPage = memo(function AdaptersPage() {