"""Skills(列表/详情/删除用户 skill)与 Memory(只读全貌/单篇)路由。""" from __future__ import annotations from uuid import UUID from fastapi import Depends, HTTPException def register_skill_memory_routes(app, *, require_user) -> None: @app.get("/v1/skills", tags=["skills"]) def list_skills(user_id: UUID = Depends(require_user)): """列出当前用户可用的 skill(内置 + 自己的),供新建 task 时下拉选择。 每次请求现扫(内置 `skills//SKILL.md` + 用户 `.skills//SKILL.md`, 稳态 ~3ms),加 / 改 / 删 skill 目录后无需重启即可在前端看到。 `core/agent_builder.py::build_agent` 同样每次新建 SkillRegistry,所以 agent 内部 `load_skill` 与 system prompt discovery 也是热的。源标 `source`(builtin/user)+ `overrides_builtin`(用户 skill 覆盖了同名内置)。`load_errors` 列出用户 skill 因 frontmatter 问题未加载的,供前端提示。 """ from core.agent_builder import build_skill_registry, load_config, resolve_workspace cfg = load_config() ws = resolve_workspace(None, cfg) reg = build_skill_registry(cfg, ws, user_id, docker=False) return { "skills": [ { "name": s.name, "description": s.description, "source": s.source, "overrides_builtin": s.name in reg.user_overrides, } for s in reg.skills.values() ], "load_errors": [{"name": n, "reason": r} for n, r in reg.load_errors], } @app.get("/v1/skills/{name}", tags=["skills"]) def get_skill(name: str, user_id: UUID = Depends(require_user)): """返回某 skill 的完整 SKILL.md 正文(供前端 modal 展开查看)。 内置 + 用户两来源都可查;同名时按 user wins 取用户那份(与 agent 看到的一致)。 """ from core.agent_builder import build_skill_registry, load_config, resolve_workspace cfg = load_config() ws = resolve_workspace(None, cfg) reg = build_skill_registry(cfg, ws, user_id, docker=False) skill = reg.get(name) if skill is None: raise HTTPException(404, f"skill not found: {name!r}") try: content = skill.full_content() except OSError as e: raise HTTPException(500, f"读取 SKILL.md 失败: {e}") return { "name": skill.name, "source": skill.source, "overrides_builtin": skill.name in reg.user_overrides, "content": content, } @app.delete("/v1/skills/{name}", status_code=204, tags=["skills"]) def delete_skill(name: str, user_id: UUID = Depends(require_user)): """删除当前用户的私有 skill(`.skills//` 整目录)。 只能删 user 源 —— 内置 skill 不可删(404,等同"用户那里没有这个可删的")。 `.skills` 在文件面板隐藏,这是 UI 上删除自己 skill 的唯一入口。 """ import shutil from core.agent_builder import build_skill_registry, load_config, resolve_workspace, user_root cfg = load_config() ws = resolve_workspace(None, cfg) reg = build_skill_registry(cfg, ws, user_id, docker=False) skill = reg.get(name) if skill is None or skill.source != "user": raise HTTPException(404, f"no user skill to delete: {name!r}") # 防穿越:目标必须落在该用户的 .skills 子树内(skill_dir 来自扫描,理应如此,仍兜一层) user_skills_dir = (user_root(ws, user_id) / ".skills").resolve() target = skill.skill_dir.resolve() if user_skills_dir not in target.parents: raise HTTPException(400, "拒绝删除 .skills 之外的路径") shutil.rmtree(target) @app.get("/v1/memory", tags=["memory"]) def get_memory(user_id: UUID = Depends(require_user)): """记忆全貌(只读):core.md 原文 + extended 列表(filename + description)。 前端「记忆」弹框一次拉满。**只读** —— 改记忆全走对话(agent 自管,见 `core/memory.py::_CONTRACT`),GUI 当"眼睛"不当"手"(DESIGN §3.7)。 每次现读 FS(同 skills 端点),agent 刚写的即时可见。 """ from core.agent_builder import resolve_workspace from core.memory import memory_view ws = resolve_workspace(None) return memory_view(ws, user_id) @app.get("/v1/memory/extended/{filename}", tags=["memory"]) def get_memory_extended(filename: str, user_id: UUID = Depends(require_user)): """读单篇 extended 原文(点开列表项时拉)。文件名非法 / 不存在 → 404。 路径穿越校验收口在 `core/memory.py::read_extended_file`(只许 `.memory/extended/` 下扁平 `.md`,再 resolve 兜一层子树)。 """ from core.agent_builder import resolve_workspace from core.memory import read_extended_file ws = resolve_workspace(None) content = read_extended_file(ws, user_id, filename) if content is None: raise HTTPException(404, f"memory file not found: {filename!r}") return {"filename": filename, "content": content}