This commit is contained in:
Your Name
2026-04-16 21:35:47 +08:00
parent ff32aa50bf
commit ebf632e651
86 changed files with 14097 additions and 585 deletions

View File

@@ -1,25 +1,151 @@
from uuid import uuid4
from fastapi import APIRouter
from app.schemas.common import ApiResponse
from app.services.detect_job_service import (
append_detect_job_event,
create_detect_job_if_needed,
get_active_detect_job_summary,
get_detect_job_summary,
get_detect_queue_health,
list_detect_jobs,
)
from app.services.detect_service import get_detect_status
from app.services.worker_control_service import start_worker, stop_worker
from app.services.detect_run_service import create_detect_run_snapshot, finalize_detect_run, mark_detect_run_stopping
from app.services.settings_service import get_settings_payload, resolve_thread_count
from app.services.worker_control_service import send_worker_command, start_worker
router = APIRouter(tags=["detect"])
def _build_settings_summary(settings_payload: dict) -> dict:
thread_count_resolution = resolve_thread_count(settings_payload=settings_payload)
return {
"thread_count": int(thread_count_resolution["effective_thread_count"]),
"thread_count_default": int(thread_count_resolution["default_thread_count"]),
"thread_count_source": str(thread_count_resolution["source"]),
"thread_count_override": thread_count_resolution["override_thread_count"],
"thread_count_node_code": str(thread_count_resolution["node_code"]),
"proxy_enable": settings_payload["proxy_config"].get("proxy_enable", False),
"allow_direct": settings_payload["proxy_config"].get("allow_direct", False),
"proxy_pool_count": len(settings_payload["proxy_config"].get("proxy_urls", [])),
}
@router.get("/detect/status", response_model=ApiResponse)
def detect_status() -> ApiResponse:
return ApiResponse(data=get_detect_status())
@router.get("/detect/job/active", response_model=ApiResponse)
def detect_active_job() -> ApiResponse:
return ApiResponse(data=get_active_detect_job_summary(event_limit=50))
@router.get("/detect/jobs", response_model=ApiResponse)
def detect_jobs(limit: int = 20) -> ApiResponse:
return ApiResponse(data={"items": list_detect_jobs(limit=limit), "limit": max(1, min(int(limit or 20), 100))})
@router.get("/detect/queue-summary", response_model=ApiResponse)
def detect_queue_summary(window_minutes: int = 15) -> ApiResponse:
return ApiResponse(data=get_detect_queue_health(window_minutes=window_minutes))
@router.get("/detect/jobs/{job_id}", response_model=ApiResponse)
def detect_job_detail(job_id: int) -> ApiResponse:
data = get_detect_job_summary(job_id, event_limit=100)
if not data:
return ApiResponse(code=1, message="检测任务不存在", data=None)
return ApiResponse(data=data)
@router.post("/detect/start", response_model=ApiResponse)
def start_detect() -> ApiResponse:
job_summary = create_detect_job_if_needed(limit=1000, created_by="api")
if not job_summary:
return ApiResponse(
code=0,
message="当前没有可创建的检测任务",
data={
"action": "start",
"job": None,
"poll_after_seconds": 2,
"refresh_status": True,
},
)
cycle_token = uuid4().hex[:10]
append_detect_job_event(
job_summary["job_id"],
event_type="job_dispatch_requested",
message="控制面已发送检测启动请求",
payload={
"cycle_token": cycle_token,
"status": job_summary.get("status"),
"items_pending": job_summary.get("items_pending", 0),
"items_claimed": job_summary.get("items_claimed", 0),
"items_running": job_summary.get("items_running", 0),
},
)
ok, message = start_worker()
if not ok:
append_detect_job_event(
job_summary["job_id"],
event_type="job_dispatch_failed",
level="error",
message=f"启动 Worker 失败: {message}",
payload={"cycle_token": cycle_token},
)
return ApiResponse(
code=1,
message=message,
data={
"action": "start",
"job": job_summary,
"poll_after_seconds": 2,
"refresh_status": True,
},
)
command_ok, command_message = send_worker_command(
"start_detection",
payload={
"cycle_token": cycle_token,
"job_id": job_summary["job_id"],
"job_code": job_summary["job_code"],
},
)
append_detect_job_event(
job_summary["job_id"],
event_type="job_dispatch_sent" if command_ok else "job_dispatch_rejected",
level="info" if command_ok else "error",
message=command_message,
payload={"cycle_token": cycle_token},
)
snapshot = get_detect_status()
settings_payload = get_settings_payload()
settings_summary = _build_settings_summary(settings_payload)
if command_ok:
create_detect_run_snapshot(
message=f"{message}{command_message}",
runtime={
"mode": snapshot.get("worker_mode", ""),
"running": snapshot.get("worker_online", False),
"process_count": snapshot.get("worker_process_count", 0),
"latest_start_time": snapshot.get("worker_latest_start_time", ""),
"message": snapshot.get("worker_runtime_message", ""),
},
progress=snapshot.get("progress", {}),
settings_summary=settings_summary,
)
return ApiResponse(
code=0 if ok else 1,
message=message,
code=0 if command_ok else 1,
message=f"{message}{command_message}" if command_ok else command_message,
data={
"action": "start",
"job": job_summary,
"poll_after_seconds": 2,
"refresh_status": True,
},
@@ -28,7 +154,45 @@ def start_detect() -> ApiResponse:
@router.post("/detect/stop", response_model=ApiResponse)
def stop_detect() -> ApiResponse:
ok, message = stop_worker()
active_job = get_active_detect_job_summary(event_limit=10)
ok, message = send_worker_command("stop_detection")
if active_job:
append_detect_job_event(
active_job["job_id"],
event_type="job_stop_requested" if ok else "job_stop_request_failed",
level="info" if ok else "error",
message=message,
payload={"cycle_token": active_job.get("current_cycle_token", "")},
)
snapshot = get_detect_status()
settings_payload = get_settings_payload()
settings_summary = _build_settings_summary(settings_payload)
mark_detect_run_stopping(
message=message,
runtime={
"mode": snapshot.get("worker_mode", ""),
"running": snapshot.get("worker_online", False),
"process_count": snapshot.get("worker_process_count", 0),
"latest_start_time": snapshot.get("worker_latest_start_time", ""),
"message": snapshot.get("worker_runtime_message", ""),
},
progress=snapshot.get("progress", {}),
settings_summary=settings_summary,
)
if not snapshot.get("worker_online", False):
finalize_detect_run(
message=message,
runtime={
"mode": snapshot.get("worker_mode", ""),
"running": snapshot.get("worker_online", False),
"process_count": snapshot.get("worker_process_count", 0),
"latest_start_time": snapshot.get("worker_latest_start_time", ""),
"message": snapshot.get("worker_runtime_message", ""),
},
progress=snapshot.get("progress", {}),
settings_summary=settings_summary,
active_job=active_job,
)
return ApiResponse(
code=0 if ok else 1,
message=message,

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from fastapi import APIRouter, Query
from app.schemas.common import ApiResponse
from app.services.domains_service import batch_update_domains, domain_filter_options, fetch_domains
from app.services.domains_service import batch_update_domains, domain_filter_options, fetch_domain_detail, fetch_domains
router = APIRouter(tags=["domains"])
@@ -24,6 +26,8 @@ def domain_list(
beian_year: int | None = Query(default=None),
snapshot_year: str | None = Query(default=None),
website_url: str | None = Query(default=None),
company_type: str | None = Query(default=None),
source_type: int | None = Query(default=None),
backlink_gt_10: bool | None = Query(default=None),
) -> ApiResponse:
return ApiResponse(
@@ -39,11 +43,21 @@ def domain_list(
beian_year=beian_year,
snapshot_year=snapshot_year,
website_url=website_url,
company_type=company_type,
source_type=source_type,
backlink_gt_10=backlink_gt_10,
)
)
@router.get("/domains/{domain_id}", response_model=ApiResponse)
def domain_detail(domain_id: int) -> ApiResponse:
data = fetch_domain_detail(domain_id)
if not data:
return ApiResponse(code=1, message="域名不存在", data=None)
return ApiResponse(data=data)
@router.post("/domains/batch-update", response_model=ApiResponse)
def domain_batch_update(payload: dict) -> ApiResponse:
result = batch_update_domains(payload.get("domain_ids", []), payload.get("updates", {}))

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, File, UploadFile
from fastapi import APIRouter, File, Form, UploadFile
from app.schemas.common import ApiResponse
from app.services.import_task_service import create_import_task, list_import_tasks, retry_import_task
@@ -18,12 +18,24 @@ def import_tasks() -> ApiResponse:
@router.post("/imports/upload", response_model=ApiResponse)
async def upload_import(file: UploadFile = File(...)) -> ApiResponse:
async def upload_import(
file: UploadFile = File(...),
source_type: int = Form(default=7),
) -> ApiResponse:
content = await file.read()
task = create_import_task(content, file.filename or "domains.txt")
task = create_import_task(content, file.filename or "domains.txt", source_type=source_type)
return ApiResponse(message="导入任务已创建", data=task)
@router.post("/imports/text", response_model=ApiResponse)
def upload_import_text(payload: dict) -> ApiResponse:
text = str(payload.get("text") or "")
filename = str(payload.get("filename") or "manual_domains.txt")
source_type = int(payload.get("source_type") or 6)
task = create_import_task(text.encode("utf-8"), filename, source_type=source_type)
return ApiResponse(message="文本导入任务已创建", data=task)
@router.post("/imports/tasks/{task_id}/retry", response_model=ApiResponse)
def import_task_retry(task_id: str) -> ApiResponse:
task = retry_import_task(task_id)

View File

@@ -0,0 +1,93 @@
from fastapi import APIRouter, File, HTTPException, UploadFile
from app.schemas.common import ApiResponse
from app.services.juming_service import (
get_juming_preferences,
get_juming_status,
login_jucha_with_juming_cookie,
login_juming,
update_juming_preferences,
upload_juming_cookie,
)
from app.services.juming_task_service import create_juming_task, list_juming_tasks, request_stop_juming_task, retry_juming_task
router = APIRouter(tags=["juming"])
@router.get("/juming/status", response_model=ApiResponse)
def juming_status() -> ApiResponse:
return ApiResponse(data=get_juming_status())
@router.get("/juming/preferences", response_model=ApiResponse)
def juming_preferences() -> ApiResponse:
return ApiResponse(data=get_juming_preferences())
@router.put("/juming/preferences", response_model=ApiResponse)
def update_juming_preferences_route(payload: dict) -> ApiResponse:
try:
data = update_juming_preferences(payload)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="聚名采集偏好已保存", data=data)
@router.post("/juming/crawl", response_model=ApiResponse)
def juming_crawl(payload: dict) -> ApiResponse:
try:
data = create_juming_task(payload)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="聚名采集任务已创建", data=data)
@router.get("/juming/tasks", response_model=ApiResponse)
def juming_tasks() -> ApiResponse:
return ApiResponse(data=list_juming_tasks())
@router.post("/juming/tasks/{task_id}/retry", response_model=ApiResponse)
def juming_task_retry(task_id: str) -> ApiResponse:
try:
data = retry_juming_task(task_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="聚名采集任务已重新加入队列", data=data)
@router.post("/juming/tasks/{task_id}/stop", response_model=ApiResponse)
def juming_task_stop(task_id: str) -> ApiResponse:
try:
data = request_stop_juming_task(task_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="聚名采集任务已发送停止请求", data=data)
@router.post("/juming/login", response_model=ApiResponse)
def juming_login(payload: dict) -> ApiResponse:
try:
data = login_juming(str(payload.get("email") or ""), str(payload.get("password") or ""))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="聚名登录成功", data=data)
@router.post("/juming/jucha-login", response_model=ApiResponse)
def juming_jucha_login() -> ApiResponse:
try:
data = login_jucha_with_juming_cookie()
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="聚查联名登录成功", data=data)
@router.post("/juming/cookie/upload", response_model=ApiResponse)
async def juming_cookie_upload(file: UploadFile = File(...)) -> ApiResponse:
content = await file.read()
try:
data = upload_juming_cookie(file.filename or "juming_cookies.pkl", content)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="聚名 Cookie 已上传", data=data)

