ForcePilot/backend/package/yuxi/services/filesystem_service.py
Wenjie Zhang c772e5ca3a refactor: Agent 运行时架构重构 - 移除 RuntimeConfigMiddleware,统一上下文准备与工具解析
- 删除 runtime_config_middleware.py,将功能合并到:
  - prepare_agent_runtime_context(): 统一上下文准备入口
  - resolve_configured_runtime_tools(): 运行时工具解析
- context.py: 规范化函数重命名 (_names → _keys),重构 config 加载流程
- chatbot/deep_agent graph: 集成新上下文准备流程,移除 RuntimeConfigMiddleware 引用
- skills_middleware: 抽取 normalize_string_list 为共享工具函数
- subagent_service: get_subagents_from_names → get_subagents_from_slugs,支持 slug 查询
- 对应更新 repositories/services/routers 适配新接口
2026-05-26 17:40:17 +08:00

162 lines
5.3 KiB
Python

from __future__ import annotations
import asyncio
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.agents.backends import create_agent_composite_backend
from yuxi.agents.backends.sandbox.backend import _looks_like_binary
from yuxi.agents.context import BaseContext, normalize_agent_context_config, prepare_agent_runtime_context
from yuxi.repositories.agent_config_repository import AgentConfigRepository
from yuxi.repositories.conversation_repository import ConversationRepository
from yuxi.services.conversation_service import require_user_conversation
from yuxi.storage.postgres.models_business import User
async def _resolve_filesystem_context(
*,
db: AsyncSession,
user: User,
agent_id: str,
agent_config_id: int | None,
) -> BaseContext:
context = BaseContext(thread_id="", uid=str(user.uid))
repo = AgentConfigRepository(db)
config_item = None
if agent_config_id is not None:
config_item = await repo.get_by_id(config_id=int(agent_config_id))
if config_item is not None and (config_item.uid != str(user.uid) or config_item.agent_id != agent_id):
config_item = None
if config_item is None:
config_item = await repo.get_or_create_default(
uid=str(user.uid),
agent_id=agent_id,
created_by=str(user.uid),
)
normalized_config = await normalize_agent_context_config(
(config_item.config_json or {}).get("context", {}),
db=db,
user=user,
)
context.update_from_dict(normalized_config)
return context
async def _resolve_filesystem_state(
*,
thread_id: str,
user: User,
db: AsyncSession,
agent_id: str | None,
agent_config_id: int | None,
):
conv_repo = ConversationRepository(db)
conversation = await require_user_conversation(conv_repo, thread_id, str(user.uid))
runtime_context = await _resolve_filesystem_context(
db=db,
user=user,
agent_id=agent_id or conversation.agent_id,
agent_config_id=agent_config_id,
)
runtime_context.thread_id = thread_id
runtime_context.uid = str(user.uid)
await prepare_agent_runtime_context(runtime_context)
return conversation, runtime_context
async def list_filesystem_entries_view(
*,
thread_id: str,
path: str,
agent_id: str | None,
agent_config_id: int | None,
current_user: User,
db: AsyncSession,
) -> dict:
if not thread_id:
raise HTTPException(status_code=422, detail="thread_id 不能为空")
normalized_path = (path or "/").strip() or "/"
_conversation, runtime_context = await _resolve_filesystem_state(
thread_id=thread_id,
user=current_user,
db=db,
agent_id=agent_id,
agent_config_id=agent_config_id,
)
runtime_stub = type("RuntimeStub", (), {"context": runtime_context})()
composite_backend = create_agent_composite_backend(runtime_stub)
try:
entries = await asyncio.to_thread(composite_backend.ls_info, normalized_path)
except PermissionError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e)) from e
return {"entries": entries or []}
async def read_file_content_view(
*,
thread_id: str,
path: str,
agent_id: str | None,
agent_config_id: int | None,
current_user: User,
db: AsyncSession,
) -> dict:
if not thread_id:
raise HTTPException(status_code=422, detail="thread_id 不能为空")
if not path:
raise HTTPException(status_code=422, detail="path 不能为空")
normalized_path = path.strip()
_conversation, runtime_context = await _resolve_filesystem_state(
thread_id=thread_id,
user=current_user,
db=db,
agent_id=agent_id,
agent_config_id=agent_config_id,
)
runtime_stub = type("RuntimeStub", (), {"context": runtime_context})()
composite_backend = create_agent_composite_backend(runtime_stub)
try:
responses = await asyncio.to_thread(composite_backend.download_files, [normalized_path])
except PermissionError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e)) from e
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
response = responses[0] if responses else None
if response is None:
raise HTTPException(status_code=404, detail="文件不存在")
if response.error == "file_not_found":
raise HTTPException(status_code=404, detail="文件不存在")
if response.error == "is_directory":
raise HTTPException(status_code=400, detail="当前路径是目录")
if response.error == "read_failed":
raise HTTPException(status_code=400, detail="文件读取失败")
if response.error:
raise HTTPException(status_code=400, detail=response.error)
raw_content = response.content or b""
if _looks_like_binary(raw_content):
raise HTTPException(status_code=400, detail="当前文件是二进制文件,不能按文本读取")
try:
content = raw_content.decode("utf-8")
except UnicodeDecodeError:
content = raw_content.decode("utf-8", errors="replace")
return {"content": content}