zcbot/web/routers/skills_memory.py

111 lines
5.2 KiB
Python

"""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/<name>/SKILL.md` + 用户 `.skills/<name>/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/<name>/` 整目录)。
只能删 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}