diff --git a/AGENTS.md b/AGENTS.md index 9e4667e1..0eb39ab7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,3 +121,4 @@ make format # 格式化代码 1. 参考 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范编写提交信息。 2. 使用中文提交信息,标题简洁明了,描述具体改动内容和原因。 +3. 创建 PR 需要参考 [CONTRIBUTING.md](CONTRIBUTING.md) 中的 PR 模板[PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md),并在提交前完成其中的检查项。 diff --git a/CLAUDE.md b/CLAUDE.md index 9e4667e1..0eb39ab7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,3 +121,4 @@ make format # 格式化代码 1. 参考 [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 规范编写提交信息。 2. 使用中文提交信息,标题简洁明了,描述具体改动内容和原因。 +3. 创建 PR 需要参考 [CONTRIBUTING.md](CONTRIBUTING.md) 中的 PR 模板[PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md),并在提交前完成其中的检查项。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74ff5d9a..d67cae44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,7 @@ docker logs api-dev --tail 100 2. 在对应目录完成开发与测试 3. 提交清晰的 Commit Message 4. 发起 Pull Request,并说明修改内容、原因和验证方式 +5. PR 模板 [PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md) 中的检查项需要在提交前完成 示例: diff --git a/backend/package/yuxi/agents/buildin/chatbot/prompt.py b/backend/package/yuxi/agents/buildin/chatbot/prompt.py index df1d3c9b..ae885b17 100644 --- a/backend/package/yuxi/agents/buildin/chatbot/prompt.py +++ b/backend/package/yuxi/agents/buildin/chatbot/prompt.py @@ -12,6 +12,9 @@ PROMPT = f""" 专门用来回答用户的问题。请根据用户提供的信息,尽可能详细地回答问题。 如果你不确定答案,可以说你不知道,但请尽量提供相关的信息或建议。请保持礼貌和专业。 +<| 内部执行约束 |> +以下内容仅用于指导你的内部执行过程,不属于面向用户的基本设定。除非用户明确询问系统如何工作,否则不要主动向用户说明工作区、文件系统、知识库路径、工具调用方式等内部实现细节。 + <| 文件系统约束 |> 系统主要工作路径为 {VIRTUAL_PATH_PREFIX},但必须遵守规范: - {VIRTUAL_PATH_WORKSPACE}:用于存放工作文件(用户目录,不要轻易写入) diff --git a/backend/server/routers/skill_router.py b/backend/server/routers/skill_router.py index 14933a5e..aab5aad4 100644 --- a/backend/server/routers/skill_router.py +++ b/backend/server/routers/skill_router.py @@ -9,7 +9,7 @@ from fastapi.responses import FileResponse from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession -from server.utils.auth_middleware import get_admin_user, get_db, get_superadmin_user +from server.utils.auth_middleware import get_admin_user, get_db from yuxi.services.remote_skill_install_service import install_remote_skill, list_remote_skills from yuxi.services.skill_service import ( BuiltinSkillUpdateConflictError, @@ -92,10 +92,10 @@ async def list_skills_route( @skills.get("/dependency-options") async def get_skill_dependency_options_route( - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """获取 skill 依赖项可选列表(仅超级管理员)。""" + """获取 skill 依赖项可选列表(管理员)。""" try: return {"success": True, "data": await get_skill_dependency_options(db)} except Exception as e: @@ -105,7 +105,7 @@ async def get_skill_dependency_options_route( @skills.get("/builtin") async def list_builtin_skills_route( - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): try: @@ -141,7 +141,7 @@ async def list_builtin_skills_route( @skills.post("/builtin/{slug}/install") async def install_builtin_skill_route( slug: str, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): try: @@ -160,7 +160,7 @@ async def install_builtin_skill_route( async def update_builtin_skill_route( slug: str, payload: BuiltinSkillUpdateRequest, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): try: @@ -188,10 +188,10 @@ async def update_builtin_skill_route( @skills.post("/import") async def import_skill_route( file: UploadFile = File(...), - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """导入技能包(支持 ZIP 或单个 SKILL.md,仅超级管理员)。""" + """导入技能包(支持 ZIP 或单个 SKILL.md,管理员)。""" try: file_bytes = await file.read() item = await import_skill_zip( @@ -213,7 +213,7 @@ async def import_skill_route( @skills.post("/remote/list") async def list_remote_skills_route( payload: RemoteSkillSourceRequest, - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), ): try: return {"success": True, "data": await list_remote_skills(payload.source)} @@ -229,7 +229,7 @@ async def list_remote_skills_route( @skills.post("/remote/install") async def install_remote_skill_route( payload: RemoteSkillInstallRequest, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): try: @@ -254,10 +254,10 @@ async def install_remote_skill_route( @skills.get("/{slug}/tree") async def get_skill_tree_route( slug: str, - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """获取技能目录树(仅超级管理员)。""" + """获取技能目录树(管理员)。""" try: tree = await get_skill_tree(db, slug) return {"success": True, "data": tree} @@ -274,10 +274,10 @@ async def get_skill_tree_route( async def get_skill_file_route( slug: str, path: str = Query(..., description="相对 skill 根目录路径"), - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """读取技能文本文件(仅超级管理员)。""" + """读取技能文本文件(管理员)。""" try: data = await read_skill_file(db, slug, path) return {"success": True, "data": data} @@ -294,10 +294,10 @@ async def get_skill_file_route( async def create_skill_file_route( slug: str, payload: SkillNodeCreateRequest, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """创建技能文件或目录(仅超级管理员)。""" + """创建技能文件或目录(管理员)。""" try: await create_skill_node( db, @@ -321,10 +321,10 @@ async def create_skill_file_route( async def update_skill_file_route( slug: str, payload: SkillFileUpdateRequest, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """更新技能文本文件(仅超级管理员)。""" + """更新技能文本文件(管理员)。""" try: await update_skill_file( db, @@ -347,10 +347,10 @@ async def update_skill_file_route( async def update_skill_dependencies_route( slug: str, payload: SkillDependenciesUpdateRequest, - current_user: User = Depends(get_superadmin_user), + current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """更新 skill 依赖(仅超级管理员)。""" + """更新 skill 依赖(管理员)。""" try: item = await update_skill_dependencies( db, @@ -374,10 +374,10 @@ async def update_skill_dependencies_route( async def delete_skill_file_route( slug: str, path: str = Query(..., description="相对 skill 根目录路径"), - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """删除技能文件或目录(仅超级管理员)。""" + """删除技能文件或目录(管理员)。""" try: await delete_skill_node(db, slug=slug, relative_path=path) return {"success": True} @@ -394,10 +394,10 @@ async def delete_skill_file_route( async def export_skill_route( slug: str, background_tasks: BackgroundTasks, - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """导出技能压缩包(仅超级管理员)。""" + """导出技能压缩包(管理员)。""" try: export_path, download_name = await export_skill_zip(db, slug) background_tasks.add_task(_cleanup_export_file, export_path) @@ -418,10 +418,10 @@ async def export_skill_route( @skills.delete("/{slug}") async def delete_skill_route( slug: str, - _current_user: User = Depends(get_superadmin_user), + _current_user: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): - """删除技能(目录 + 数据库记录,仅超级管理员)。""" + """删除技能(目录 + 数据库记录,管理员)。""" try: await delete_skill(db, slug=slug) return {"success": True} diff --git a/backend/test/unit/routers/test_skill_router.py b/backend/test/unit/routers/test_skill_router.py index a3fe5188..9fab7c33 100644 --- a/backend/test/unit/routers/test_skill_router.py +++ b/backend/test/unit/routers/test_skill_router.py @@ -4,11 +4,11 @@ from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient from server.routers.skill_router import skills -from server.utils.auth_middleware import get_admin_user, get_db, get_superadmin_user +from server.utils.auth_middleware import get_admin_user, get_db from yuxi.storage.postgres.models_business import Skill, User -def _build_app(*, allow_superadmin: bool) -> FastAPI: +def _build_app(*, allow_admin: bool = True) -> FastAPI: app = FastAPI() app.include_router(skills, prefix="/api") @@ -16,6 +16,8 @@ def _build_app(*, allow_superadmin: bool) -> FastAPI: return None async def fake_admin_user(): + if not allow_admin: + raise HTTPException(status_code=403, detail="需要管理员权限") return User( username="admin", user_id="admin", @@ -23,19 +25,8 @@ def _build_app(*, allow_superadmin: bool) -> FastAPI: role="admin", ) - async def fake_superadmin_user(): - if not allow_superadmin: - raise HTTPException(status_code=403, detail="需要超级管理员权限") - return User( - username="root", - user_id="root", - password_hash="x", - role="superadmin", - ) - app.dependency_overrides[get_db] = fake_db app.dependency_overrides[get_admin_user] = fake_admin_user - app.dependency_overrides[get_superadmin_user] = fake_superadmin_user return app @@ -52,7 +43,7 @@ def test_list_skills_route_returns_data(monkeypatch): monkeypatch.setattr("server.routers.skill_router.list_skills", fake_list_skills) - app = _build_app(allow_superadmin=True) + app = _build_app() client = TestClient(app) resp = client.get("/api/system/skills") assert resp.status_code == 200, resp.text @@ -61,15 +52,21 @@ def test_list_skills_route_returns_data(monkeypatch): assert payload["data"][0]["slug"] == "demo" -def test_import_skill_requires_superadmin(): - app = _build_app(allow_superadmin=False) +def test_skill_management_requires_admin(): + app = _build_app(allow_admin=False) client = TestClient(app) - resp = client.post( + dependency_resp = client.get("/api/system/skills/dependency-options") + assert dependency_resp.status_code == 403 + + import_resp = client.post( "/api/system/skills/import", files={"file": ("demo.zip", b"not zip", "application/zip")}, ) - assert resp.status_code == 403 + assert import_resp.status_code == 403 + + delete_resp = client.delete("/api/system/skills/demo") + assert delete_resp.status_code == 403 def test_import_skill_route_accepts_skill_md(monkeypatch): @@ -90,7 +87,7 @@ def test_import_skill_route_accepts_skill_md(monkeypatch): monkeypatch.setattr("server.routers.skill_router.import_skill_zip", fake_import_skill_zip) - app = _build_app(allow_superadmin=True) + app = _build_app() client = TestClient(app) resp = client.post( @@ -100,7 +97,7 @@ def test_import_skill_route_accepts_skill_md(monkeypatch): assert resp.status_code == 200, resp.text assert captured["filename"] == "SKILL.md" assert "name: demo" in captured["file_bytes"] - assert captured["created_by"] == "root" + assert captured["created_by"] == "admin" def test_update_skill_file_passes_operator(monkeypatch): @@ -114,7 +111,7 @@ def test_update_skill_file_passes_operator(monkeypatch): monkeypatch.setattr("server.routers.skill_router.update_skill_file", fake_update_skill_file) - app = _build_app(allow_superadmin=True) + app = _build_app() client = TestClient(app) resp = client.put( @@ -127,7 +124,7 @@ def test_update_skill_file_passes_operator(monkeypatch): assert resp.status_code == 200, resp.text assert captured["slug"] == "demo" assert captured["relative_path"] == "SKILL.md" - assert captured["updated_by"] == "root" + assert captured["updated_by"] == "admin" def test_dependency_options_route(monkeypatch): @@ -140,7 +137,7 @@ def test_dependency_options_route(monkeypatch): monkeypatch.setattr("server.routers.skill_router.get_skill_dependency_options", fake_get_skill_dependency_options) - app = _build_app(allow_superadmin=True) + app = _build_app() client = TestClient(app) resp = client.get("/api/system/skills/dependency-options") assert resp.status_code == 200, resp.text @@ -178,7 +175,7 @@ def test_update_skill_dependencies_route(monkeypatch): monkeypatch.setattr("server.routers.skill_router.update_skill_dependencies", fake_update_skill_dependencies) - app = _build_app(allow_superadmin=True) + app = _build_app() client = TestClient(app) resp = client.put( "/api/system/skills/demo/dependencies", @@ -193,7 +190,7 @@ def test_update_skill_dependencies_route(monkeypatch): assert captured["tool_dependencies"] == ["calculator"] assert captured["mcp_dependencies"] == ["mcp-a"] assert captured["skill_dependencies"] == ["other-skill"] - assert captured["updated_by"] == "root" + assert captured["updated_by"] == "admin" def test_list_remote_skills_route(monkeypatch): @@ -203,7 +200,7 @@ def test_list_remote_skills_route(monkeypatch): monkeypatch.setattr("server.routers.skill_router.list_remote_skills", fake_list_remote_skills) - app = _build_app(allow_superadmin=True) + app = _build_app() client = TestClient(app) resp = client.post("/api/system/skills/remote/list", json={"source": "anthropics/skills"}) assert resp.status_code == 200, resp.text @@ -230,7 +227,7 @@ def test_install_remote_skill_route(monkeypatch): monkeypatch.setattr("server.routers.skill_router.install_remote_skill", fake_install_remote_skill) - app = _build_app(allow_superadmin=True) + app = _build_app() client = TestClient(app) resp = client.post( "/api/system/skills/remote/install", @@ -242,4 +239,4 @@ def test_install_remote_skill_route(monkeypatch): assert payload["data"]["slug"] == "frontend-design" assert captured["source"] == "anthropics/skills" assert captured["skill"] == "frontend-design" - assert captured["created_by"] == "root" + assert captured["created_by"] == "admin" diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index 1eb7cad8..5004befa 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -18,6 +18,8 @@ - 拓宽检索的知识源,统一多知识源(channel),目前已知知识库/知识图谱/网页,可拓展:个人知识库、数据库、历史对话等 - 前置任务,多知识库并行检索(扩展 query_kb) - 新增 query_keywords 工具,专门用于基于关键词命中的排序,也结合词频(和 BM25 的区别?) +- 调研将当前知识库映射为虚拟文件系统工具的可行性,先明确文件树映射、权限边界、内容读取与 Agent 工具调用形态,再决定是否实现 +- 参考 AgenticRAG 方案扩展当前 Search 工具:等待虚拟文件系统构建完成后,改进 Search 返回递增文件序列 ID,新增 Find 与 Open 能力;Summary 暂缓 - 评估,基于 Agent 的评估,这里应该是结合 Langfuse 实现 ### Bugs @@ -36,6 +38,7 @@ ### 0.6.2 开发记录 +- 下放扩展管理权限:普通管理员现在可进入扩展管理并完整管理 Tools、MCP、SubAgent、Skills;同步放开 Skill 管理接口权限并补充权限测试。 - 调整 Agent 知识库默认选择:未显式配置知识库时默认启用当前用户可访问的全部知识库,显式保存空列表仍表示不启用知识库。 - 优化评估基准自动生成:仅支持 commonrag/Milvus 知识库,默认参考 chunks 数量改为 1;多 chunk 场景复用知识库向量检索选择相似 chunks,不再对全量 chunks 重新计算 embedding,并移除前端 Embedding 模型选择。 - 修复知识库文档入库状态回退:当已解析文件缺失 `markdown_file` 解析产物时,索引流程会将文件状态恢复为未解析,便于重新解析而不是停留在索引失败。 diff --git a/web/src/apis/skill_api.js b/web/src/apis/skill_api.js index e09a4518..e0ddc821 100644 --- a/web/src/apis/skill_api.js +++ b/web/src/apis/skill_api.js @@ -1,10 +1,4 @@ -import { - apiAdminGet, - apiSuperAdminDelete, - apiSuperAdminGet, - apiSuperAdminPost, - apiSuperAdminPut -} from './base' +import { apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base' const BASE_URL = '/api/system/skills' @@ -15,67 +9,67 @@ export const listSkills = async () => { export const importSkillZip = async (file) => { const formData = new FormData() formData.append('file', file) - return apiSuperAdminPost(`${BASE_URL}/import`, formData) + return apiAdminPost(`${BASE_URL}/import`, formData) } export const listRemoteSkills = async (source) => { - return apiSuperAdminPost(`${BASE_URL}/remote/list`, { source }) + return apiAdminPost(`${BASE_URL}/remote/list`, { source }) } export const installRemoteSkill = async (payload) => { - return apiSuperAdminPost(`${BASE_URL}/remote/install`, payload) + return apiAdminPost(`${BASE_URL}/remote/install`, payload) } export const getSkillDependencyOptions = async () => { - return apiSuperAdminGet(`${BASE_URL}/dependency-options`) + return apiAdminGet(`${BASE_URL}/dependency-options`) } export const listBuiltinSkills = async () => { - return apiSuperAdminGet(`${BASE_URL}/builtin`) + return apiAdminGet(`${BASE_URL}/builtin`) } export const installBuiltinSkill = async (slug) => { - return apiSuperAdminPost(`${BASE_URL}/builtin/${encodeURIComponent(slug)}/install`) + return apiAdminPost(`${BASE_URL}/builtin/${encodeURIComponent(slug)}/install`) } export const updateBuiltinSkill = async (slug, force = false) => { - return apiSuperAdminPost(`${BASE_URL}/builtin/${encodeURIComponent(slug)}/update`, { force }) + return apiAdminPost(`${BASE_URL}/builtin/${encodeURIComponent(slug)}/update`, { force }) } export const getSkillTree = async (slug) => { - return apiSuperAdminGet(`${BASE_URL}/${encodeURIComponent(slug)}/tree`) + return apiAdminGet(`${BASE_URL}/${encodeURIComponent(slug)}/tree`) } export const getSkillFile = async (slug, path) => { - return apiSuperAdminGet( + return apiAdminGet( `${BASE_URL}/${encodeURIComponent(slug)}/file?path=${encodeURIComponent(path)}` ) } export const createSkillFile = async (slug, payload) => { - return apiSuperAdminPost(`${BASE_URL}/${encodeURIComponent(slug)}/file`, payload) + return apiAdminPost(`${BASE_URL}/${encodeURIComponent(slug)}/file`, payload) } export const updateSkillFile = async (slug, payload) => { - return apiSuperAdminPut(`${BASE_URL}/${encodeURIComponent(slug)}/file`, payload) + return apiAdminPut(`${BASE_URL}/${encodeURIComponent(slug)}/file`, payload) } export const updateSkillDependencies = async (slug, payload) => { - return apiSuperAdminPut(`${BASE_URL}/${encodeURIComponent(slug)}/dependencies`, payload) + return apiAdminPut(`${BASE_URL}/${encodeURIComponent(slug)}/dependencies`, payload) } export const deleteSkillFile = async (slug, path) => { - return apiSuperAdminDelete( + return apiAdminDelete( `${BASE_URL}/${encodeURIComponent(slug)}/file?path=${encodeURIComponent(path)}` ) } export const exportSkill = async (slug) => { - return apiSuperAdminGet(`${BASE_URL}/${encodeURIComponent(slug)}/export`, {}, 'blob') + return apiAdminGet(`${BASE_URL}/${encodeURIComponent(slug)}/export`, {}, 'blob') } export const deleteSkill = async (slug) => { - return apiSuperAdminDelete(`${BASE_URL}/${encodeURIComponent(slug)}`) + return apiAdminDelete(`${BASE_URL}/${encodeURIComponent(slug)}`) } export const skillApi = { diff --git a/web/src/layouts/AppLayout.vue b/web/src/layouts/AppLayout.vue index aa6a0739..1e1dd734 100644 --- a/web/src/layouts/AppLayout.vue +++ b/web/src/layouts/AppLayout.vue @@ -152,14 +152,12 @@ const mainList = computed(() => { }) } - if (userStore.isSuperAdmin) { - items.push({ - name: '扩展管理', - path: '/extensions', - icon: Blocks, - activeIcon: Blocks - }) - } + items.push({ + name: '扩展管理', + path: '/extensions', + icon: Blocks, + activeIcon: Blocks + }) items.push({ name: '模型配置', diff --git a/web/src/router/index.js b/web/src/router/index.js index f06bb170..7f48d2b1 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -134,8 +134,7 @@ const router = createRouter({ meta: { keepAlive: false, requiresAuth: true, - requiresAdmin: true, - requiresSuperAdmin: true + requiresAdmin: true } }, { @@ -145,8 +144,7 @@ const router = createRouter({ meta: { keepAlive: false, requiresAuth: true, - requiresAdmin: true, - requiresSuperAdmin: true + requiresAdmin: true } }, { @@ -156,8 +154,7 @@ const router = createRouter({ meta: { keepAlive: false, requiresAuth: true, - requiresAdmin: true, - requiresSuperAdmin: true + requiresAdmin: true } }, { @@ -167,8 +164,7 @@ const router = createRouter({ meta: { keepAlive: false, requiresAuth: true, - requiresAdmin: true, - requiresSuperAdmin: true + requiresAdmin: true } } ]