diff --git a/backend/package/yuxi/agents/backends/sandbox/paths.py b/backend/package/yuxi/agents/backends/sandbox/paths.py index 60cc6a39..4786fdf7 100644 --- a/backend/package/yuxi/agents/backends/sandbox/paths.py +++ b/backend/package/yuxi/agents/backends/sandbox/paths.py @@ -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 创建失败:路径已被目录占用") diff --git a/backend/package/yuxi/agents/base.py b/backend/package/yuxi/agents/base.py index 166e8833..30a01b36 100644 --- a/backend/package/yuxi/agents/base.py +++ b/backend/package/yuxi/agents/base.py @@ -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 { diff --git a/backend/package/yuxi/agents/buildin/chatbot/prompt.py b/backend/package/yuxi/agents/buildin/chatbot/prompt.py index e57e03a9..213970f6 100644 --- a/backend/package/yuxi/agents/buildin/chatbot/prompt.py +++ b/backend/package/yuxi/agents/buildin/chatbot/prompt.py @@ -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 打开知识库文档。 """ # 效果不好,暂时不启用 diff --git a/backend/package/yuxi/agents/context.py b/backend/package/yuxi/agents/context.py index 86a5a876..3b55ba47 100644 --- a/backend/package/yuxi/agents/context.py +++ b/backend/package/yuxi/agents/context.py @@ -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) diff --git a/backend/package/yuxi/repositories/agent_config_repository.py b/backend/package/yuxi/repositories/agent_config_repository.py index 6cc4135a..fb1a8381 100644 --- a/backend/package/yuxi/repositories/agent_config_repository.py +++ b/backend/package/yuxi/repositories/agent_config_repository.py @@ -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: