feat:对接中心平台,完善接口鉴权
This commit is contained in:
+15
-6
@@ -31,12 +31,12 @@ class AgentService:
|
||||
self._active: set[str] = set()
|
||||
self._conversation_of: dict[str, str] = {}
|
||||
|
||||
async def prepare_run(self, request: DesignRequest):
|
||||
async def prepare_run(self, request: DesignRequest, user_id: int):
|
||||
# 会话 ID 必须由后端签发,不再为缺失的 ID 兜底生成
|
||||
conversation_id = request.context.conversation_id
|
||||
if conversation_id is None:
|
||||
raise BizException(422, "请先创建会话并传入 conversation_id")
|
||||
if not await self.store.exists(conversation_id):
|
||||
if not await self.store.exists(conversation_id, user_id):
|
||||
raise BizException(404, "会话不存在或已过期")
|
||||
|
||||
# 新请求始终独立建任务,不用 conversation_id 充当检查点 ID
|
||||
@@ -72,9 +72,10 @@ class AgentService:
|
||||
"message_id": str(uuid4()),
|
||||
},
|
||||
conversation_id,
|
||||
user_id,
|
||||
)
|
||||
|
||||
async def prepare_resume(self, request: ResumeRequest):
|
||||
async def prepare_resume(self, request: ResumeRequest, user_id: int):
|
||||
# 在发送流响应头之前完成校验并占用任务
|
||||
thread_id = str(request.thread_id)
|
||||
# 检查和占用之间没有 await,同一事件循环内不会重复进入
|
||||
@@ -83,7 +84,10 @@ class AgentService:
|
||||
snapshot = await self.graph.aget_state(
|
||||
{"configurable": {"thread_id": thread_id}}
|
||||
)
|
||||
if not snapshot.values:
|
||||
# 不属于当前用户的任务与不存在同样处理,先于其余校验,不泄露任务状态
|
||||
if not snapshot.values or not await self.store.exists(
|
||||
snapshot.values["conversation_id"], user_id
|
||||
):
|
||||
raise BizException(404, "任务不存在或已过期")
|
||||
if not any(task.interrupts for task in snapshot.tasks):
|
||||
raise BizException(409, "任务当前未等待用户补充")
|
||||
@@ -119,6 +123,7 @@ class AgentService:
|
||||
update={"message_id": str(uuid4())},
|
||||
),
|
||||
conversation_id,
|
||||
user_id,
|
||||
)
|
||||
except BaseException:
|
||||
self.release(thread_id)
|
||||
@@ -196,13 +201,17 @@ class AgentService:
|
||||
failure_reason=state.get("failure_reason"),
|
||||
)
|
||||
|
||||
async def stream(self, thread_id, payload, session_id):
|
||||
async def stream(self, thread_id, payload, session_id, user_id=None):
|
||||
"""消费图事件,完整状态只用于整理最终结果,不直接暴露给客户端"""
|
||||
state = None
|
||||
with logger.contextualize(thread_id=thread_id):
|
||||
async with aclosing(
|
||||
astream_screen(
|
||||
self.graph, payload, thread_id=thread_id, session_id=session_id
|
||||
self.graph,
|
||||
payload,
|
||||
thread_id=thread_id,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
) as events:
|
||||
async for mode, data in events:
|
||||
|
||||
@@ -11,9 +11,11 @@ from app.schemas.conversation import Turn
|
||||
class ConversationStore(Protocol):
|
||||
"""内存和 MongoDB 实现同一组方法,业务代码不需要关心具体存储"""
|
||||
|
||||
async def create(self) -> str: ...
|
||||
async def create(self, user_id: int) -> str: ...
|
||||
|
||||
async def exists(self, conversation_id: str) -> bool: ...
|
||||
async def exists(self, conversation_id: str, user_id: int) -> bool:
|
||||
"""会话已签发、未过期且属于该用户;不属于时与不存在同样返回 False"""
|
||||
...
|
||||
|
||||
async def load_turns(
|
||||
self, conversation_id: str, limit: int | None
|
||||
@@ -32,6 +34,7 @@ class InMemoryConversationStore:
|
||||
self._empty_ttl = timedelta(seconds=ttl)
|
||||
# 会话 ID -> 创建时间;轮次按 thread_id 去重并保持首次写入顺序
|
||||
self._created: dict[str, datetime] = {}
|
||||
self._owner: dict[str, int] = {}
|
||||
self._turns: dict[str, dict[str, Turn]] = {}
|
||||
|
||||
def _purge_expired(self) -> None:
|
||||
@@ -43,17 +46,22 @@ class InMemoryConversationStore:
|
||||
if created < deadline and not self._turns.get(key)
|
||||
]:
|
||||
del self._created[conversation_id]
|
||||
self._owner.pop(conversation_id, None)
|
||||
self._turns.pop(conversation_id, None)
|
||||
|
||||
async def create(self) -> str:
|
||||
async def create(self, user_id: int) -> str:
|
||||
self._purge_expired()
|
||||
conversation_id = str(uuid4())
|
||||
self._created[conversation_id] = _now()
|
||||
self._owner[conversation_id] = user_id
|
||||
return conversation_id
|
||||
|
||||
async def exists(self, conversation_id: str) -> bool:
|
||||
async def exists(self, conversation_id: str, user_id: int) -> bool:
|
||||
self._purge_expired()
|
||||
return conversation_id in self._created
|
||||
return (
|
||||
conversation_id in self._created
|
||||
and self._owner.get(conversation_id) == user_id
|
||||
)
|
||||
|
||||
async def load_turns(self, conversation_id: str, limit: int | None) -> list[Turn]:
|
||||
turns = list(self._turns.get(conversation_id, {}).values())
|
||||
|
||||
@@ -42,6 +42,8 @@ class MongoConversationStore:
|
||||
)
|
||||
# 空会话清理按创建时间扫描
|
||||
await self._conversations.create_index("created_at")
|
||||
# 按用户查询会话
|
||||
await self._conversations.create_index("user_id")
|
||||
|
||||
async def _purge_expired(self) -> None:
|
||||
# 只清理没有任何轮次且超过保留时长的空会话,有对话的会话不受影响
|
||||
@@ -63,17 +65,20 @@ class MongoConversationStore:
|
||||
if empty:
|
||||
await self._conversations.delete_many({"_id": {"$in": empty}})
|
||||
|
||||
async def create(self) -> str:
|
||||
async def create(self, user_id: int) -> str:
|
||||
await self._purge_expired()
|
||||
conversation_id = str(uuid4())
|
||||
# user_id 为后续长期记忆预留,当前恒为空
|
||||
# user_id 来自用户中心,会话只对创建者可见
|
||||
await self._conversations.insert_one(
|
||||
{"_id": conversation_id, "created_at": _now(), "user_id": None}
|
||||
{"_id": conversation_id, "created_at": _now(), "user_id": user_id}
|
||||
)
|
||||
return conversation_id
|
||||
|
||||
async def exists(self, conversation_id: str) -> bool:
|
||||
document = await self._conversations.find_one({"_id": conversation_id})
|
||||
async def exists(self, conversation_id: str, user_id: int) -> bool:
|
||||
# 不属于该用户的会话与不存在同样处理,不泄露会话是否存在
|
||||
document = await self._conversations.find_one(
|
||||
{"_id": conversation_id, "user_id": user_id}
|
||||
)
|
||||
if document is None:
|
||||
return False
|
||||
if document["created_at"] >= _now() - self._empty_ttl:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from loguru import logger
|
||||
from pymongo.asynchronous.database import AsyncDatabase
|
||||
|
||||
from app.resources.component import load_catalog_file, set_catalog_cache
|
||||
from app.resources.component import set_catalog_cache
|
||||
from app.schemas.component_resource import ComponentCatalog
|
||||
|
||||
# 元数据记录的 _id,组件记录的 _id 为组件 key
|
||||
@@ -49,11 +49,13 @@ class MongoMaterialRepository:
|
||||
|
||||
|
||||
async def warm_material_cache(db: AsyncDatabase | None) -> ComponentCatalog | None:
|
||||
"""启动时把物料目录载入进程缓存;Mongo 未启用或尚未导入时回退读本地 JSON"""
|
||||
"""启动时把物料目录从 MongoDB 载入进程缓存;未启用或尚未导入时目录为空,不回退读文件"""
|
||||
catalog = await MongoMaterialRepository(db).load_all() if db is not None else None
|
||||
if catalog is None:
|
||||
catalog = load_catalog_file()
|
||||
logger.warning("物料未从 MongoDB 加载,回退使用本地目录文件")
|
||||
logger.error(
|
||||
"物料目录未载入:{},新增、更换组件等需要物料的修改将无法执行",
|
||||
"未配置 MONGODB_URI" if db is None else "materials 集合尚未导入",
|
||||
)
|
||||
else:
|
||||
logger.info("已从 MongoDB 加载物料 {} 个", len(catalog.components))
|
||||
set_catalog_cache(catalog)
|
||||
|
||||
Reference in New Issue
Block a user