first
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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", {}))
|
||||
|
||||
@@ -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)
|
||||
|
||||
93
domain-api/app/api/routes/juming.py
Normal file
93
domain-api/app/api/routes/juming.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
17
domain-api/app/api/routes/sensitive_words.py
Normal file
17
domain-api/app/api/routes/sensitive_words.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user