View File

@@ -1,8 +1,13 @@
from fastapi import APIRouter
from typing import Optional
from fastapi import APIRouter, Header
from app.schemas.common import ApiResponse
from app.services.cluster_runtime_service import get_cluster_snapshot
from app.services.runtime_control_service import runtime_action
from app.services.runtime_status_service import get_runtime_preflight, get_runtime_status
from app.services.sync_push_service import ingest_runtime_projection
from app.services.sync_record_service import get_sync_summary, list_sync_records
router = APIRouter(tags=["runtime"])
@@ -12,11 +17,37 @@ def runtime_status() -> ApiResponse:
return ApiResponse(data=get_runtime_status())
@router.get("/runtime/readiness", response_model=ApiResponse)
def runtime_readiness() -> ApiResponse:
return ApiResponse(data=(get_runtime_status().get("readiness") or {}))
@router.get("/runtime/preflight", response_model=ApiResponse)
def runtime_preflight() -> ApiResponse:
return ApiResponse(data=get_runtime_preflight())
@router.get("/runtime/cluster", response_model=ApiResponse)
def runtime_cluster() -> ApiResponse:
return ApiResponse(data=get_cluster_snapshot())
@router.get("/runtime/sync-summary", response_model=ApiResponse)
def runtime_sync_summary() -> ApiResponse:
return ApiResponse(data=get_sync_summary())
@router.get("/runtime/sync-records", response_model=ApiResponse)
def runtime_sync_records(limit: int = 20) -> ApiResponse:
return ApiResponse(data={"records": list_sync_records(limit=limit)})
@router.post("/runtime/sync-ingest", response_model=ApiResponse)
def runtime_sync_ingest(payload: dict, x_domaincheck_sync_token: Optional[str] = Header(default=None)) -> ApiResponse:
ok, message, data = ingest_runtime_projection(payload, shared_token=x_domaincheck_sync_token)
return ApiResponse(code=0 if ok else 1, message=message, data=data)
@router.post("/runtime/actions/{action}", response_model=ApiResponse)
def runtime_action_trigger(action: str) -> ApiResponse:
ok, message, data = runtime_action(action)

