144 lines
6.3 KiB
Python
144 lines
6.3 KiB
Python
"""Project ownership, knowledge validation and server-controlled run policy."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from copy import deepcopy
|
|
|
|
from fastapi import HTTPException, Request
|
|
|
|
from app.gateway.auth.disabled_mode import is_auth_disabled
|
|
from app.gateway.deps import get_agent_store, get_checkpointer, get_optional_user_from_request
|
|
from deerflow.agents.analysis_project import SOLUTION_TEMPLATE_ID
|
|
from deerflow.config.agents_config import load_agent_config, validate_agent_id
|
|
from deerflow.runtime.user_context import get_effective_user_id
|
|
|
|
|
|
async def validate_project_agent(request: Request, agent_id: str | None):
|
|
if not agent_id:
|
|
return None
|
|
try:
|
|
validate_agent_id(agent_id)
|
|
except ValueError as exc:
|
|
raise HTTPException(422, "智能体标识无效") from exc
|
|
user_id, is_admin = await actor(request)
|
|
store = get_agent_store(request)
|
|
row = await store.get_visible(agent_id, user_id)
|
|
if row is None and is_admin:
|
|
row = await store.get_any(agent_id)
|
|
if row is None:
|
|
raise HTTPException(422, "所选智能体不存在或无访问权限,请重新选择")
|
|
return row
|
|
|
|
|
|
def project_store(request: Request):
|
|
store = getattr(request.app.state, "analysis_project_store", None)
|
|
if store is None:
|
|
raise HTTPException(503, "项目存储尚未就绪")
|
|
return store
|
|
|
|
|
|
async def actor(request: Request) -> tuple[str, bool]:
|
|
user = await get_optional_user_from_request(request)
|
|
if user:
|
|
return str(user.id), getattr(user, "system_role", None) == "admin"
|
|
if is_auth_disabled():
|
|
return get_effective_user_id(), True
|
|
raise HTTPException(401, "请先登录")
|
|
|
|
|
|
async def owned_project(request: Request, project_id: str) -> dict:
|
|
user_id, _ = await actor(request)
|
|
project = await project_store(request).get(project_id, user_id)
|
|
if project is None:
|
|
raise HTTPException(404, "项目不存在或无权访问")
|
|
return project
|
|
|
|
|
|
async def validate_knowledge(request: Request, project: dict) -> None:
|
|
user_id, is_admin = await actor(request)
|
|
for store_name, field in (("llmwiki_store", "knowledge_base_ids"), ("assistant_knowledge_store", "assistant_knowledge_base_ids")):
|
|
ids = project.get(field) or []
|
|
if not ids:
|
|
continue
|
|
store = getattr(request.app.state, store_name, None)
|
|
if store is None:
|
|
raise HTTPException(503, "知识库服务暂不可用")
|
|
for base_id in ids:
|
|
row = await store.get_authorized(base_id, user_id, write=False, is_admin=is_admin) if field == "knowledge_base_ids" else await store.get_base(base_id)
|
|
if row is None:
|
|
raise HTTPException(422, "所选知识库不存在或已无访问权限,请重新配置项目背景")
|
|
|
|
|
|
async def prepare_project_run(body, thread_id: str, request: Request):
|
|
"""Resolve policy from the immutable binding, never from caller metadata.
|
|
|
|
Applies to all Gateway run endpoints, including direct SDK calls. Strip
|
|
forged policy even on ordinary threads; all other chat behavior is preserved.
|
|
"""
|
|
body = body.model_copy(deep=True) if hasattr(body, "model_copy") else deepcopy(body)
|
|
body.context = dict(body.context or {})
|
|
body.context.pop("analysis_project", None)
|
|
body.config = dict(body.config or {})
|
|
for key in ("context", "configurable"):
|
|
if isinstance(body.config.get(key), dict):
|
|
body.config[key].pop("analysis_project", None)
|
|
store = getattr(request.app.state, "analysis_project_store", None)
|
|
if store is None:
|
|
return body, None
|
|
project = await store.project_for_thread(thread_id)
|
|
if project is None:
|
|
return body, None
|
|
user_id, _ = await actor(request)
|
|
if project["user_id"] != user_id:
|
|
raise HTTPException(404, "项目不存在或无权访问")
|
|
thread_config = await store.config_for(f"thread:{thread_id}")
|
|
agent_id = thread_config.get("agent_id")
|
|
await validate_project_agent(request, agent_id)
|
|
agent_config = await asyncio.to_thread(load_agent_config, agent_id) if agent_id else None
|
|
knowledge_ids = list(getattr(agent_config, "llmwiki_knowledge_base_ids", None) or [])
|
|
await validate_knowledge(request, {"knowledge_base_ids": knowledge_ids})
|
|
for message in (body.input or {}).get("messages", []):
|
|
if isinstance(message, dict) and isinstance(message.get("additional_kwargs"), dict):
|
|
# Source snapshots must come from this run's server retrieval.
|
|
for key in ("llmwiki_reference_sources", "llmwiki_reference_query", "prompt_prefix"):
|
|
message["additional_kwargs"].pop(key, None)
|
|
artifact_path = None
|
|
if project["mode"] == "solution":
|
|
checkpoint = await get_checkpointer(request).aget_tuple({"configurable": {"thread_id": thread_id}})
|
|
values = checkpoint.checkpoint.get("channel_values", {}) if checkpoint else {}
|
|
if "/mnt/user-data/outputs/report.md" in (values.get("artifacts") or []):
|
|
artifact_path = "/mnt/user-data/outputs/report.md"
|
|
policy = {
|
|
"analysis_project": {
|
|
"id": project["id"],
|
|
"name": project["name"],
|
|
"mode": project["mode"],
|
|
"background": project["background"],
|
|
"template_id": SOLUTION_TEMPLATE_ID,
|
|
"report_config": project.get("report_config") or {},
|
|
"strategy_config": project.get("strategy_config") or {},
|
|
},
|
|
"llmwiki_knowledge_base_ids": knowledge_ids,
|
|
"assistant_knowledge_base_ids": [],
|
|
"writing_mode": project["mode"] == "solution",
|
|
# The artifact path is resolved by the existing agent/middleware, not a
|
|
# client-supplied path that could skip the first-report pipeline.
|
|
"writing_artifact_path": artifact_path,
|
|
"agent_id": agent_id,
|
|
"agent_name": None,
|
|
"is_plan_mode": False,
|
|
"subagent_enabled": False,
|
|
"is_bootstrap": False,
|
|
"is_writing_setup": False,
|
|
"is_report_structure_setup": False,
|
|
"knowledge_rag_enabled": False,
|
|
}
|
|
body.assistant_id = agent_id or "lead_agent"
|
|
body.context.update(policy)
|
|
body.metadata = {**(body.metadata or {}), "agent_id": agent_id, "agent_name": None, "analysis_project_id": project["id"]}
|
|
for key in ("context", "configurable"):
|
|
if isinstance(body.config.get(key), dict):
|
|
body.config[key].update(policy)
|
|
return body, project
|