Files
mini-openclaw-v2/backend/api/files.py
T
qiaoqichaoandClaude Code d56c699ce6 chore(v2): 从兴邦预审v2白名单抽取独立可运行基座
按 docs/v2/05-implementation-plan.md §1 候选迁移表与 claude_use/T00 白名单,
从 55e2cdb 抽取 224 个文件(2.48 MB)到独立目录 F:/mini-OpenClaw-v2。

- 排除:凭据(.env/server-deploy.conf/旧DELIVERY.md)、运行数据(xingbang_data 等
  4 个 SQLite)、依赖(venv/node_modules 符号链接)、归档(_backup*/test-evidence/
  ori_file)、Office 临时文件
- 目标目录独立 git 仓库,初始恢复点;Gitea 远程与分支待 OQ-06 Q2 定案
- 未部署、未启动服务、未调用模型

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 12:58:26 +08:00

103 lines
3.2 KiB
Python

"""GET/POST /api/files + GET /api/skills — File read/write for Monaco editor."""
from pathlib import Path
import yaml
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter()
BASE_DIR = Path(__file__).resolve().parent.parent
# Whitelist of editable directories (relative to backend/)
ALLOWED_PREFIXES = ["workspace/", "memory/", "skills/", "knowledge/"]
# Whitelist of specific root-level files that can be accessed
ALLOWED_ROOT_FILES = {"SKILLS_SNAPSHOT.md"}
def _validate_path(rel_path: str) -> Path:
"""Validate and resolve file path within allowed directories."""
normalized = rel_path.replace("\\", "/").lstrip("./")
if not (
any(normalized.startswith(prefix) for prefix in ALLOWED_PREFIXES)
or normalized in ALLOWED_ROOT_FILES
):
raise HTTPException(status_code=403, detail=f"Access denied: {rel_path}")
full_path = (BASE_DIR / normalized).resolve()
if not str(full_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Path traversal detected")
return full_path
@router.get("/files")
async def read_file(path: str):
file_path = _validate_path(path)
if not file_path.exists():
raise HTTPException(status_code=404, detail=f"File not found: {path}")
content = file_path.read_text(encoding="utf-8")
return {"path": path, "content": content}
class FileSaveRequest(BaseModel):
path: str
content: str
@router.post("/files")
async def save_file(request: FileSaveRequest):
file_path = _validate_path(request.path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(request.content, encoding="utf-8")
# Trigger memory index rebuild when MEMORY.md is saved
normalized = request.path.replace("\\", "/").lstrip("./")
if normalized == "memory/MEMORY.md":
try:
from graph.memory_indexer import get_memory_indexer
indexer = get_memory_indexer(BASE_DIR)
indexer.rebuild_index()
except Exception:
pass
return {"path": request.path, "status": "saved"}
@router.get("/skills")
async def list_skills():
"""Scan skills/ directory and return skill list with name, path, description."""
skills_dir = BASE_DIR / "skills"
if not skills_dir.exists():
return {"skills": []}
skills: list[dict[str, str]] = []
for skill_dir in sorted(skills_dir.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
continue
name = skill_dir.name
description = ""
rel_path = f"skills/{name}/SKILL.md"
# Parse YAML frontmatter
try:
text = skill_md.read_text(encoding="utf-8")
if text.startswith("---"):
parts = text.split("---", 2)
if len(parts) >= 3:
meta = yaml.safe_load(parts[1])
if isinstance(meta, dict):
name = meta.get("name", name)
description = meta.get("description", "")
except Exception:
pass
skills.append({"name": name, "path": rel_path, "description": description})
return {"skills": skills}