View File

@@ -0,0 +1,17 @@
from fastapi import APIRouter
from app.schemas.common import ApiResponse
from app.services.sensitive_words_service import get_sensitive_words_payload, save_sensitive_words_payload
router = APIRouter(tags=["sensitive_words"])
@router.get("/sensitive-words", response_model=ApiResponse)
def get_sensitive_words() -> ApiResponse:
return ApiResponse(data=get_sensitive_words_payload())
@router.put("/sensitive-words", response_model=ApiResponse)
def update_sensitive_words(payload: dict) -> ApiResponse:
data = save_sensitive_words_payload(payload)
return ApiResponse(message=f"已保存 {data['total']} 个敏感词", data=data)

View File

@@ -5,12 +5,15 @@ from fastapi.responses import FileResponse
from app.core.files import settings_backup_root
from app.schemas.common import ApiResponse
from app.services.juziseo_service import get_juziseo_status, login_juziseo
from app.services.settings_service import (
backup_current_settings,
export_settings_snapshot,
get_credentials_payload,
get_settings_payload,
import_settings_snapshot,
list_settings_backups,
update_credentials_payload,
update_settings_payload,
validate_settings_payload,
)
@@ -61,6 +64,30 @@ def get_settings_backups() -> ApiResponse:
return ApiResponse(data=list_settings_backups())
@router.get("/settings/credentials", response_model=ApiResponse)
def get_settings_credentials() -> ApiResponse:
return ApiResponse(data=get_credentials_payload())
@router.put("/settings/credentials", response_model=ApiResponse)
def update_settings_credentials(payload: dict) -> ApiResponse:
return ApiResponse(message="credentials updated", data=update_credentials_payload(payload))
@router.get("/settings/juziseo/status", response_model=ApiResponse)
def get_settings_juziseo_status() -> ApiResponse:
return ApiResponse(data=get_juziseo_status())
@router.post("/settings/juziseo/login", response_model=ApiResponse)
def settings_juziseo_login(payload: dict) -> ApiResponse:
try:
data = login_juziseo(str(payload.get("email") or ""), str(payload.get("password") or ""))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return ApiResponse(message="桔子SEO登录成功", data=data)
@router.get("/settings/backups/download/{filename}")
def download_settings_backup(filename: str):
path = settings_backup_root() / Path(filename).name