Files
ai-kefu-admin/app/platform/routes/site_import_routes.py
T

106 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""site_import API(spec §4)——2 端点,两角色可用,_tenant_guard 守租户。
- POST /{slug}/site-import:新建(body.url)或复活(body.retry="generate"
+ body.task_id)。校验次序:鉴权(_tenant_guard 依赖链,401/403/409 门
连锁)→ 租户存在且未停用(404/403)→ 形态预检(retry 只认 generate 且
task_id 非空;新建 URL 过 safe_fetch 的 URL 层预检——scheme/host 字面量
禁段/端口白名单,spec §5「内网/非法地址 POST 即拒」,连接层二次兜底)
→ task.start。异常映射(Task 7 备忘):ImportRateLimited→429 /
ConcurrentLimit→409 / LookupError→404(retry 目标不存在)/ ValueError→
409(非 generate-failed 态),str(exc) 作 detail——任务层的白话文案
原样透出,API 层不二次翻译(FetchError 已在任务层白话化,同理由)。
- GET /{slug}/site-import/{task_id}:轮询。task.get 的 (slug, task_id)
联合索引即越权防线(跨租户持他人 id → None → 404);30 分钟过期同理
404「任务不存在或已过期」(前端按终态「任务已过期,请重新导入」处理)。
响应五键(spec §4):status(=任务 state)/ phase / pages(=任务
pages_fetched)/ draft(仅 done,spec §4 草稿包整包)/ error。
启动只做校验+建任务——pipeline 由任务模块的后台 aio task 跑,本层不
感知 crawler/generator(测试也因此能整体打桩 task.start/task.get)。
"""
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.platform.db import TenantConfigVersion
from app.platform.routes.tenant_routes import _get_tenant_or_404, _tenant_guard
from app.platform.site_import import safe_fetch, task
router = APIRouter(prefix="/api/platform/tenants", tags=["platform-site-import"])
class StartRequest(BaseModel):
url: str = ""
retry: str = "" # "generate" 复活模式(spec §3.3)
task_id: str = "" # retry 时指定任务
@router.post("/{slug}/site-import")
async def start_import(slug: str, body: StartRequest, request: Request,
user=Depends(_tenant_guard)):
"""启动/复活导入任务 → {task_id}。校验次序与异常映射见模块 docstring。"""
engine = request.app.state.platform_engine
async with AsyncSession(engine, expire_on_commit=False) as session:
tenant = await _get_tenant_or_404(session, slug)
# 租户配置快照(终审 Important #2:AI 三级链的租户槽位——传给
# task.start → make_provider;自有 ai.* 配置的租户烧自己配额)
latest = (await session.execute(
select(TenantConfigVersion.config_json)
.where(TenantConfigVersion.tenant_id == tenant.id)
.order_by(TenantConfigVersion.version.desc())
.limit(1))).scalar_one_or_none()
if tenant.status != "active":
raise HTTPException(
status_code=403, detail=f"租户 {slug!r} 已停用,无法发起导入")
url: str | None = None
retry_task_id: str | None = None
if body.retry:
if body.retry != "generate":
raise HTTPException(
status_code=400,
detail=f"retry 只支持 generate(实际 {body.retry!r})")
if not body.task_id:
raise HTTPException(
status_code=400, detail="retry 形态必须带 task_id")
retry_task_id = body.task_id
else:
# URL 层预检单一事实源在 safe_fetch(scheme/禁段字面量/端口)——
# 私有口内部复用,不另抄一份漂移;不公网形态统一白话 400
try:
safe_fetch._validate_url(body.url)
except safe_fetch.FetchError as exc:
raise HTTPException(
status_code=400, detail="仅支持公网网站地址") from exc
url = body.url
try:
task_id = await task.start(slug, url, retry_task_id=retry_task_id,
config_data=latest)
except task.ImportRateLimited as exc:
raise HTTPException(status_code=429, detail=str(exc)) from exc
except task.ConcurrentLimit as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except LookupError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return {"task_id": task_id}
@router.get("/{slug}/site-import/{task_id}")
async def get_import_task(slug: str, task_id: str,
user=Depends(_tenant_guard)):
"""轮询任务状态(spec §4 五键;404=不存在/已过期/跨租户)。"""
t = task.get(slug, task_id)
if t is None:
raise HTTPException(status_code=404, detail="任务不存在或已过期")
return {
"status": t.state,
"phase": t.phase,
"pages": t.pages_fetched,
"draft": t.draft if t.state == task.TaskState.DONE else None,
"error": t.error,
}