refactor(agents): 智能体配置改为用户隔离并添加权限控制
- AgentConfigRepository 从 department_id 改为 uid - 添加 Context 字段权限过滤功能 - 更新 sandbox paths 和 base agent 配置
This commit is contained in:
parent
78116ec59d
commit
96dfaf619a
@ -63,12 +63,21 @@ def sandbox_workspace_agents_prompt_file(thread_id: str, uid: str) -> Path:
|
||||
return sandbox_workspace_dir(thread_id, uid) / WORKSPACE_AGENTS_DIR_NAME / WORKSPACE_AGENTS_PROMPT_FILE_NAME
|
||||
|
||||
|
||||
def _chmod_writable(path: Path, *, dir: bool = False) -> None:
|
||||
mode = 0o777 if dir else 0o666
|
||||
try:
|
||||
path.chmod(mode)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def ensure_workspace_default_files(workspace_dir: Path) -> None:
|
||||
agents_dir = workspace_dir / WORKSPACE_AGENTS_DIR_NAME
|
||||
agents_file = agents_dir / WORKSPACE_AGENTS_PROMPT_FILE_NAME
|
||||
|
||||
try:
|
||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
_chmod_writable(agents_dir, dir=True)
|
||||
except FileExistsError:
|
||||
logger.warning("工作区默认 Agents 目录创建失败:路径已被文件占用")
|
||||
return
|
||||
@ -79,6 +88,7 @@ def ensure_workspace_default_files(workspace_dir: Path) -> None:
|
||||
try:
|
||||
with agents_file.open("xb"):
|
||||
pass
|
||||
_chmod_writable(agents_file)
|
||||
except FileExistsError:
|
||||
if agents_file.is_dir():
|
||||
logger.warning("工作区默认 AGENTS.md 创建失败:路径已被目录占用")
|
||||
|
||||
@ -41,12 +41,12 @@ class BaseAgent:
|
||||
"""Get the agent's class name."""
|
||||
return self.__class__.__name__
|
||||
|
||||
async def get_info(self, include_configurable_items: bool = True):
|
||||
async def get_info(self, include_configurable_items: bool = True, user_role: str | None = None):
|
||||
# metadata 固定在代码中,由各 Agent 的类属性提供
|
||||
metadata = self.load_metadata()
|
||||
configurable_items = {}
|
||||
if include_configurable_items:
|
||||
configurable_items = self.context_schema.get_configurable_items()
|
||||
configurable_items = self.context_schema.get_configurable_items(user_role=user_role)
|
||||
|
||||
# Merge metadata with class attributes, metadata takes precedence
|
||||
return {
|
||||
|
||||
@ -11,21 +11,19 @@ PROMPT = f"""
|
||||
专门用来回答用户的问题。请根据用户提供的信息,尽可能详细地回答问题。
|
||||
如果你不确定答案,可以说你不知道,但请尽量提供相关的信息或建议。请保持礼貌和专业。
|
||||
|
||||
<| 内部执行约束 |>
|
||||
以下内容仅用于指导你的内部执行过程,不属于面向用户的基本设定。除非用户明确询问系统如何工作,否则不要主动向用户说明工作区、文件系统、知识库路径、工具调用方式等内部实现细节。
|
||||
<| 内部执行约束:重要 |>
|
||||
以下内容仅用于指导你的内部执行过程,不属于面向用户的基本设定。除非用户明确询问系统如何工作,
|
||||
否则不要主动向用户说明工作区、文件系统、知识库路径、工具调用方式等内部实现细节。
|
||||
|
||||
<| 文件系统约束 |>
|
||||
系统主要工作路径为 {VIRTUAL_PATH_PREFIX},但必须遵守规范:
|
||||
- {VIRTUAL_PATH_WORKSPACE}:用于存放工作文件(用户目录,不要轻易写入)
|
||||
- {VIRTUAL_PATH_OUTPUTS}:用于写入的文件夹
|
||||
- {VIRTUAL_PATH_OUTPUTS}/tmp/:用于存放中间结果或备份内容
|
||||
- {VIRTUAL_PATH_UPLOADS}:用于存放用户上传的文件
|
||||
- {VIRTUAL_PATH_UPLOADS}:用于存放用户上传的附件(只读,除非用户要求,否则不得写入)
|
||||
- {VIRTUAL_PATH_WORKSPACE}:用于存放用户文件(用户私人目录,除非用户要求,否则不得写入)
|
||||
- 其他路径:非必要不写入其他路径
|
||||
|
||||
非必要不写入其他路径
|
||||
|
||||
<| 知识库访问 |>
|
||||
当 query_kb 中没有找到相关内容,或者需要进一步基于检索结果获取更详细上下文时,
|
||||
使用 open_kb_document 按 resource_id 和 file_id 打开知识库文档。
|
||||
"""
|
||||
|
||||
# 效果不好,暂时不启用
|
||||
|
||||
@ -7,6 +7,41 @@ from typing import get_origin
|
||||
from yuxi import config as sys_config
|
||||
|
||||
|
||||
def _role_can_access(auth: str | None, role: str | None) -> bool:
|
||||
if not auth:
|
||||
return True
|
||||
if auth == "admin":
|
||||
return role in {"admin", "superadmin"}
|
||||
if auth == "superadmin":
|
||||
return role == "superadmin"
|
||||
return False
|
||||
|
||||
|
||||
def filter_config_by_role(
|
||||
config_json: dict,
|
||||
role: str | None,
|
||||
context_schema: type["BaseContext"] | None = None,
|
||||
) -> dict:
|
||||
"""按 Context 字段 metadata.auth 过滤 config_json.context。"""
|
||||
if not isinstance(config_json, dict):
|
||||
return {}
|
||||
|
||||
schema = context_schema or BaseContext
|
||||
restricted_fields = {
|
||||
f.name
|
||||
for f in fields(schema)
|
||||
if f.metadata.get("auth") and not _role_can_access(str(f.metadata.get("auth")), role)
|
||||
}
|
||||
if not restricted_fields:
|
||||
return dict(config_json)
|
||||
|
||||
filtered = dict(config_json)
|
||||
context = filtered.get("context")
|
||||
if isinstance(context, dict):
|
||||
filtered["context"] = {key: value for key, value in context.items() if key not in restricted_fields}
|
||||
return filtered
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BaseContext:
|
||||
"""
|
||||
@ -123,11 +158,13 @@ class BaseContext:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_configurable_items(cls):
|
||||
def get_configurable_items(cls, user_role: str | None = None):
|
||||
"""实现一个可配置的参数列表,在 UI 上配置时使用"""
|
||||
configurable_items = {}
|
||||
for f in fields(cls):
|
||||
if f.init and not f.metadata.get("hide", False):
|
||||
if user_role is not None and not _role_can_access(f.metadata.get("auth"), user_role):
|
||||
continue
|
||||
if f.metadata.get("configurable", True):
|
||||
type_name = cls._get_type_name(f.type)
|
||||
|
||||
|
||||
@ -28,10 +28,10 @@ class AgentConfigRepository:
|
||||
def __init__(self, db_session: AsyncSession):
|
||||
self.db = db_session
|
||||
|
||||
async def list_by_department_agent(self, *, department_id: int, agent_id: str) -> list[AgentConfig]:
|
||||
async def list_by_user_agent(self, *, uid: str, agent_id: str) -> list[AgentConfig]:
|
||||
result = await self.db.execute(
|
||||
select(AgentConfig)
|
||||
.where(AgentConfig.department_id == department_id, AgentConfig.agent_id == agent_id)
|
||||
.where(AgentConfig.uid == uid, AgentConfig.agent_id == agent_id)
|
||||
.order_by(AgentConfig.is_default.desc(), AgentConfig.id.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
@ -45,16 +45,13 @@ class AgentConfigRepository:
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def set_default(self, *, config: AgentConfig, updated_by: str | None = None) -> AgentConfig:
|
||||
if config.is_default:
|
||||
return config
|
||||
|
||||
now = utc_now_naive()
|
||||
|
||||
# 先清空该部门+智能体的所有默认配置
|
||||
# 先清空该用户+智能体的所有默认配置
|
||||
await self.db.execute(
|
||||
update(AgentConfig)
|
||||
.where(
|
||||
AgentConfig.department_id == config.department_id,
|
||||
AgentConfig.uid == config.uid,
|
||||
AgentConfig.agent_id == config.agent_id,
|
||||
)
|
||||
.values(is_default=False, updated_at=now, updated_by=updated_by)
|
||||
@ -69,10 +66,10 @@ class AgentConfigRepository:
|
||||
await self.db.refresh(config)
|
||||
return config
|
||||
|
||||
async def get_default(self, *, department_id: int, agent_id: str) -> AgentConfig | None:
|
||||
async def get_default(self, *, uid: str, agent_id: str) -> AgentConfig | None:
|
||||
result = await self.db.execute(
|
||||
select(AgentConfig).where(
|
||||
AgentConfig.department_id == department_id,
|
||||
AgentConfig.uid == uid,
|
||||
AgentConfig.agent_id == agent_id,
|
||||
AgentConfig.is_default.is_(True),
|
||||
)
|
||||
@ -82,20 +79,20 @@ class AgentConfigRepository:
|
||||
async def get_or_create_default(
|
||||
self,
|
||||
*,
|
||||
department_id: int,
|
||||
uid: str,
|
||||
agent_id: str,
|
||||
created_by: str | None = None,
|
||||
) -> AgentConfig:
|
||||
existing = await self.get_default(department_id=department_id, agent_id=agent_id)
|
||||
existing = await self.get_default(uid=uid, agent_id=agent_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
items = await self.list_by_department_agent(department_id=department_id, agent_id=agent_id)
|
||||
items = await self.list_by_user_agent(uid=uid, agent_id=agent_id)
|
||||
if items:
|
||||
return items[0]
|
||||
|
||||
config = AgentConfig(
|
||||
department_id=department_id,
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
name=DEFAULT_CONFIG_NAME,
|
||||
description=None,
|
||||
@ -114,9 +111,9 @@ class AgentConfigRepository:
|
||||
await self.db.refresh(config)
|
||||
return config
|
||||
|
||||
async def _name_exists(self, *, department_id: int, agent_id: str, name: str, exclude_id: int | None) -> bool:
|
||||
async def _name_exists(self, *, uid: str, agent_id: str, name: str, exclude_id: int | None) -> bool:
|
||||
stmt = select(AgentConfig.id).where(
|
||||
AgentConfig.department_id == department_id,
|
||||
AgentConfig.uid == uid,
|
||||
AgentConfig.agent_id == agent_id,
|
||||
AgentConfig.name == name,
|
||||
)
|
||||
@ -128,36 +125,30 @@ class AgentConfigRepository:
|
||||
async def ensure_unique_name(
|
||||
self,
|
||||
*,
|
||||
department_id: int,
|
||||
uid: str,
|
||||
agent_id: str,
|
||||
desired_name: str,
|
||||
exclude_id: int | None = None,
|
||||
) -> str:
|
||||
candidate = desired_name.strip() or "未命名配置"
|
||||
if not await self._name_exists(
|
||||
department_id=department_id, agent_id=agent_id, name=candidate, exclude_id=exclude_id
|
||||
):
|
||||
if not await self._name_exists(uid=uid, agent_id=agent_id, name=candidate, exclude_id=exclude_id):
|
||||
return candidate
|
||||
|
||||
base = f"{candidate}-副本"
|
||||
if not await self._name_exists(
|
||||
department_id=department_id, agent_id=agent_id, name=base, exclude_id=exclude_id
|
||||
):
|
||||
if not await self._name_exists(uid=uid, agent_id=agent_id, name=base, exclude_id=exclude_id):
|
||||
return base
|
||||
|
||||
idx = 2
|
||||
while True:
|
||||
candidate2 = f"{base}{idx}"
|
||||
if not await self._name_exists(
|
||||
department_id=department_id, agent_id=agent_id, name=candidate2, exclude_id=exclude_id
|
||||
):
|
||||
if not await self._name_exists(uid=uid, agent_id=agent_id, name=candidate2, exclude_id=exclude_id):
|
||||
return candidate2
|
||||
idx += 1
|
||||
|
||||
async def create(
|
||||
self,
|
||||
*,
|
||||
department_id: int,
|
||||
uid: str,
|
||||
agent_id: str,
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
@ -169,13 +160,13 @@ class AgentConfigRepository:
|
||||
created_by: str | None = None,
|
||||
) -> AgentConfig:
|
||||
unique_name = await self.ensure_unique_name(
|
||||
department_id=department_id,
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
desired_name=name,
|
||||
exclude_id=None,
|
||||
)
|
||||
config = AgentConfig(
|
||||
department_id=department_id,
|
||||
uid=uid,
|
||||
agent_id=agent_id,
|
||||
name=unique_name,
|
||||
description=description,
|
||||
@ -183,7 +174,7 @@ class AgentConfigRepository:
|
||||
pics=pics or [],
|
||||
examples=examples or [],
|
||||
config_json=config_json or {},
|
||||
is_default=bool(is_default),
|
||||
is_default=False,
|
||||
created_by=created_by,
|
||||
updated_by=created_by,
|
||||
created_at=utc_now_naive(),
|
||||
@ -192,6 +183,8 @@ class AgentConfigRepository:
|
||||
self.db.add(config)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(config)
|
||||
if is_default:
|
||||
return await self.set_default(config=config, updated_by=created_by)
|
||||
return config
|
||||
|
||||
async def update(
|
||||
@ -208,7 +201,7 @@ class AgentConfigRepository:
|
||||
) -> AgentConfig:
|
||||
if name is not None:
|
||||
config.name = await self.ensure_unique_name(
|
||||
department_id=config.department_id,
|
||||
uid=config.uid,
|
||||
agent_id=config.agent_id,
|
||||
desired_name=name,
|
||||
exclude_id=config.id,
|
||||
@ -231,16 +224,16 @@ class AgentConfigRepository:
|
||||
return config
|
||||
|
||||
async def delete(self, *, config: AgentConfig, updated_by: str | None = None) -> None:
|
||||
department_id = config.department_id
|
||||
uid = config.uid
|
||||
agent_id = config.agent_id
|
||||
was_default = bool(config.is_default)
|
||||
|
||||
await self.db.delete(config)
|
||||
await self.db.commit()
|
||||
|
||||
remaining = await self.list_by_department_agent(department_id=department_id, agent_id=agent_id)
|
||||
remaining = await self.list_by_user_agent(uid=uid, agent_id=agent_id)
|
||||
if not remaining:
|
||||
await self.get_or_create_default(department_id=department_id, agent_id=agent_id, created_by=updated_by)
|
||||
await self.get_or_create_default(uid=uid, agent_id=agent_id, created_by=updated_by)
|
||||
return
|
||||
|
||||
if was_default:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user