111 lines
5.2 KiB
Python
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}
|