Forráskód Böngészése

接入标注平台,修复DPO报错

lxylxy123321 1 napja
szülő
commit
487151fa5f

+ 4 - 4
backend/.env

@@ -57,7 +57,7 @@ JWT_SECRET_KEY=change-me-in-production-use-a-long-random-string
 JWT_ACCESS_EXPIRE_MINUTES=20
 JWT_REFRESH_EXPIRE_HOURS=24
 
-# --- 样本中心 ---
-SAMPLE_CENTER_BASE_URL=http://192.168.92.61
-SAMPLE_CENTER_APP_ID=WviiGL8KQE20tQhmhQPQhhJ5QpFK51F6
-SAMPLE_CENTER_APP_SECRET=9WXP88hEHJiHRSiUdmx7ip5oQPzY0bnJNsEswQo04sk6juCplyJTcnAiZsv7e3lJ
+# --- 标注平台 ---
+ANNOTATION_PLATFORM_BASE_URL=http://192.168.92.61:9003
+ANNOTATION_PLATFORM_APP_ID=nlKLQJdJK3f5ub7UDfQ_E71z2Lo3YSQx
+ANNOTATION_PLATFORM_APP_SECRET=wh0HU_9T83rYMjfLFToNxFOKcrk_8H7Ba_27nNGlPqtTf9ROCytsOgp2ue0ol5mm

+ 4 - 4
backend/.env.docker

@@ -46,7 +46,7 @@ JWT_SECRET_KEY=change-me-in-production-use-a-long-random-string
 JWT_ACCESS_EXPIRE_MINUTES=20
 JWT_REFRESH_EXPIRE_HOURS=24
 
-# --- 样本中心 ---
-SAMPLE_CENTER_BASE_URL=http://192.168.92.61
-SAMPLE_CENTER_APP_ID=WviiGL8KQE20tQhmhQPQhhJ5QpFK51F6
-SAMPLE_CENTER_APP_SECRET=9WXP88hEHJiHRSiUdmx7ip5oQPzY0bnJNsEswQo04sk6juCplyJTcnAiZsv7e3lJ
+# --- 标注平台 ---
+ANNOTATION_PLATFORM_BASE_URL=http://192.168.92.61:9003
+ANNOTATION_PLATFORM_APP_ID=nlKLQJdJK3f5ub7UDfQ_E71z2Lo3YSQx
+ANNOTATION_PLATFORM_APP_SECRET=wh0HU_9T83rYMjfLFToNxFOKcrk_8H7Ba_27nNGlPqtTf9ROCytsOgp2ue0ol5mm

+ 62 - 0
backend/app/api/annotation_platform.py

@@ -0,0 +1,62 @@
+"""标注平台 API 路由。"""
+
+from fastapi import APIRouter, Query, HTTPException
+
+from app.schemas.annotation_platform import (
+    AnnotationProjectListResponse,
+    AnnotationProjectDetailResponse,
+    ProjectImportResponse,
+)
+from app.services import annotation_platform_service
+
+router = APIRouter()
+
+
+@router.get("/projects", response_model=AnnotationProjectListResponse)
+async def get_projects(
+    page: int = Query(default=1, ge=1),
+    page_size: int = Query(default=20, ge=1, le=100),
+    name: str | None = Query(default=None),
+    type: str | None = Query(default=None),
+    status: str | None = Query(default=None),
+):
+    """获取标注平台项目列表。"""
+    try:
+        data = await annotation_platform_service.list_projects(
+            page, page_size, name=name, project_type=type, status=status,
+        )
+        return AnnotationProjectListResponse(**data)
+    except ValueError as e:
+        raise HTTPException(status_code=503, detail=str(e))
+    except Exception as e:
+        raise HTTPException(status_code=502, detail=f"标注平台请求失败: {str(e)}")
+
+
+@router.get("/projects/{project_id}", response_model=AnnotationProjectDetailResponse)
+async def get_project_detail(project_id: str):
+    """获取项目详情。"""
+    try:
+        data = await annotation_platform_service.get_project_detail(project_id)
+        return AnnotationProjectDetailResponse(**data)
+    except ValueError as e:
+        raise HTTPException(status_code=503, detail=str(e))
+    except Exception as e:
+        raise HTTPException(status_code=502, detail=f"标注平台请求失败: {str(e)}")
+
+
+@router.post("/projects/{project_id}/import", response_model=ProjectImportResponse)
+async def import_project(
+    project_id: str,
+    project_name: str = Query(default=""),
+    format: str = Query(default="alpaca"),
+):
+    """从标注平台导出项目数据并导入为训练数据集。"""
+    try:
+        data = await annotation_platform_service.import_project_dataset(
+            project_id, project_name=project_name, format=format,
+        )
+        return ProjectImportResponse(**data)
+    except ValueError as e:
+        raise HTTPException(status_code=503, detail=str(e))
+    except Exception as e:
+        raise HTTPException(status_code=502, detail=f"标注平台导入失败: {str(e)}")

+ 0 - 51
backend/app/api/sample_center.py

@@ -1,51 +0,0 @@
-"""样本中心 API 路由。"""
-
-from fastapi import APIRouter, Query, HTTPException
-
-from app.schemas.sample_center import (
-    KnowledgeBaseListResponse,
-    KnowledgeBaseDetailResponse,
-    KbImportResponse,
-)
-from app.services import sample_center_service
-
-router = APIRouter()
-
-
-@router.get("/knowledge-bases", response_model=KnowledgeBaseListResponse)
-async def get_knowledge_bases(
-    page: int = Query(default=1, ge=1),
-    page_size: int = Query(default=20, ge=1, le=100),
-):
-    """获取样本中心知识库列表。"""
-    try:
-        data = await sample_center_service.list_knowledge_bases(page, page_size)
-        return KnowledgeBaseListResponse(**data)
-    except ValueError as e:
-        raise HTTPException(status_code=503, detail=str(e))
-    except Exception as e:
-        raise HTTPException(status_code=502, detail=f"样本中心请求失败: {str(e)}")
-
-
-@router.get("/knowledge-bases/{kb_id}", response_model=KnowledgeBaseDetailResponse)
-async def get_knowledge_base_detail(kb_id: str):
-    """获取知识库详情。"""
-    try:
-        data = await sample_center_service.get_knowledge_base_detail(kb_id)
-        return KnowledgeBaseDetailResponse(**data)
-    except ValueError as e:
-        raise HTTPException(status_code=503, detail=str(e))
-    except Exception as e:
-        raise HTTPException(status_code=502, detail=f"样本中心请求失败: {str(e)}")
-
-
-@router.post("/knowledge-bases/{kb_id}/import", response_model=KbImportResponse)
-async def import_from_knowledge_base(kb_id: str, kb_name: str = ""):
-    """从知识库导入数据到训练数据集。"""
-    try:
-        data = await sample_center_service.import_kb_to_dataset(kb_id, kb_name)
-        return KbImportResponse(**data)
-    except ValueError as e:
-        raise HTTPException(status_code=503, detail=str(e))
-    except Exception as e:
-        raise HTTPException(status_code=502, detail=f"样本中心请求失败: {str(e)}")

+ 4 - 4
backend/app/config.py

@@ -112,10 +112,10 @@ class Settings(BaseSettings):
     compute_node_remote_env: str = "production"
     compute_node_ssh_timeout: int = 300  # SSH 命令超时(秒)
 
-    # --- 样本中心 ---
-    sample_center_base_url: str = "http://192.168.92.61"  # 样本中心 API 地址,如 https://sample.example.com
-    sample_center_app_id: str = "WviiGL8KQE20tQhmhQPQhhJ5QpFK51F6"  # 样本中心应用标识
-    sample_center_app_secret: str = "9WXP88hEHJiHRSiUdmx7ip5oQPzY0bnJNsEswQo04sk6juCplyJTcnAiZsv7e3lJ"  # 样本中心应用密钥
+    # --- 标注平台 ---
+    annotation_platform_base_url: str = "http://192.168.92.61:9003"
+    annotation_platform_app_id: str = "nlKLQJdJK3f5ub7UDfQ_E71z2Lo3YSQx"
+    annotation_platform_app_secret: str = "wh0HU_9T83rYMjfLFToNxFOKcrk_8H7Ba_27nNGlPqtTf9ROCytsOgp2ue0ol5mm"
 
     # --- SSO 统一认证 ---
     sso_base_url: str = "http://192.168.92.61:8200"

+ 22 - 0
backend/app/engines/text_engine.py

@@ -255,8 +255,29 @@ class TextEngine(BaseEngine):
                 callbacks=all_callbacks,
             )
         elif task_type == "dpo":
+            from copy import deepcopy
+
             from trl import DPOConfig, DPOTrainer
 
+            # 显式创建 reference model 并冻结,避免 AdaLora 多 adapter 冲突
+            ref_model = deepcopy(self._model)
+            ref_model.eval()
+            for param in ref_model.parameters():
+                param.requires_grad = False
+            # 将 ref_model 上的 PEFT adapter 设为推理模式
+            # AdaLora 只允许 1 个可训练 adapter,policy model 已有 1 个
+            if hasattr(ref_model, "set_adapter"):
+                try:
+                    ref_model.set_adapter("default", inference_mode=True)
+                except Exception:
+                    pass
+            elif hasattr(ref_model, "peft_config"):
+                for adapter_name in list(ref_model.peft_config.keys()):
+                    try:
+                        ref_model.peft_config[adapter_name].inference_mode = True
+                    except Exception:
+                        pass
+
             base_trainer_kwargs = dict(
                 output_dir=output_dir,
                 num_train_epochs=epochs,
@@ -275,6 +296,7 @@ class TextEngine(BaseEngine):
 
             trainer = DPOTrainer(
                 model=self._model,
+                ref_model=ref_model,
                 args=DPOConfig(**base_trainer_kwargs),
                 train_dataset=dataset,
                 processing_class=self._tokenizer,

+ 49 - 0
backend/app/schemas/annotation_platform.py

@@ -0,0 +1,49 @@
+"""标注平台数据模型。"""
+
+from pydantic import BaseModel
+
+
+class AnnotationProjectItem(BaseModel):
+    project_id: str
+    project_name: str
+    description: str = ""
+    project_type: str = ""       # "image" | "text"
+    task_type: str = ""
+    status: str = ""
+    created_by: str = ""
+    created_at: str = ""
+    updated_at: str = ""
+    task_count: int = 0
+    completed_task_count: int = 0
+
+
+class AnnotationProjectListResponse(BaseModel):
+    items: list[AnnotationProjectItem]
+    total: int
+    page: int
+    page_size: int
+
+
+class AnnotationProjectDetailResponse(BaseModel):
+    project_id: str
+    project_name: str
+    description: str = ""
+    project_type: str = ""
+    task_type: str = ""
+    status: str = ""
+    created_by: str = ""
+    created_at: str = ""
+    updated_at: str = ""
+    task_count: int = 0
+    completed_task_count: int = 0
+    assigned_task_count: int = 0
+    completion_percentage: float = 0.0
+
+
+class ProjectImportResponse(BaseModel):
+    project_id: str
+    project_name: str
+    format: str
+    total_exported: int
+    dataset_id: str
+    dataset_name: str

+ 0 - 63
backend/app/schemas/sample_center.py

@@ -1,63 +0,0 @@
-from pydantic import BaseModel, field_validator
-
-
-class KnowledgeBaseItem(BaseModel):
-    id: str
-    name: str
-    parent_table: str = ""
-    child_table: str = ""
-    document_count: int
-    status: str
-    created_at: str
-    created_by: str
-    metadata_schema: list[dict] = []
-
-    @field_validator("parent_table", "child_table", mode="before")
-    @classmethod
-    def none_to_empty(cls, v: str | None) -> str:
-        return v if v is not None else ""
-
-
-class KnowledgeBaseListResponse(BaseModel):
-    total: int
-    page: int
-    page_size: int
-    items: list[KnowledgeBaseItem]
-
-
-class KnowledgeBaseDetailResponse(BaseModel):
-    id: str
-    name: str
-    description: str = ""
-    parent_table: str = ""
-    child_table: str = ""
-    document_count: int
-    status: str
-    created_at: str
-    updated_at: str = ""
-    created_by: str
-    metadata_schema: list[dict] = []
-
-    @field_validator("parent_table", "child_table", mode="before")
-    @classmethod
-    def none_to_empty(cls, v: str | None) -> str:
-        return v if v is not None else ""
-
-
-class ImportTaskResponse(BaseModel):
-    task_id: str
-    status: str
-
-
-class KbImportResponse(BaseModel):
-    kb_id: str
-    kb_name: str
-    document_count: int
-    metadata_schema: list[dict] = []
-    parent_table: str = ""
-    child_table: str = ""
-
-    @field_validator("parent_table", "child_table", mode="before")
-    @classmethod
-    def none_to_empty(cls, v: str | None) -> str:
-        return v if v is not None else ""

+ 269 - 0
backend/app/services/annotation_platform_service.py

@@ -0,0 +1,269 @@
+"""标注平台 API 客户端服务。
+
+对接标注平台的对外 API,支持 HMAC-SHA256 签名认证。
+功能:列出项目、获取项目详情、导出并下载数据集。
+"""
+
+import hashlib
+import hmac
+import secrets
+import time
+import uuid
+from datetime import datetime
+from pathlib import Path
+from typing import Any
+
+import httpx
+
+from app.config import get_settings
+from app.core.db import async_session, DatasetRecord
+from app.core.logging import logger
+
+settings = get_settings()
+
+# Token 缓存(内存中)
+_token_cache: dict[str, Any] = {}
+
+
+def _get_base_url() -> str:
+    if not settings.annotation_platform_base_url:
+        raise ValueError("标注平台地址未配置,请检查 ANNOTATION_PLATFORM_BASE_URL 环境变量")
+    return settings.annotation_platform_base_url.rstrip("/")
+
+
+def _get_credentials() -> tuple[str, str]:
+    if not settings.annotation_platform_app_id or not settings.annotation_platform_app_secret:
+        raise ValueError("标注平台凭证未配置,请检查 ANNOTATION_PLATFORM_APP_ID 和 ANNOTATION_PLATFORM_APP_SECRET")
+    return settings.annotation_platform_app_id, settings.annotation_platform_app_secret
+
+
+def _sign(app_secret: str, app_id: str, timestamp: str, nonce: str) -> str:
+    """HMAC-SHA256 签名。"""
+    message = app_id + timestamp + nonce
+    return hmac.new(app_secret.encode(), message.encode(), hashlib.sha256).hexdigest()
+
+
+def _check_token_valid() -> bool:
+    if not _token_cache.get("access_token"):
+        return False
+    expires_at = _token_cache.get("expires_at", 0)
+    return time.time() < expires_at - 300  # 提前 5 分钟刷新
+
+
+async def get_token() -> str:
+    """获取 Access Token,带缓存。"""
+    if _check_token_valid():
+        return _token_cache["access_token"]
+
+    app_id, app_secret = _get_credentials()
+    base_url = _get_base_url()
+
+    timestamp = str(int(time.time()))
+    nonce = secrets.token_hex(8)  # 16 位十六进制随机字符串
+    signature = _sign(app_secret, app_id, timestamp, nonce)
+
+    async with httpx.AsyncClient(timeout=30) as client:
+        resp = await client.post(
+            f"{base_url}/api/v1/open/auth/token",
+            headers={
+                "X-Api-Key": app_id,
+                "X-Signature": signature,
+                "X-Timestamp": timestamp,
+                "X-Nonce": nonce,
+            },
+        )
+        resp.raise_for_status()
+        body = resp.json()
+
+    if body.get("code") != 0:
+        raise RuntimeError(f"获取标注平台 Token 失败: {body.get('message')}")
+
+    data = body["data"]
+    _token_cache["access_token"] = data["access_token"]
+    _token_cache["expires_in"] = data.get("expires_in", 7200)
+    _token_cache["expires_at"] = time.time() + data.get("expires_in", 7200)
+
+    return data["access_token"]
+
+
+def _auth_headers() -> dict[str, str]:
+    token = _token_cache.get("access_token", "")
+    return {
+        "Authorization": f"Bearer {token}",
+        "Content-Type": "application/json",
+    }
+
+
+async def _request(method: str, path: str, **kwargs) -> dict[str, Any]:
+    """统一的请求方法,自动携带 Token 并处理错误。"""
+    await get_token()
+    base_url = _get_base_url()
+
+    async with httpx.AsyncClient(timeout=60) as client:
+        resp = await client.request(
+            method,
+            f"{base_url}{path}",
+            headers=_auth_headers(),
+            **kwargs,
+        )
+        resp.raise_for_status()
+        body = resp.json()
+
+    if body.get("code") != 0:
+        raise RuntimeError(f"标注平台请求失败: {body.get('message')}")
+
+    return body.get("data", {})
+
+
+# ---------- 项目列表 ----------
+
+async def list_projects(
+    page: int = 1,
+    page_size: int = 20,
+    name: str | None = None,
+    project_type: str | None = None,
+    status: str | None = None,
+) -> dict[str, Any]:
+    """获取标注平台项目列表。"""
+    params: dict[str, Any] = {"page": page, "page_size": page_size}
+    if name:
+        params["name"] = name
+    if project_type:
+        params["type"] = project_type
+    if status:
+        params["status"] = status
+
+    return await _request("GET", "/api/v1/open/projects", params=params)
+
+
+# ---------- 项目详情 ----------
+
+async def get_project_detail(project_id: str) -> dict[str, Any]:
+    """获取项目详情。"""
+    return await _request("GET", f"/api/v1/open/projects/{project_id}")
+
+
+# ---------- 数据集导出与下载 ----------
+
+async def import_project_dataset(
+    project_id: str,
+    project_name: str = "",
+    format: str = "alpaca",
+) -> dict[str, Any]:
+    """导出并下载项目数据集,保存到本地并写入数据库。
+
+    流程:
+    1. POST 请求导出 → 获取 file_url
+    2. GET 下载文件 → 保存到 uploads 目录
+    3. 写入 DatasetRecord 数据库
+    """
+    # 1. 请求导出
+    export_data = await _request(
+        "POST",
+        f"/api/v1/open/projects/{project_id}/datasets/download",
+        json={"format": format, "completed_only": True},
+    )
+
+    file_url = export_data.get("file_url", "")
+    file_name = export_data.get("file_name", f"{project_id}_{format}.json")
+    total_exported = export_data.get("total_exported", 0)
+
+    if not file_url:
+        raise RuntimeError("标注平台未返回下载链接")
+
+    # 2. 下载文件
+    await get_token()
+    base_url = _get_base_url()
+    download_url = f"{base_url}{file_url}" if file_url.startswith("/") else file_url
+
+    async with httpx.AsyncClient(timeout=120) as client:
+        resp = await client.get(
+            download_url,
+            headers={"Authorization": f"Bearer {_token_cache.get('access_token', '')}"},
+            follow_redirects=True,
+        )
+        resp.raise_for_status()
+        file_content = resp.content
+
+    # 3. 保存到 uploads 目录
+    upload_dir = settings.uploads_dir
+    upload_dir.mkdir(parents=True, exist_ok=True)
+
+    safe_name = f"{project_name or project_id}_{file_name}" if project_name else file_name
+    # 清理文件名中的非法字符
+    safe_name = "".join(c if c.isalnum() or c in "._-" else "_" for c in safe_name)
+    file_path = upload_dir / safe_name
+    if file_path.exists():
+        file_path = upload_dir / f"{uuid.uuid4().hex[:8]}_{safe_name}"
+
+    file_path.write_bytes(file_content)
+
+    # 4. 检测格式和记录数
+    fmt = _detect_format(file_path.name)
+    record_count = _count_records(file_path, fmt)
+
+    # 5. 写入数据库
+    record_id = str(uuid.uuid4())
+    record = DatasetRecord(
+        id=record_id,
+        name=file_path.name,
+        format=fmt,
+        record_count=record_count,
+        file_path=str(file_path),
+        created_at=datetime.utcnow(),
+    )
+    async with async_session() as session:
+        session.add(record)
+        await session.commit()
+
+    logger.info(f"Imported dataset from annotation platform: {project_id} -> {file_path.name} ({record_count} records)")
+
+    return {
+        "project_id": project_id,
+        "project_name": project_name or project_id,
+        "format": format,
+        "total_exported": total_exported,
+        "dataset_id": record_id,
+        "dataset_name": file_path.name,
+    }
+
+
+def _detect_format(filename: str) -> str:
+    """根据文件名推断格式。"""
+    name = filename.lower()
+    if name.endswith(".jsonl"):
+        return "jsonl"
+    if name.endswith(".csv"):
+        return "csv"
+    if name.endswith(".parquet"):
+        return "parquet"
+    if name.endswith(".json"):
+        return "json"
+    return "json"
+
+
+def _count_records(file_path: Path, fmt: str) -> int:
+    """计算文件中的记录数。"""
+    import json
+
+    if not file_path.exists():
+        return 0
+
+    try:
+        if fmt == "jsonl":
+            with open(file_path, "r", encoding="utf-8") as f:
+                return sum(1 for line in f if line.strip())
+        elif fmt == "json":
+            with open(file_path, "r", encoding="utf-8") as f:
+                data = json.load(f)
+            if isinstance(data, list):
+                return len(data)
+            return 1
+        elif fmt == "csv":
+            import csv
+            with open(file_path, "r", encoding="utf-8") as f:
+                return sum(1 for _ in csv.DictReader(f))
+    except Exception:
+        return 0
+
+    return 0

+ 0 - 180
backend/app/services/sample_center_service.py

@@ -1,180 +0,0 @@
-"""样本中心 API 客户端服务。"""
-
-import httpx
-import time
-from typing import Any
-
-from app.config import get_settings
-from app.core.logging import logger
-
-settings = get_settings()
-
-# Token 缓存(内存中)
-_token_cache: dict[str, Any] = {}
-
-
-def _get_base_url() -> str:
-    if not settings.sample_center_base_url:
-        raise ValueError("样本中心地址未配置,请检查 SAMPLE_CENTER_BASE_URL 环境变量")
-    return settings.sample_center_base_url.rstrip("/")
-
-
-def _get_credentials() -> tuple[str, str]:
-    if not settings.sample_center_app_id or not settings.sample_center_app_secret:
-        raise ValueError("样本中心凭证未配置,请检查 SAMPLE_CENTER_APP_ID 和 SAMPLE_CENTER_APP_SECRET")
-    return settings.sample_center_app_id, settings.sample_center_app_secret
-
-
-def _check_token_valid() -> bool:
-    if not _token_cache.get("access_token"):
-        return False
-    expires_at = _token_cache.get("expires_at", 0)
-    return time.time() < expires_at - 300  # 提前 5 分钟过期
-
-
-async def get_token() -> str:
-    if _check_token_valid():
-        return _token_cache["access_token"]
-
-    app_id, app_secret = _get_credentials()
-    base_url = _get_base_url()
-
-    async with httpx.AsyncClient(timeout=30) as client:
-        resp = await client.post(
-            f"{base_url}/api/v1/auth/token",
-            json={"app_id": app_id, "app_secret": app_secret},
-        )
-        resp.raise_for_status()
-        body = resp.json()
-
-    if body.get("code") != "000000":
-        raise RuntimeError(f"获取样本中心 Token 失败: {body.get('message')}")
-
-    data = body["data"]
-    _token_cache["access_token"] = data["access_token"]
-    _token_cache["expires_in"] = data.get("expires_in", 7200)
-    _token_cache["expires_at"] = time.time() + data.get("expires_in", 7200)
-    _token_cache["token_type"] = data.get("token_type", "Bearer")
-
-    return data["access_token"]
-
-
-def _auth_headers() -> dict[str, str]:
-    app_id, _ = _get_credentials()
-    token = _token_cache.get("access_token", "")
-    return {
-        "Authorization": f"Bearer {token}",
-        "X-App-Id": app_id,
-        "Content-Type": "application/json",
-    }
-
-
-async def list_knowledge_bases(page: int = 1, page_size: int = 20) -> dict[str, Any]:
-    """获取知识库列表。"""
-    token = await get_token()
-    base_url = _get_base_url()
-
-    async with httpx.AsyncClient(timeout=30) as client:
-        resp = await client.get(
-            f"{base_url}/api/v1/knowledge-bases",
-            params={"page": page, "page_size": page_size},
-            headers=_auth_headers(),
-        )
-        resp.raise_for_status()
-        body = resp.json()
-
-    if body.get("code") != "000000":
-        raise RuntimeError(f"获取知识库列表失败: {body.get('message')}")
-
-    return body["data"]
-
-
-async def get_knowledge_base_detail(kb_id: str) -> dict[str, Any]:
-    """获取知识库详情。"""
-    token = await get_token()
-    base_url = _get_base_url()
-
-    async with httpx.AsyncClient(timeout=30) as client:
-        resp = await client.get(
-            f"{base_url}/api/v1/knowledge-bases/{kb_id}",
-            headers=_auth_headers(),
-        )
-        resp.raise_for_status()
-        body = resp.json()
-
-    if body.get("code") != "000000":
-        raise RuntimeError(f"获取知识库详情失败: {body.get('message')}")
-
-    return body["data"]
-
-
-async def batch_import(kb_id: str, parents: list[dict], children: list[dict] | None = None,
-                       callback_url: str | None = None) -> dict[str, Any]:
-    """提交批量入库任务。"""
-    import uuid
-
-    token = await get_token()
-    base_url = _get_base_url()
-    task_no = f"IMP{int(time.time())}{uuid.uuid4().hex[:8]}"
-
-    payload: dict[str, Any] = {
-        "task_no": task_no,
-        "parents": parents,
-    }
-    if children:
-        payload["children"] = children
-    if callback_url:
-        payload["callback_url"] = callback_url
-
-    async with httpx.AsyncClient(timeout=60) as client:
-        resp = await client.post(
-            f"{base_url}/api/v1/knowledge-bases/{kb_id}/batch-import",
-            json=payload,
-            headers=_auth_headers(),
-        )
-        resp.raise_for_status()
-        body = resp.json()
-
-    if body.get("code") != "000000":
-        raise RuntimeError(f"批量入库提交失败: {body.get('message')}")
-
-    return body["data"]
-
-
-async def query_import_task(task_id: str) -> dict[str, Any]:
-    """查询批量入库任务状态。"""
-    token = await get_token()
-    base_url = _get_base_url()
-
-    async with httpx.AsyncClient(timeout=30) as client:
-        resp = await client.get(
-            f"{base_url}/api/v1/knowledge-bases/batch-import/{task_id}",
-            headers=_auth_headers(),
-        )
-        resp.raise_for_status()
-        body = resp.json()
-
-    if body.get("code") != "000000":
-        raise RuntimeError(f"查询任务失败: {body.get('message')}")
-
-    return body["data"]
-
-
-async def import_kb_to_dataset(kb_id: str, kb_name: str) -> dict[str, Any]:
-    """从知识库导入数据:查询知识库详情,将数据转为训练格式并保存为数据集。
-
-    由于样本中心的批量入库是异步任务,这里采用直接查询知识库内容的方式。
-    先获取知识库详情,然后根据 metadata_schema 构建训练数据集。
-    """
-    kb_detail = await get_knowledge_base_detail(kb_id)
-
-    # 这里返回知识库信息,前端可据此展示给用户
-    # 实际的数据导入由批量入库 API 完成
-    return {
-        "kb_id": kb_id,
-        "kb_name": kb_name or kb_detail.get("name", ""),
-        "document_count": kb_detail.get("document_count", 0),
-        "metadata_schema": kb_detail.get("metadata_schema", []),
-        "parent_table": kb_detail.get("parent_table", ""),
-        "child_table": kb_detail.get("child_table", ""),
-    }

+ 2 - 2
backend/main.py

@@ -77,7 +77,7 @@ def create_app() -> FastAPI:
     from app.api import deployment as deployment_api
     from app.api import inference as inference_api
     from app.api import auth as auth_api
-    from app.api import sample_center as sample_center_api
+    from app.api import annotation_platform as annotation_platform_api
     from app.api import api_keys as api_keys_api
     from app.core.auth import get_current_active_user
 
@@ -120,7 +120,7 @@ def create_app() -> FastAPI:
         dependencies=[Depends(get_current_active_user)],
     )
     app.include_router(
-        sample_center_api.router, prefix="/api/v1/sample-center", tags=["sample-center"],
+        annotation_platform_api.router, prefix="/api/v1/annotation-platform", tags=["annotation-platform"],
         dependencies=[Depends(get_current_active_user)],
     )
 

+ 38 - 39
frontend/src/api/client.ts

@@ -181,18 +181,22 @@ const api = {
       apiFetch(`/api/v1/deployment/${id}/status`).then(r => r.json()) as Promise<DeployResponse>,
   },
 
-  // --- Sample Center ---
-  sampleCenter: {
-    listKnowledgeBases: (page = 1, page_size = 20) =>
-      apiFetch(`/api/v1/sample-center/knowledge-bases?page=${page}&page_size=${page_size}`)
-        .then(r => r.json()) as Promise<KnowledgeBaseListResponse>,
-    getKnowledgeBaseDetail: (kb_id: string) =>
-      apiFetch(`/api/v1/sample-center/knowledge-bases/${kb_id}`)
-        .then(r => r.json()) as Promise<KnowledgeBaseDetailResponse>,
-    importFromKnowledgeBase: (kb_id: string, kb_name = '') =>
-      apiFetch(`/api/v1/sample-center/knowledge-bases/${kb_id}/import?kb_name=${encodeURIComponent(kb_name)}`, {
+  // --- Annotation Platform ---
+  annotationPlatform: {
+    listProjects: (page = 1, page_size = 20, name?: string, type?: string) => {
+      const params = new URLSearchParams({ page: String(page), page_size: String(page_size) })
+      if (name) params.set('name', name)
+      if (type) params.set('type', type)
+      return apiFetch(`/api/v1/annotation-platform/projects?${params}`)
+        .then(r => r.json()) as Promise<AnnotationProjectListResponse>
+    },
+    getProjectDetail: (project_id: string) =>
+      apiFetch(`/api/v1/annotation-platform/projects/${project_id}`)
+        .then(r => r.json()) as Promise<AnnotationProjectDetailResponse>,
+    importProject: (project_id: string, project_name = '', format = 'alpaca') =>
+      apiFetch(`/api/v1/annotation-platform/projects/${project_id}/import?project_name=${encodeURIComponent(project_name)}&format=${format}`, {
         method: 'POST',
-      }).then(r => r.json()) as Promise<KbImportResponse>,
+      }).then(r => r.json()) as Promise<ProjectImportResponse>,
   },
 
   // --- API Keys ---
@@ -435,44 +439,39 @@ interface InferenceResponse {
   error?: string
 }
 
-interface MetadataSchemaField {
-  field_name_cn: string
-  field_name_en: string
-  field_type: string
+interface AnnotationProjectItem {
+  project_id: string
+  project_name: string
   description: string
-}
-
-interface KnowledgeBaseItem {
-  id: string
-  name: string
-  parent_table: string
-  child_table: string
-  document_count: number
-  status: number
-  created_at: string
+  project_type: string
+  task_type: string
+  status: string
   created_by: string
-  metadata_schema: MetadataSchemaField[]
+  created_at: string
+  updated_at: string
+  task_count: number
+  completed_task_count: number
 }
 
-interface KnowledgeBaseListResponse {
+interface AnnotationProjectListResponse {
+  items: AnnotationProjectItem[]
   total: number
   page: number
   page_size: number
-  items: KnowledgeBaseItem[]
 }
 
-interface KnowledgeBaseDetailResponse extends KnowledgeBaseItem {
-  description: string
-  updated_at: string
+interface AnnotationProjectDetailResponse extends AnnotationProjectItem {
+  assigned_task_count: number
+  completion_percentage: number
 }
 
-interface KbImportResponse {
-  kb_id: string
-  kb_name: string
-  document_count: number
-  metadata_schema: MetadataSchemaField[]
-  parent_table: string
-  child_table: string
+interface ProjectImportResponse {
+  project_id: string
+  project_name: string
+  format: string
+  total_exported: number
+  dataset_id: string
+  dataset_name: string
 }
 
 interface ApiKeyCreateResponse {
@@ -491,4 +490,4 @@ interface ApiKeyInfo {
   created_at?: string
 }
 
-export type { ModelInfo, ModelTestRequest, ModelTestResponse, ModelDownloadResponse, ModelDownloadTaskResponse, DatasetInfo, DatasetDownloadResponse, DatasetDownloadTaskResponse, DatasetPreview, DatasetValidation, TrainingJob, TrainingConfig, EvalConfig, EvalResult, DeployConfig, DeployServeConfig, DeployResponse, DeployedServiceInfo, AdapterInfo, InferenceRequest, InferenceResponse, KnowledgeBaseItem, KnowledgeBaseListResponse, KnowledgeBaseDetailResponse, KbImportResponse, ApiKeyCreateResponse, ApiKeyInfo }
+export type { ModelInfo, ModelTestRequest, ModelTestResponse, ModelDownloadResponse, ModelDownloadTaskResponse, DatasetInfo, DatasetDownloadResponse, DatasetDownloadTaskResponse, DatasetPreview, DatasetValidation, TrainingJob, TrainingConfig, EvalConfig, EvalResult, DeployConfig, DeployServeConfig, DeployResponse, DeployedServiceInfo, AdapterInfo, InferenceRequest, InferenceResponse, AnnotationProjectItem, AnnotationProjectListResponse, AnnotationProjectDetailResponse, ProjectImportResponse, ApiKeyCreateResponse, ApiKeyInfo }

+ 109 - 93
frontend/src/pages/Datasets.tsx

@@ -1,5 +1,5 @@
 import { useState, useEffect, useRef, useCallback } from 'react'
-import api, { DatasetInfo, KnowledgeBaseItem, DatasetDownloadTaskResponse } from '../api/client'
+import api, { DatasetInfo, AnnotationProjectItem, DatasetDownloadTaskResponse } from '../api/client'
 import { Database, Upload, Loader2, FolderOpen, CheckCircle, XCircle, Eye, Trash2, FileText } from 'lucide-react'
 
 function formatBadge(format: string) {
@@ -124,14 +124,14 @@ export function Datasets() {
   const [previewData, setPreviewData] = useState<{ columns: string[]; rows: { row_index: number; data: Record<string, unknown> }[] } | null>(null)
   const inputRef = useRef<HTMLInputElement>(null)
 
-  // Sample center modal state
-  const [showSampleCenter, setShowSampleCenter] = useState(false)
-  const [kbList, setKbList] = useState<KnowledgeBaseItem[]>([])
-  const [kbLoading, setKbLoading] = useState(false)
-  const [kbImporting, setKbImporting] = useState<string | null>(null)
-  const [kbStatus, setKbStatus] = useState('')
-  const [kbPage, setKbPage] = useState(1)
-  const [kbTotal, setKbTotal] = useState(0)
+  // Annotation platform modal state
+  const [showAnnotationPlatform, setShowAnnotationPlatform] = useState(false)
+  const [projectList, setProjectList] = useState<AnnotationProjectItem[]>([])
+  const [projectLoading, setProjectLoading] = useState(false)
+  const [projectImporting, setProjectImporting] = useState<string | null>(null)
+  const [projectStatus, setProjectStatus] = useState('')
+  const [projectPage, setProjectPage] = useState(1)
+  const [projectTotal, setProjectTotal] = useState(0)
 
   // Active downloads tracking
   const [activeDownloads, setActiveDownloads] = useState<Map<string, DatasetDownloadTaskResponse>>(new Map())
@@ -249,30 +249,30 @@ export function Datasets() {
     }
   }
 
-  const fetchKnowledgeBases = useCallback((page = 1) => {
-    setKbLoading(true)
-    api.sampleCenter.listKnowledgeBases(page, 20)
+  const fetchProjects = useCallback((page = 1) => {
+    setProjectLoading(true)
+    api.annotationPlatform.listProjects(page, 20)
       .then(res => {
-        setKbList(res.items)
-        setKbTotal(res.total)
-        setKbPage(res.page)
+        setProjectList(res.items)
+        setProjectTotal(res.total)
+        setProjectPage(res.page)
       })
-      .catch(err => setKbStatus(`获取知识库列表失败: ${err.message}`))
-      .finally(() => setKbLoading(false))
+      .catch(err => setProjectStatus(`获取项目列表失败: ${err.message}`))
+      .finally(() => setProjectLoading(false))
   }, [])
 
-  const handleImportFromKB = async (kb: KnowledgeBaseItem) => {
-    setKbImporting(kb.id)
-    setKbStatus(`正在导入 "${kb.name}" ...`)
+  const handleImportProject = async (project: AnnotationProjectItem) => {
+    setProjectImporting(project.project_id)
+    setProjectStatus(`正在导入 "${project.project_name}" ...`)
     try {
-      await api.sampleCenter.importFromKnowledgeBase(kb.id, kb.name)
-      setKbStatus(`"${kb.name}" 导入请求已提交,可在样本中心查看入库进度`)
+      const res = await api.annotationPlatform.importProject(project.project_id, project.project_name, 'alpaca')
+      setProjectStatus(`"${res.dataset_name}" 导入成功,共 ${res.total_exported} 条数据`)
       fetchDatasets()
     } catch (err: unknown) {
       const msg = err instanceof Error ? err.message : '导入失败'
-      setKbStatus(`导入失败: ${msg}`)
+      setProjectStatus(`导入失败: ${msg}`)
     } finally {
-      setKbImporting(null)
+      setProjectImporting(null)
     }
   }
 
@@ -398,47 +398,47 @@ export function Datasets() {
         </div>
       )}
 
-      {/* Sample Center section */}
+      {/* Annotation Platform section */}
       <div style={{
         marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
         boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
       }}>
-        <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>样本中心</h2>
+        <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>标注平台</h2>
         <p style={{ fontSize: 13, color: '#64748b', margin: '0 0 12px' }}>
-          从样本中心导入知识库数据作为训练数据集
+          从标注平台导入已完成的标注项目作为训练数据集
         </p>
         <button
-          onClick={() => { setShowSampleCenter(true); fetchKnowledgeBases(1); }}
+          onClick={() => { setShowAnnotationPlatform(true); fetchProjects(1); }}
           style={{
             padding: '10px 20px', borderRadius: 8, border: 'none',
             background: '#8b5cf6', color: '#fff', cursor: 'pointer', fontSize: 14, fontWeight: 600,
           }}
         >
           <FolderOpen size={16} style={{ display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />
-          从样本中心导入
+          从标注平台导入
         </button>
       </div>
 
-      {/* Sample Center Modal */}
-      {showSampleCenter && (
+      {/* Annotation Platform Modal */}
+      {showAnnotationPlatform && (
         <div
           style={{
             position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
             display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
           }}
-          onClick={() => setShowSampleCenter(false)}
+          onClick={() => setShowAnnotationPlatform(false)}
         >
           <div
             onClick={e => e.stopPropagation()}
             style={{
-              background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 700,
+              background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 750,
               maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
             }}
           >
             <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
-              <h2 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>样本中心 - 知识库列表</h2>
+              <h2 style={{ margin: 0, fontSize: 17, fontWeight: 600 }}>标注平台 - 项目列表</h2>
               <button
-                onClick={() => setShowSampleCenter(false)}
+                onClick={() => setShowAnnotationPlatform(false)}
                 style={{
                   border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 20,
                   color: '#64748b', padding: '4px 8px', borderRadius: 4,
@@ -447,108 +447,124 @@ export function Datasets() {
             </div>
 
             <p style={{ fontSize: 13, color: '#64748b', margin: '0 0 16px' }}>
-              选择要导入的知识库,数据将转为训练格式
+              选择要导入的标注项目,文本项目将转为 Alpaca 格式训练数据
             </p>
 
-            {kbLoading && (
+            {projectLoading && (
               <div style={{ textAlign: 'center', padding: 20, color: '#94a3b8' }}>
                 <Loader2 size={24} style={{ animation: 'lucide-spin 1s linear infinite' }} />
                 <div style={{ marginTop: 8, fontSize: 13 }}>加载中...</div>
               </div>
             )}
 
-            {!kbLoading && kbList.length === 0 && (
+            {!projectLoading && projectList.length === 0 && (
               <div style={{ padding: 20, textAlign: 'center', color: '#94a3b8', fontSize: 14 }}>
-                暂无可用的知识库
+                暂无可用的标注项目
               </div>
             )}
 
-            {!kbLoading && kbList.length > 0 && (
+            {!projectLoading && projectList.length > 0 && (
               <div style={{ border: '1px solid #e2e8f0', borderRadius: 8, overflow: 'hidden' }}>
                 <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
                   <thead>
                     <tr style={{ background: '#f5f3ff', borderBottom: '2px solid #e2e8f0', textAlign: 'left' }}>
-                      <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>名称</th>
-                      <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>文档数</th>
+                      <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>项目名称</th>
+                      <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>类型</th>
                       <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>状态</th>
-                      <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>字段</th>
+                      <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>进度</th>
                       <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>操作</th>
                     </tr>
                   </thead>
                   <tbody>
-                    {kbList.map(kb => (
-                      <tr key={kb.id} style={{ borderBottom: '1px solid #f1f5f9' }}>
-                        <td style={{ padding: '10px 12px', fontWeight: 500 }}>{kb.name}</td>
-                        <td style={{ padding: '10px 12px', fontSize: 13 }}>{kb.document_count}</td>
-                        <td style={{ padding: '10px 12px', fontSize: 13 }}>
-                          <span style={{
-                            display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
-                            background: kb.status === 1 ? '#dcfce7' : '#f1f5f9',
-                            color: kb.status === 1 ? '#16a34a' : '#64748b',
-                          }}>
-                            {kb.status === 1 ? '启用' : '禁用'}
-                          </span>
-                        </td>
-                        <td style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', maxWidth: 200 }}>
-                          {kb.metadata_schema.slice(0, 3).map(f => f.field_name_cn).join('、')}
-                          {kb.metadata_schema.length > 3 ? '...' : ''}
-                        </td>
-                        <td style={{ padding: '10px 12px' }}>
-                          <button
-                            onClick={() => handleImportFromKB(kb)}
-                            disabled={kbImporting === kb.id}
-                            style={{
-                              padding: '4px 12px', color: '#8b5cf6', border: '1px solid #8b5cf6',
-                              borderRadius: 6, background: kbImporting === kb.id ? '#f5f3ff' : 'transparent',
-                              cursor: kbImporting === kb.id ? 'not-allowed' : 'pointer',
-                              fontSize: 12, fontWeight: 500, opacity: kbImporting === kb.id ? 0.7 : 1,
-                            }}
-                          >
-                            {kbImporting === kb.id ? (
-                              <><Loader2 size={12} style={{ animation: 'lucide-spin 1s linear infinite', display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />导入中</>
-                            ) : '导入'}
-                          </button>
-                        </td>
-                      </tr>
-                    ))}
+                    {projectList.map(p => {
+                      const isText = p.project_type === 'text'
+                      const progress = p.task_count > 0 ? Math.round((p.completed_task_count / p.task_count) * 100) : 0
+                      return (
+                        <tr key={p.project_id} style={{ borderBottom: '1px solid #f1f5f9' }}>
+                          <td style={{ padding: '10px 12px', fontWeight: 500 }}>{p.project_name}</td>
+                          <td style={{ padding: '10px 12px' }}>
+                            <span style={{
+                              display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
+                              background: isText ? '#dbeafe' : '#fef3c7',
+                              color: isText ? '#2563eb' : '#d97706',
+                            }}>
+                              {isText ? '文本' : '图片'}
+                            </span>
+                          </td>
+                          <td style={{ padding: '10px 12px', fontSize: 13 }}>
+                            <span style={{
+                              display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
+                              background: p.status === 'completed' ? '#dcfce7' : p.status === 'in_progress' ? '#dbeafe' : '#f1f5f9',
+                              color: p.status === 'completed' ? '#16a34a' : p.status === 'in_progress' ? '#2563eb' : '#64748b',
+                            }}>
+                              {p.status === 'completed' ? '已完成' : p.status === 'in_progress' ? '进行中' : p.status}
+                            </span>
+                          </td>
+                          <td style={{ padding: '10px 12px', fontSize: 13, color: '#64748b' }}>
+                            {p.completed_task_count}/{p.task_count}
+                            <span style={{ marginLeft: 4, fontSize: 11, color: '#94a3b8' }}>({progress}%)</span>
+                          </td>
+                          <td style={{ padding: '10px 12px' }}>
+                            {isText ? (
+                              <button
+                                onClick={() => handleImportProject(p)}
+                                disabled={projectImporting === p.project_id}
+                                style={{
+                                  padding: '4px 12px', color: '#8b5cf6', border: '1px solid #8b5cf6',
+                                  borderRadius: 6, background: projectImporting === p.project_id ? '#f5f3ff' : 'transparent',
+                                  cursor: projectImporting === p.project_id ? 'not-allowed' : 'pointer',
+                                  fontSize: 12, fontWeight: 500, opacity: projectImporting === p.project_id ? 0.7 : 1,
+                                }}
+                              >
+                                {projectImporting === p.project_id ? (
+                                  <><Loader2 size={12} style={{ animation: 'lucide-spin 1s linear infinite', display: 'inline', verticalAlign: 'middle', marginRight: 4 }} />导入中</>
+                                ) : '导入'}
+                              </button>
+                            ) : (
+                              <span style={{ fontSize: 12, color: '#94a3b8' }}>不支持训练</span>
+                            )}
+                          </td>
+                        </tr>
+                      )
+                    })}
                   </tbody>
                 </table>
               </div>
             )}
 
-            {!kbLoading && kbTotal > 20 && (
+            {!projectLoading && projectTotal > 20 && (
               <div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 16, alignItems: 'center' }}>
                 <button
-                  disabled={kbPage <= 1}
-                  onClick={() => fetchKnowledgeBases(kbPage - 1)}
+                  disabled={projectPage <= 1}
+                  onClick={() => fetchProjects(projectPage - 1)}
                   style={{
                     padding: '4px 12px', borderRadius: 6, border: '1px solid #cbd5e1',
-                    background: '#fff', cursor: kbPage <= 1 ? 'not-allowed' : 'pointer',
-                    opacity: kbPage <= 1 ? 0.5 : 1, fontSize: 13,
+                    background: '#fff', cursor: projectPage <= 1 ? 'not-allowed' : 'pointer',
+                    opacity: projectPage <= 1 ? 0.5 : 1, fontSize: 13,
                   }}
                 >上一页</button>
                 <span style={{ fontSize: 13, color: '#64748b' }}>
-                  第 {kbPage} 页 / 共 {Math.ceil(kbTotal / 20)} 页
+                  第 {projectPage} 页 / 共 {Math.ceil(projectTotal / 20)} 页
                 </span>
                 <button
-                  disabled={kbPage * 20 >= kbTotal}
-                  onClick={() => fetchKnowledgeBases(kbPage + 1)}
+                  disabled={projectPage * 20 >= projectTotal}
+                  onClick={() => fetchProjects(projectPage + 1)}
                   style={{
                     padding: '4px 12px', borderRadius: 6, border: '1px solid #cbd5e1',
-                    background: '#fff', cursor: kbPage * 20 >= kbTotal ? 'not-allowed' : 'pointer',
-                    opacity: kbPage * 20 >= kbTotal ? 0.5 : 1, fontSize: 13,
+                    background: '#fff', cursor: projectPage * 20 >= projectTotal ? 'not-allowed' : 'pointer',
+                    opacity: projectPage * 20 >= projectTotal ? 0.5 : 1, fontSize: 13,
                   }}
                 >下一页</button>
               </div>
             )}
 
-            {kbStatus && (
+            {projectStatus && (
               <p style={{
                 marginTop: 12, padding: '8px 12px', borderRadius: 6, fontSize: 13,
-                background: kbStatus.includes('失败') ? '#fff1f2' : '#f0fdf4',
-                color: kbStatus.includes('失败') ? '#e11d48' : '#16a34a',
-                border: `1px solid ${kbStatus.includes('失败') ? '#fecdd3' : '#bbf7d0'}`,
-              }}>{kbStatus}</p>
+                background: projectStatus.includes('失败') ? '#fff1f2' : '#f0fdf4',
+                color: projectStatus.includes('失败') ? '#e11d48' : '#16a34a',
+                border: `1px solid ${projectStatus.includes('失败') ? '#fecdd3' : '#bbf7d0'}`,
+              }}>{projectStatus}</p>
             )}
           </div>
         </div>

+ 7 - 73
result.txt

@@ -1,73 +1,7 @@
-(base) [root@localhost ~]# docker exec finetune-trainer ps aux | grep python | grep -v grep
-root         372  5.7  1.9 57650712 10328360 ?   Sl   10:33   2:34 /opt/conda/bin/python inference_worker.py --model-path /root/Fine-tuning/backend/data/adapters/3819e7af-6c9b-4fde-88d0-35784e6afeda_merged --port 8100
-root         504  0.6  1.6 17399148 8919172 ?    Sl   10:33   0:17 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         842  0.0  0.9 17102096 4842448 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         844  0.0  0.9 17102096 4842248 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         846  0.0  0.9 17102096 4842252 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         848  0.0  0.9 17102096 4842252 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         851  0.0  0.9 17102096 4842252 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         853  0.0  0.9 17102096 4842260 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         855  0.0  0.9 17102096 4842260 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         857  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         859  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         861  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         863  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         865  0.0  0.9 17102096 4842268 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         867  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         869  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         871  0.0  0.9 17102096 4842268 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         873  0.0  0.9 17102096 4842268 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         875  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         877  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         879  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         881  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         883  0.0  0.9 17102096 4842276 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         885  0.0  0.9 17102096 4842276 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         887  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         889  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         891  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         893  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         895  0.0  0.9 17102096 4841776 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         897  0.0  0.9 17102096 4841776 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         899  0.0  0.9 17102096 4841784 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         901  0.0  0.9 17102096 4841764 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         903  0.0  0.9 17102096 4842292 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         905  0.0  0.9 17102096 4842292 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root        4661 52.8  0.0      0     0 ?        Z    11:15   1:19 [python] <defunct>
-(base) [root@localhost ~]# docker exec finetune-trainer ps aux | grep python | grep -v grep
-root         372  5.3  1.9 57650712 10328360 ?   Sl   10:33   2:35 /opt/conda/bin/python inference_worker.py --model-path /root/Fine-tuning/backend/data/adapters/3819e7af-6c9b-4fde-88d0-35784e6afeda_merged --port 8100
-root         504  0.6  1.6 17399148 8919172 ?    Sl   10:33   0:17 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         842  0.0  0.9 17102096 4842448 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         844  0.0  0.9 17102096 4842248 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         846  0.0  0.9 17102096 4842252 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         848  0.0  0.9 17102096 4842252 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         851  0.0  0.9 17102096 4842252 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         853  0.0  0.9 17102096 4842260 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         855  0.0  0.9 17102096 4842260 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         857  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         859  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         861  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         863  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         865  0.0  0.9 17102096 4842268 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         867  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         869  0.0  0.9 17102096 4842264 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         871  0.0  0.9 17102096 4842268 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         873  0.0  0.9 17102096 4842268 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         875  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         877  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         879  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         881  0.0  0.9 17102096 4842272 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         883  0.0  0.9 17102096 4842276 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         885  0.0  0.9 17102096 4842276 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         887  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         889  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         891  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         893  0.0  0.9 17102096 4842280 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         895  0.0  0.9 17102096 4841776 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         897  0.0  0.9 17102096 4841776 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         899  0.0  0.9 17102096 4841784 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         901  0.0  0.9 17102096 4841764 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         903  0.0  0.9 17102096 4842292 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root         905  0.0  0.9 17102096 4842292 ?    Sl   10:33   0:00 /opt/conda/bin/python /opt/conda/lib/python3.10/site-packages/torch/_inductor/compile_worker/__main__.py --pickler=torch._inductor.compile_worker.subproc_pool.SubprocPickler --kind=fork --workers=32 --parent=372 --read-fd=7 --write-fd=10 --torch-key=kdnYoFpyXJfmeFh07c0N00WVSuau0TZN11yUZqCrSHo=
-root        4661 21.5  0.0      0     0 ?        Z    11:15   1:19 [python] <defunct>
-root        5234 42.1  0.0      0     0 ?        Z    11:18   1:21 [python] <defunct>
+NFO:     172.20.0.4:53454 - "GET /api/v1/training/jobs HTTP/1.0" 200 OK
+2026-05-26 08:47:52 | ERROR    | peft-platform | Remote job ef093221-dfbf-4e0e-af51-eb08d29803ec failed: AdaLoraModel supports only 1 trainable adapter. When using multiple adapters, set inference_mode to True for all adapters except the one you want to train.
+INFO:     172.20.0.4:56038 - "GET /api/v1/training/jobs HTTP/1.0" 200 OK
+INFO:     172.20.0.4:56044 - "GET /api/v1/training/jobs HTTP/1.0" 200 OK
+2026-05-26 08:48:02 | ERROR    | peft-platform | SSH command timeout after 10s: docker exec finetune-trainer bash -c 'kill -9 12111 2>/dev/null; pkill -9 -P 12111 2>/dev/null'
+2026-05-26 08:48:02 | INFO     | peft-platform | Killed remote process 12111 via docker exec
+2026-05-26 08:48:02 | INFO     | peft-platform | Remote training launched for job ef093221-dfbf-4e0e-af51-eb08d29803ec

+ 504 - 0
标注平台对外API接口文档.md

@@ -0,0 +1,504 @@
+# 标注平台对外 API 接口文档
+
+> 版本:v1.0
+> 更新日期:2026-05-20
+> 基础 URL:`http://{host}:{port}`(部署后由标注平台提供具体地址)
+
+---
+
+## 1. 快速开始
+
+### 1.1 接入流程
+
+1. 向标注平台申请接入,获取 `app_id` 和 `app_secret`
+2. 使用签名算法获取 Access Token
+3. 携带 Token 调用业务接口
+
+### 1.2 所有接口统一前缀
+
+```
+/api/v1/open
+```
+
+### 1.3 统一响应格式
+
+所有接口返回如下 JSON 结构:
+
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": { ... }
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `code` | int | 业务状态码,`0` 表示成功,非 `0` 表示失败 |
+| `message` | string | 提示信息 |
+| `data` | object | 业务数据,失败时为 `null` |
+
+---
+
+## 2. 认证机制
+
+### 2.1 凭证说明
+
+接入标注平台后,会分配一对凭证:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `app_id` | string | 应用唯一标识 |
+| `app_secret` | string | 应用密钥,**仅在创建时展示一次,请妥善保存** |
+
+### 2.2 签名算法
+
+每次请求获取 Token 时,需使用 HMAC-SHA256 对请求参数签名:
+
+```
+signature = HMAC-SHA256(
+    key = app_secret,
+    message = app_id + timestamp + nonce
+)
+```
+
+参数说明:
+
+| 参数 | 说明 |
+|------|------|
+| `timestamp` | 当前 Unix 时间戳(秒),与服务器时间差不能超过 5 分钟 |
+| `nonce` | 16 位随机字符串,每次请求不同,用于防重放 |
+| `signature` | 十六进制编码的签名结果 |
+
+---
+
+## 3. 接口列表
+
+| 序号 | 方法 | 路径 | 说明 | 认证方式 |
+|------|------|------|------|----------|
+| 1 | POST | `/api/v1/open/auth/token` | 获取 Access Token | API Key 签名 |
+| 2 | POST | `/api/v1/open/auth/refresh` | 刷新 Access Token | Bearer Token |
+| 3 | GET | `/api/v1/open/projects` | 查询项目列表 | Bearer Token |
+| 4 | GET | `/api/v1/open/projects/{project_id}` | 查询项目详情 | Bearer Token |
+| 5 | POST | `/api/v1/open/projects/{project_id}/datasets/download` | 数据集下载 | Bearer Token |
+| 6 | GET | `/api/v1/open/datasets/downloads/{download_token}` | 获取下载文件 | Bearer Token |
+
+---
+
+## 4. 接口详情
+
+### 4.1 获取 Access Token
+
+**请求**
+
+```
+POST /api/v1/open/auth/token
+```
+
+**请求头**
+
+| Header | 类型 | 必录 | 说明 |
+|--------|------|------|------|
+| `X-Api-Key` | string | 是 | 申请的 `app_id` |
+| `X-Signature` | string | 是 | HMAC-SHA256 签名(十六进制) |
+| `X-Timestamp` | string | 是 | Unix 时间戳(秒) |
+| `X-Nonce` | string | 是 | 16 位随机字符串 |
+
+**请求体**:无
+
+**响应示例**
+
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "access_token": "eyJhbGciOiJIUzI1NiIs...",
+    "token_type": "Bearer",
+    "expires_in": 7200
+  }
+}
+```
+
+**错误码**
+
+| HTTP 状态码 | error_code | 说明 |
+|-------------|-----------|------|
+| 401 | INVALID_API_KEY | app_id 不存在 |
+| 401 | APP_DISABLED | 应用已被禁用 |
+| 401 | INVALID_SIGNATURE | 签名验证失败 |
+| 401 | TIMESTAMP_EXPIRED | 时间戳过期(超过 5 分钟) |
+| 401 | NONCE_USED | Nonce 已被使用 |
+
+---
+
+### 4.2 刷新 Access Token
+
+**请求**
+
+```
+POST /api/v1/open/auth/refresh
+```
+
+**请求头**
+
+| Header | 类型 | 必录 | 说明 |
+|--------|------|------|------|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
+
+**请求体**:无
+
+**响应示例**
+
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "access_token": "eyJhbGciOiJIUzI1NiIs...",
+    "token_type": "Bearer",
+    "expires_in": 7200
+  }
+}
+```
+
+---
+
+### 4.3 查询项目列表
+
+**请求**
+
+```
+GET /api/v1/open/projects
+```
+
+**请求头**
+
+| Header | 类型 | 必录 | 说明 |
+|--------|------|------|------|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
+
+**查询参数**
+
+| 参数 | 类型 | 必录 | 说明 |
+|------|------|------|------|
+| `name` | string | 否 | 项目名称(模糊匹配) |
+| `type` | string | 否 | 项目类型:`image`(图片)/ `text`(文本) |
+| `status` | string | 否 | 项目状态筛选 |
+| `page` | int | 否 | 页码,默认 1 |
+| `page_size` | int | 否 | 每页数量,默认 20,最大 100 |
+
+**响应示例**
+
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "items": [
+      {
+        "project_id": "proj_abc123def456",
+        "project_name": "商品图片分类标注",
+        "description": "对电商平台商品图片进行分类标注",
+        "project_type": "image",
+        "task_type": "image_classification",
+        "status": "in_progress",
+        "created_by": "admin",
+        "created_at": "2026-05-10T10:30:00",
+        "updated_at": "2026-05-15T14:20:00",
+        "task_count": 500,
+        "completed_task_count": 350
+      }
+    ],
+    "total": 1,
+    "page": 1,
+    "page_size": 20,
+    "total_pages": 1,
+    "has_next": false,
+    "has_prev": false
+  }
+}
+```
+
+**字段说明**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `project_id` | string | 项目唯一标识 |
+| `project_name` | string | 项目名称 |
+| `description` | string | 项目描述 |
+| `project_type` | string | 项目类型:`image`(图片)/ `text`(文本) |
+| `task_type` | string | 具体任务类型(见下方映射表) |
+| `status` | string | 项目状态:`draft` / `configuring` / `ready` / `in_progress` / `completed` |
+| `created_by` | string | 项目创建人用户名 |
+| `created_at` | string | 创建时间(ISO 8601) |
+| `updated_at` | string | 最后更新时间(ISO 8601) |
+| `task_count` | int | 总任务数 |
+| `completed_task_count` | int | 已完成任务数 |
+
+**任务类型映射表**
+
+| `task_type` | `project_type` | 说明 |
+|-------------|---------------|------|
+| `text_classification` | `text` | 文本分类 |
+| `ner` | `text` | 命名实体识别 |
+| `image_classification` | `image` | 图片分类 |
+| `object_detection` | `image` | 目标检测 |
+| `polygon` | `image` | 多边形标注 |
+
+---
+
+### 4.4 查询项目详情
+
+**请求**
+
+```
+GET /api/v1/open/projects/{project_id}
+```
+
+**路径参数**
+
+| 参数 | 类型 | 必录 | 说明 |
+|------|------|------|------|
+| `project_id` | string | 是 | 项目 ID |
+
+**请求头**
+
+| Header | 类型 | 必录 | 说明 |
+|--------|------|------|------|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
+
+**响应示例**
+
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "project_id": "proj_abc123def456",
+    "project_name": "商品图片分类标注",
+    "description": "对电商平台商品图片进行分类标注",
+    "project_type": "image",
+    "task_type": "image_classification",
+    "status": "in_progress",
+    "created_by": "admin",
+    "created_at": "2026-05-10T10:30:00",
+    "updated_at": "2026-05-15T14:20:00",
+    "task_count": 500,
+    "completed_task_count": 350,
+    "assigned_task_count": 500,
+    "completion_percentage": 70.0
+  }
+}
+```
+
+**错误码**
+
+| HTTP 状态码 | error_code | 说明 |
+|-------------|-----------|------|
+| 404 | PROJECT_NOT_FOUND | 项目不存在 |
+
+---
+
+### 4.5 数据集下载
+
+**请求**
+
+```
+POST /api/v1/open/projects/{project_id}/datasets/download
+```
+
+**路径参数**
+
+| 参数 | 类型 | 必录 | 说明 |
+|------|------|------|------|
+| `project_id` | string | 是 | 项目 ID |
+
+**请求头**
+
+| Header | 类型 | 必录 | 说明 |
+|--------|------|------|------|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
+
+**请求体**
+
+| 参数 | 类型 | 必录 | 说明 |
+|------|------|------|------|
+| `format` | string | 是 | 数据集格式(见下方格式说明) |
+| `completed_only` | boolean | 否 | 是否只导出已完成的任务,默认 `true` |
+
+**支持的数据集格式**
+
+| `format` 值 | 数据类型 | 格式说明 |
+|-------------|---------|---------|
+| `alpaca` | 文本 | Alpaca 指令微调格式 |
+| `sharegpt` | 文本 | ShareGPT 对话格式 |
+| `json` | 图片 | 平台原生 JSON 格式 |
+| `csv` | 图片 | CSV 表格格式 |
+| `coco` | 图片 | COCO 目标检测格式 |
+| `yolo` | 图片 | YOLO 目标检测格式 |
+| `pascal_voc` | 图片 | PascalVOC XML 格式 |
+
+**请求示例**
+
+```json
+{
+  "format": "coco",
+  "completed_only": true
+}
+```
+
+**响应示例**
+
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "project_id": "proj_abc123def456",
+    "format": "coco",
+    "total_exported": 350,
+    "file_url": "/api/v1/open/datasets/downloads/dl_abc123",
+    "file_name": "proj_abc123def456_coco_20260517_143000.json",
+    "file_size": 2048576,
+    "expires_at": "2026-05-17T16:30:00",
+    "status": "completed"
+  }
+}
+```
+
+**字段说明**
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `project_id` | string | 项目 ID |
+| `format` | string | 导出格式 |
+| `total_exported` | int | 导出的任务数量 |
+| `file_url` | string | 文件下载链接(相对路径,需拼接基础 URL) |
+| `file_name` | string | 文件名 |
+| `file_size` | int | 文件大小(字节) |
+| `expires_at` | string | 下载链接过期时间(ISO 8601),2 小时后过期 |
+| `status` | string | 导出状态:`completed` / `failed` |
+
+**错误码**
+
+| HTTP 状态码 | error_code | 说明 |
+|-------------|-----------|------|
+| 400 | INVALID_FORMAT | 不支持的导出格式 |
+| 400 | FORMAT_NOT_COMPATIBLE | 格式与项目类型不兼容 |
+| 404 | PROJECT_NOT_FOUND | 项目不存在 |
+| 404 | NO_DATA_AVAILABLE | 项目中没有可导出的数据 |
+| 500 | EXPORT_FAILED | 导出失败 |
+
+---
+
+### 4.6 获取下载文件
+
+**请求**
+
+```
+GET /api/v1/open/datasets/downloads/{download_token}
+```
+
+拿到 4.5 接口返回的 `file_url` 后,调用此接口下载实际文件。
+
+**路径参数**
+
+| 参数 | 类型 | 必录 | 说明 |
+|------|------|------|------|
+| `download_token` | string | 是 | 下载令牌(从 4.5 接口返回的 `file_url` 中提取) |
+
+**请求头**
+
+| Header | 类型 | 必录 | 说明 |
+|--------|------|------|------|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
+
+**响应**
+
+- `Content-Type`: `application/octet-stream` 或对应格式的 MIME type
+- `Content-Disposition`: `attachment; filename="..."`
+- 响应体为实际文件内容
+
+**错误码**
+
+| HTTP 状态码 | error_code | 说明 |
+|-------------|-----------|------|
+| 410 | DOWNLOAD_EXPIRED | 下载链接已过期 |
+| 404 | NOT_FOUND | 下载资源不存在 |
+
+---
+
+## 5. 错误码汇总
+
+### 5.1 通用错误码
+
+| code | error_code | HTTP 状态码 | 说明 |
+|------|-----------|------------|------|
+| 0 | SUCCESS | 200 | 请求成功 |
+| 1000 | INTERNAL_ERROR | 500 | 服务器内部错误 |
+| 1001 | INVALID_REQUEST | 400 | 请求参数无效 |
+| 1002 | UNAUTHORIZED | 401 | 未认证或认证过期 |
+
+### 5.2 认证错误码
+
+| code | error_code | HTTP 状态码 | 说明 |
+|------|-----------|------------|------|
+| 2001 | INVALID_API_KEY | 401 | app_id 不存在 |
+| 2002 | APP_DISABLED | 401 | 应用已被禁用 |
+| 2003 | INVALID_SIGNATURE | 401 | 签名验证失败 |
+| 2004 | TIMESTAMP_EXPIRED | 401 | 时间戳过期 |
+| 2005 | NONCE_USED | 401 | Nonce 已被使用 |
+| 2006 | TOKEN_EXPIRED | 401 | Access Token 已过期 |
+| 2007 | TOKEN_INVALID | 401 | Access Token 无效 |
+
+### 5.3 业务错误码
+
+| code | error_code | HTTP 状态码 | 说明 |
+|------|-----------|------------|------|
+| 3001 | PROJECT_NOT_FOUND | 404 | 项目不存在 |
+| 3002 | INVALID_FORMAT | 400 | 不支持的导出格式 |
+| 3003 | FORMAT_NOT_COMPATIBLE | 400 | 格式与项目类型不兼容 |
+| 3004 | NO_DATA_AVAILABLE | 404 | 项目中没有可导出的数据 |
+| 3005 | EXPORT_FAILED | 500 | 导出失败 |
+| 3006 | DOWNLOAD_EXPIRED | 410 | 下载链接已过期 |
+
+---
+
+## 6. 调用示例
+
+### 6.1 curl 示例
+
+```bash
+# 1. 获取 Token
+curl -X POST http://localhost:8003/api/v1/open/auth/token \
+  -H "X-Api-Key: app_id_xxx" \
+  -H "X-Signature: abc123..." \
+  -H "X-Timestamp: 1716000000" \
+  -H "X-Nonce: a1b2c3d4e5f6g7h8"
+
+# 2. 查询项目列表
+curl -X GET "http://localhost:8003/api/v1/open/projects?page=1&page_size=20" \
+  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
+
+# 3. 下载数据集
+curl -X POST http://localhost:8003/api/v1/open/projects/proj_xxx/datasets/download \
+  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
+  -H "Content-Type: application/json" \
+  -d '{"format": "coco", "completed_only": true}'
+
+# 4. 下载文件
+curl -X GET http://localhost:8003/api/v1/open/datasets/downloads/dl_abc123 \
+  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
+```
+
+---
+
+
+## 7. 注意事项
+
+1. **Token 有效期**:Access Token 默认 2 小时过期,过期后需重新通过签名获取
+2. **下载链接有效期**:数据集下载接口返回的 `file_url` 2 小时后过期,请及时下载
+3. **Nonce 唯一性**:每次请求的 `nonce` 必须不同,重复使用会被拒绝
+4. **时间同步**:请求时间戳与服务器时间差不能超过 5 分钟,请确保客户端时钟准确
+5. **格式兼容**:文本项目只能使用 `alpaca` / `sharegpt` 格式,图片项目只能使用 `json` / `csv` / `coco` / `yolo` / `pascal_voc` 格式