695 lines
28 KiB
Python
695 lines
28 KiB
Python
"""API v1:Bearer Token 版轻量接口(后台任务 + 轮询 + 文件)。"""
|
||
from __future__ import annotations
|
||
import os
|
||
import json
|
||
import zipfile
|
||
from io import BytesIO
|
||
from pathlib import Path
|
||
from typing import Dict, Any, Optional
|
||
|
||
from flask import Blueprint, request, jsonify, send_file, session
|
||
|
||
from .api_auth import api_token_required
|
||
from .tasks import task_manager
|
||
from .context import get_user_resources, ensure_conversation_loaded, get_upload_guard, apply_conversation_overrides
|
||
from .files import sanitize_filename_preserve_unicode
|
||
from .utils_common import debug_log
|
||
from config.model_profiles import MODEL_PROFILES
|
||
from core.tool_config import TOOL_CATEGORIES
|
||
from . import state
|
||
from modules.personalization_manager import sanitize_personalization_payload
|
||
|
||
api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1")
|
||
|
||
@api_v1_bp.route("/tools", methods=["GET"])
|
||
@api_token_required
|
||
def list_tools_api():
|
||
"""返回工具分类与工具名称列表。"""
|
||
categories = []
|
||
for cid, cat in TOOL_CATEGORIES.items():
|
||
categories.append({
|
||
"id": cid,
|
||
"label": cat.label,
|
||
"default_enabled": bool(cat.default_enabled),
|
||
"silent_when_disabled": bool(cat.silent_when_disabled),
|
||
"tools": list(cat.tools),
|
||
})
|
||
return jsonify({"success": True, "categories": categories})
|
||
|
||
|
||
# -------------------- 工作区管理 --------------------
|
||
def _sanitize_workspace_id(ws_id: str) -> str:
|
||
import re
|
||
ws = (ws_id or "").strip()
|
||
if not ws:
|
||
return ""
|
||
if not re.fullmatch(r"[A-Za-z0-9._-]{1,40}", ws):
|
||
return ""
|
||
return ws
|
||
|
||
|
||
def _public_workspace_info(workspace) -> Dict[str, Any]:
|
||
"""对外返回的工作区信息,隐藏宿主机绝对路径。"""
|
||
return {
|
||
"success": True,
|
||
"workspace_id": workspace.workspace_id,
|
||
"project_path": "project", # 相对工作区目录,避免暴露宿主机路径
|
||
"data_dir": "data",
|
||
}
|
||
|
||
|
||
@api_v1_bp.route("/workspaces", methods=["GET"])
|
||
@api_token_required
|
||
def list_workspaces_api():
|
||
username = session.get("username")
|
||
items = state.api_user_manager.list_workspaces(username)
|
||
return jsonify({"success": True, "items": list(items.values())})
|
||
|
||
|
||
@api_v1_bp.route("/workspaces", methods=["POST"])
|
||
@api_token_required
|
||
def create_workspace_api():
|
||
username = session.get("username")
|
||
payload = request.get_json(silent=True) or {}
|
||
ws_id = _sanitize_workspace_id(payload.get("workspace_id") or payload.get("name") or "")
|
||
if not ws_id:
|
||
return jsonify({"success": False, "error": "workspace_id 只能包含字母/数字/._-,长度1-40"}), 400
|
||
try:
|
||
ws = state.api_user_manager.ensure_workspace(username, ws_id)
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": str(exc)}), 500
|
||
return jsonify(_public_workspace_info(ws))
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>", methods=["GET"])
|
||
@api_token_required
|
||
def get_workspace_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws_id = _sanitize_workspace_id(workspace_id)
|
||
if not ws_id:
|
||
return jsonify({"success": False, "error": "workspace_id 不合法"}), 400
|
||
workspaces = state.api_user_manager.list_workspaces(username)
|
||
ws_info = workspaces.get(ws_id)
|
||
if not ws_info:
|
||
return jsonify({"success": False, "error": "workspace 不存在"}), 404
|
||
return jsonify({"success": True, "workspace": ws_info})
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>", methods=["DELETE"])
|
||
@api_token_required
|
||
def delete_workspace_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws_id = _sanitize_workspace_id(workspace_id)
|
||
if not ws_id:
|
||
return jsonify({"success": False, "error": "workspace_id 不合法"}), 400
|
||
# 阻止删除有运行中任务的工作区
|
||
running = [t for t in task_manager.list_tasks(username, ws_id) if t.status in {"pending", "running"}]
|
||
if running:
|
||
return jsonify({"success": False, "error": "该工作区有运行中的任务,无法删除"}), 409
|
||
removed = state.api_user_manager.delete_workspace(username, ws_id)
|
||
# 清理终端/容器缓存
|
||
term_key = f"{username}::{ws_id}"
|
||
state.user_terminals.pop(term_key, None)
|
||
try:
|
||
state.container_manager.release_container(term_key, reason="workspace_deleted")
|
||
except Exception:
|
||
pass
|
||
if not removed:
|
||
return jsonify({"success": False, "error": "workspace 不存在或删除失败"}), 404
|
||
return jsonify({"success": True, "workspace_id": ws_id})
|
||
|
||
|
||
def _within_uploads(workspace, rel_path: str) -> Path:
|
||
base = Path(workspace.uploads_dir).resolve()
|
||
rel = rel_path or ""
|
||
# 兼容传入包含 user_upload/ 前缀的路径
|
||
if rel.startswith("user_upload/"):
|
||
rel = rel.split("user_upload/", 1)[1]
|
||
rel = rel.lstrip("/").strip()
|
||
target = (base / rel).resolve()
|
||
if not str(target).startswith(str(base)):
|
||
raise ValueError("非法路径")
|
||
return target
|
||
|
||
|
||
def _within_project(workspace, rel_path: str, default_to_project_root: bool = True) -> Path:
|
||
"""
|
||
将相对路径解析到工作区 project 目录内,防止越界。
|
||
|
||
Args:
|
||
workspace: ApiUserWorkspace
|
||
rel_path: 相对路径,允许以 user_upload/ 开头(向后兼容)
|
||
default_to_project_root: 当 rel_path 为空时,是否默认指向 project 根
|
||
"""
|
||
base = Path(workspace.project_path).resolve()
|
||
rel = (rel_path or "").strip()
|
||
if not rel and default_to_project_root:
|
||
rel = ""
|
||
# 兼容 /workspace/<...>/ 前缀(容器路径)
|
||
if rel.startswith("/workspace/"):
|
||
rel = rel.split("/workspace/", 1)[1]
|
||
rel = rel.lstrip("/")
|
||
target = (base / rel).resolve()
|
||
if not str(target).startswith(str(base)):
|
||
raise ValueError("非法路径")
|
||
return target
|
||
|
||
|
||
def _conversation_path(workspace, conv_id: str) -> Path:
|
||
return Path(workspace.data_dir) / "conversations" / f"{conv_id}.json"
|
||
|
||
|
||
def _update_conversation_metadata(workspace, conv_id: str, updates: Dict[str, Any]):
|
||
path = _conversation_path(workspace, conv_id)
|
||
if not path.exists():
|
||
return
|
||
try:
|
||
data = {}
|
||
try:
|
||
data = json.loads(path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
data = {}
|
||
meta = data.get("metadata") or {}
|
||
meta.update({k: v for k, v in updates.items() if v is not None})
|
||
data["metadata"] = meta
|
||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
except Exception as exc:
|
||
debug_log(f"[meta] 更新对话元数据失败: {exc}")
|
||
|
||
|
||
def _prompt_dir(workspace):
|
||
# 优先使用用户级共享目录(ApiUserWorkspace.prompts_dir),否则退回 data_dir/prompts
|
||
target = getattr(workspace, "prompts_dir", None)
|
||
if target:
|
||
Path(target).mkdir(parents=True, exist_ok=True)
|
||
return Path(target)
|
||
p = Path(workspace.data_dir) / "prompts"
|
||
p.mkdir(parents=True, exist_ok=True)
|
||
return p
|
||
|
||
|
||
def _personalization_dir(workspace):
|
||
target = getattr(workspace, "personalization_dir", None)
|
||
if target:
|
||
Path(target).mkdir(parents=True, exist_ok=True)
|
||
return Path(target)
|
||
p = Path(workspace.data_dir) / "personalization"
|
||
p.mkdir(parents=True, exist_ok=True)
|
||
return p
|
||
|
||
|
||
def _resolve_workspace(username: str, workspace_id: str):
|
||
if not workspace_id:
|
||
raise RuntimeError("workspace_id 不能为空")
|
||
return state.api_user_manager.ensure_workspace(username, workspace_id)
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/conversations", methods=["POST"])
|
||
@api_token_required
|
||
def create_conversation_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
terminal, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not terminal:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
payload = request.get_json(silent=True) or {}
|
||
prompt_name = payload.get("prompt_name")
|
||
personalization_name = payload.get("personalization_name")
|
||
result = terminal.create_new_conversation()
|
||
if not result.get("success"):
|
||
return jsonify({"success": False, "error": result.get("error") or "创建对话失败"}), 500
|
||
conv_id = result.get("conversation_id")
|
||
# 记录元数据,稍后消息时可自动应用
|
||
_update_conversation_metadata(workspace, conv_id, {
|
||
"custom_prompt_name": prompt_name,
|
||
"personalization_name": personalization_name,
|
||
"workspace_id": ws.workspace_id,
|
||
})
|
||
# 立即应用覆盖
|
||
apply_conversation_overrides(terminal, workspace, conv_id)
|
||
return jsonify({"success": True, "conversation_id": conv_id, "workspace_id": ws.workspace_id})
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/messages", methods=["POST"])
|
||
@api_token_required
|
||
def send_message_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
payload = request.get_json() or {}
|
||
message = (payload.get("message") or "").strip()
|
||
images = payload.get("images") or []
|
||
conversation_id = payload.get("conversation_id")
|
||
model_key = payload.get("model_key")
|
||
thinking_mode = payload.get("thinking_mode")
|
||
run_mode = payload.get("run_mode")
|
||
max_iterations = payload.get("max_iterations")
|
||
prompt_name = payload.get("prompt_name")
|
||
personalization_name = payload.get("personalization_name")
|
||
if not message and not images:
|
||
return jsonify({"success": False, "error": "消息不能为空"}), 400
|
||
|
||
terminal, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not terminal or not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
try:
|
||
conversation_id, _ = ensure_conversation_loaded(terminal, conversation_id, workspace=workspace)
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": f"对话加载失败: {exc}"}), 400
|
||
|
||
# 应用自定义 prompt/personalization(如果提供)
|
||
try:
|
||
if prompt_name:
|
||
prompt_path = _prompt_dir(workspace) / f"{prompt_name}.txt"
|
||
if not prompt_path.exists():
|
||
return jsonify({"success": False, "error": "prompt 不存在"}), 404
|
||
terminal.context_manager.custom_system_prompt = prompt_path.read_text(encoding="utf-8")
|
||
if personalization_name:
|
||
pers_path = _personalization_dir(workspace) / f"{personalization_name}.json"
|
||
if not pers_path.exists():
|
||
return jsonify({"success": False, "error": "personalization 不存在"}), 404
|
||
try:
|
||
terminal.context_manager.custom_personalization_config = json.loads(pers_path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
return jsonify({"success": False, "error": "personalization 解析失败"}), 400
|
||
# 持久化到对话元数据
|
||
_update_conversation_metadata(workspace, conversation_id, {
|
||
"custom_prompt_name": prompt_name,
|
||
"personalization_name": personalization_name,
|
||
"workspace_id": ws.workspace_id,
|
||
})
|
||
# 立即应用个性化偏好(确保禁用工具分类生效)
|
||
try:
|
||
terminal.apply_personalization_preferences(terminal.context_manager.custom_personalization_config)
|
||
except Exception as exc:
|
||
debug_log(f"[api_v1] 应用个性化失败: {exc}")
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": f"自定义参数错误: {exc}"}), 400
|
||
|
||
try:
|
||
rec = task_manager.create_chat_task(
|
||
username=username,
|
||
workspace_id=ws.workspace_id,
|
||
message=message,
|
||
images=images,
|
||
conversation_id=conversation_id,
|
||
model_key=model_key,
|
||
thinking_mode=thinking_mode,
|
||
run_mode=run_mode,
|
||
max_iterations=max_iterations,
|
||
)
|
||
except ValueError as exc:
|
||
return jsonify({"success": False, "error": str(exc)}), 400
|
||
except RuntimeError as exc:
|
||
# 并发等业务冲突仍用 409
|
||
return jsonify({"success": False, "error": str(exc)}), 409
|
||
return jsonify({
|
||
"success": True,
|
||
"task_id": rec.task_id,
|
||
"conversation_id": rec.conversation_id,
|
||
"workspace_id": ws.workspace_id,
|
||
"status": rec.status,
|
||
"created_at": rec.created_at,
|
||
}), 202
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/conversations", methods=["GET"])
|
||
@api_token_required
|
||
def list_conversations_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
terminal, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not terminal or not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
limit = max(1, min(int(request.args.get("limit", 20)), 100))
|
||
offset = max(0, int(request.args.get("offset", 0)))
|
||
conv_dir = Path(workspace.data_dir) / "conversations"
|
||
items = []
|
||
for p in sorted(conv_dir.glob("conv_*.json"), key=lambda x: x.stat().st_mtime, reverse=True):
|
||
try:
|
||
data = json.loads(p.read_text(encoding="utf-8"))
|
||
meta = data.get("metadata") or {}
|
||
items.append({
|
||
"id": data.get("id"),
|
||
"title": data.get("title"),
|
||
"created_at": data.get("created_at"),
|
||
"updated_at": data.get("updated_at"),
|
||
"run_mode": meta.get("run_mode"),
|
||
"model_key": meta.get("model_key"),
|
||
"custom_prompt_name": meta.get("custom_prompt_name"),
|
||
"personalization_name": meta.get("personalization_name"),
|
||
"workspace_id": ws.workspace_id,
|
||
"messages_count": len(data.get("messages", [])),
|
||
})
|
||
except Exception:
|
||
continue
|
||
sliced = items[offset:offset + limit]
|
||
return jsonify({"success": True, "data": sliced, "total": len(items)})
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/conversations/<conv_id>", methods=["GET"])
|
||
@api_token_required
|
||
def get_conversation_api(workspace_id: str, conv_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
path = _conversation_path(workspace, conv_id)
|
||
if not path.exists():
|
||
return jsonify({"success": False, "error": "对话不存在"}), 404
|
||
try:
|
||
data = json.loads(path.read_text(encoding="utf-8"))
|
||
include_messages = request.args.get("full", "0") == "1"
|
||
if not include_messages:
|
||
data["messages"] = None
|
||
return jsonify({"success": True, "data": data, "workspace_id": ws.workspace_id})
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": str(exc)}), 500
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/conversations/<conv_id>", methods=["DELETE"])
|
||
@api_token_required
|
||
def delete_conversation_api(workspace_id: str, conv_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
terminal, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not terminal or not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
result = terminal.delete_conversation(conv_id)
|
||
status = 200 if result.get("success") else 404
|
||
result["workspace_id"] = ws.workspace_id
|
||
return jsonify(result), status
|
||
|
||
|
||
@api_v1_bp.route("/tasks/<task_id>", methods=["GET"])
|
||
@api_token_required
|
||
def get_task_events(task_id: str):
|
||
username = session.get("username")
|
||
rec = task_manager.get_task(username, task_id)
|
||
if not rec:
|
||
return jsonify({"success": False, "error": "任务不存在"}), 404
|
||
try:
|
||
offset = int(request.args.get("from", 0))
|
||
except Exception:
|
||
offset = 0
|
||
events = [e for e in rec.events if e["idx"] >= offset]
|
||
next_offset = events[-1]["idx"] + 1 if events else offset
|
||
return jsonify({
|
||
"success": True,
|
||
"data": {
|
||
"task_id": rec.task_id,
|
||
"workspace_id": getattr(rec, "workspace_id", None),
|
||
"status": rec.status,
|
||
"created_at": rec.created_at,
|
||
"updated_at": rec.updated_at,
|
||
"error": rec.error,
|
||
"events": events,
|
||
"next_offset": next_offset,
|
||
}
|
||
})
|
||
|
||
|
||
@api_v1_bp.route("/tasks/<task_id>/cancel", methods=["POST"])
|
||
@api_token_required
|
||
def cancel_task_api_v1(task_id: str):
|
||
username = session.get("username")
|
||
ok = task_manager.cancel_task(username, task_id)
|
||
if not ok:
|
||
return jsonify({"success": False, "error": "任务不存在"}), 404
|
||
return jsonify({"success": True})
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/files/upload", methods=["POST"])
|
||
@api_token_required
|
||
def upload_file_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
terminal, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not terminal or not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
if 'file' not in request.files:
|
||
return jsonify({"success": False, "error": "未找到文件"}), 400
|
||
file_obj = request.files['file']
|
||
raw_name = request.form.get('filename') or file_obj.filename
|
||
filename = sanitize_filename_preserve_unicode(raw_name)
|
||
if not filename:
|
||
return jsonify({"success": False, "error": "非法文件名"}), 400
|
||
subdir = request.form.get("dir") or ""
|
||
try:
|
||
target_dir = _within_uploads(workspace, subdir)
|
||
target_dir.mkdir(parents=True, exist_ok=True)
|
||
target_path = (target_dir / filename).resolve()
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": str(exc)}), 400
|
||
|
||
guard = get_upload_guard(workspace)
|
||
rel_path = str(target_path.relative_to(workspace.uploads_dir))
|
||
try:
|
||
result = guard.process_upload(
|
||
file_obj,
|
||
target_path,
|
||
username=username,
|
||
source="api_v1",
|
||
original_name=raw_name,
|
||
relative_path=rel_path,
|
||
)
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": f"保存文件失败: {exc}"}), 500
|
||
|
||
metadata = result.get("metadata", {})
|
||
return jsonify({
|
||
"success": True,
|
||
"workspace_id": ws.workspace_id,
|
||
"path": rel_path,
|
||
"filename": target_path.name,
|
||
"size": metadata.get("size"),
|
||
"sha256": metadata.get("sha256"),
|
||
})
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/files", methods=["GET"])
|
||
@api_token_required
|
||
def list_files_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
rel = request.args.get("path") or ""
|
||
try:
|
||
target = _within_project(workspace, rel)
|
||
if not target.exists():
|
||
return jsonify({"success": False, "error": "路径不存在"}), 404
|
||
if not target.is_dir():
|
||
stat = target.stat()
|
||
rel_entry = target.relative_to(workspace.project_path)
|
||
return jsonify({
|
||
"success": True,
|
||
"workspace_id": ws.workspace_id,
|
||
"items": [{
|
||
"name": target.name,
|
||
"is_dir": False,
|
||
"size": stat.st_size,
|
||
"modified_at": stat.st_mtime,
|
||
"path": str(rel_entry),
|
||
}],
|
||
"base": str(rel_entry.parent) if rel_entry.parent != Path(".") else "",
|
||
})
|
||
items = []
|
||
for entry in sorted(target.iterdir(), key=lambda p: p.name):
|
||
stat = entry.stat()
|
||
rel_entry = entry.relative_to(workspace.project_path)
|
||
items.append({
|
||
"name": entry.name,
|
||
"is_dir": entry.is_dir(),
|
||
"size": stat.st_size,
|
||
"modified_at": stat.st_mtime,
|
||
"path": str(rel_entry),
|
||
})
|
||
base_rel = target.relative_to(workspace.project_path)
|
||
base_str = "" if str(base_rel) == "." else str(base_rel)
|
||
return jsonify({"success": True, "workspace_id": ws.workspace_id, "items": items, "base": base_str})
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": str(exc)}), 400
|
||
|
||
|
||
@api_v1_bp.route("/workspaces/<workspace_id>/files/download", methods=["GET"])
|
||
@api_token_required
|
||
def download_file_api(workspace_id: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, workspace_id)
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
rel = request.args.get("path")
|
||
if not rel:
|
||
return jsonify({"success": False, "error": "缺少 path"}), 400
|
||
try:
|
||
target = _within_project(workspace, rel)
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": str(exc)}), 400
|
||
if not target.exists():
|
||
return jsonify({"success": False, "error": "文件不存在"}), 404
|
||
|
||
if target.is_dir():
|
||
memory_file = BytesIO()
|
||
with zipfile.ZipFile(memory_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
|
||
for root, _, files in os.walk(target):
|
||
for file in files:
|
||
full_path = Path(root) / file
|
||
arcname = full_path.relative_to(workspace.project_path)
|
||
zf.write(full_path, arcname=str(arcname))
|
||
memory_file.seek(0)
|
||
download_name = f"{target.name}.zip"
|
||
return send_file(memory_file, as_attachment=True, download_name=download_name, mimetype='application/zip')
|
||
return send_file(target, as_attachment=True, download_name=target.name)
|
||
|
||
|
||
@api_v1_bp.route("/prompts", methods=["GET"])
|
||
@api_token_required
|
||
def list_prompts_api():
|
||
username = session.get("username")
|
||
# prompts 按用户共享,使用默认工作区上下文
|
||
ws = _resolve_workspace(username, "default")
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
base = _prompt_dir(workspace)
|
||
items = []
|
||
for p in sorted(base.glob("*.txt")):
|
||
stat = p.stat()
|
||
items.append({
|
||
"name": p.stem,
|
||
"size": stat.st_size,
|
||
"updated_at": stat.st_mtime,
|
||
})
|
||
return jsonify({"success": True, "items": items})
|
||
|
||
|
||
@api_v1_bp.route("/prompts/<name>", methods=["GET"])
|
||
@api_token_required
|
||
def get_prompt_api(name: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, "default")
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
p = _prompt_dir(workspace) / f"{name}.txt"
|
||
if not p.exists():
|
||
return jsonify({"success": False, "error": "prompt 不存在"}), 404
|
||
return jsonify({"success": True, "name": name, "content": p.read_text(encoding="utf-8")})
|
||
|
||
|
||
@api_v1_bp.route("/prompts", methods=["POST"])
|
||
@api_token_required
|
||
def create_prompt_api():
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, "default")
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
payload = request.get_json() or {}
|
||
name = (payload.get("name") or "").strip()
|
||
content = payload.get("content") or ""
|
||
if not name:
|
||
return jsonify({"success": False, "error": "name 不能为空"}), 400
|
||
p = _prompt_dir(workspace) / f"{name}.txt"
|
||
p.write_text(content, encoding="utf-8")
|
||
return jsonify({"success": True, "name": name})
|
||
|
||
|
||
@api_v1_bp.route("/personalizations", methods=["GET"])
|
||
@api_token_required
|
||
def list_personalizations_api():
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, "default")
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
base = _personalization_dir(workspace)
|
||
items = []
|
||
for p in sorted(base.glob("*.json")):
|
||
stat = p.stat()
|
||
items.append({
|
||
"name": p.stem,
|
||
"size": stat.st_size,
|
||
"updated_at": stat.st_mtime,
|
||
})
|
||
return jsonify({"success": True, "items": items})
|
||
|
||
|
||
@api_v1_bp.route("/personalizations/<name>", methods=["GET"])
|
||
@api_token_required
|
||
def get_personalization_api(name: str):
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, "default")
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
p = _personalization_dir(workspace) / f"{name}.json"
|
||
if not p.exists():
|
||
return jsonify({"success": False, "error": "personalization 不存在"}), 404
|
||
try:
|
||
raw = json.loads(p.read_text(encoding="utf-8"))
|
||
# 读取时也做一次规范化,避免历史脏数据一直向外暴露
|
||
content = sanitize_personalization_payload(raw)
|
||
if content != raw:
|
||
p.write_text(json.dumps(content, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": f"解析失败: {exc}"}), 500
|
||
return jsonify({"success": True, "name": name, "content": content})
|
||
|
||
|
||
@api_v1_bp.route("/personalizations", methods=["POST"])
|
||
@api_token_required
|
||
def create_personalization_api():
|
||
username = session.get("username")
|
||
ws = _resolve_workspace(username, "default")
|
||
_, workspace = get_user_resources(username, workspace_id=ws.workspace_id)
|
||
if not workspace:
|
||
return jsonify({"success": False, "error": "系统未初始化"}), 503
|
||
payload = request.get_json() or {}
|
||
name = (payload.get("name") or "").strip()
|
||
content = payload.get("content")
|
||
if not name:
|
||
return jsonify({"success": False, "error": "name 不能为空"}), 400
|
||
if content is None:
|
||
return jsonify({"success": False, "error": "content 不能为空"}), 400
|
||
p = _personalization_dir(workspace) / f"{name}.json"
|
||
try:
|
||
if not isinstance(content, dict):
|
||
return jsonify({"success": False, "error": "content 必须是 JSON object"}), 400
|
||
existing = None
|
||
if p.exists():
|
||
try:
|
||
existing = json.loads(p.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
existing = None
|
||
# 规范化/清洗:截断长度、过滤非法值、回落默认
|
||
sanitized = sanitize_personalization_payload(content, fallback=existing)
|
||
p.write_text(json.dumps(sanitized, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
except Exception as exc:
|
||
return jsonify({"success": False, "error": f"保存失败: {exc}"}), 500
|
||
return jsonify({"success": True, "name": name, "content": sanitized})
|
||
|
||
|
||
@api_v1_bp.route("/models", methods=["GET"])
|
||
def list_models_api():
|
||
items = []
|
||
for key, profile in MODEL_PROFILES.items():
|
||
items.append({
|
||
"model_key": key,
|
||
"name": profile.get("name", key),
|
||
"supports_thinking": profile.get("supports_thinking", False),
|
||
"fast_only": profile.get("fast_only", False),
|
||
})
|
||
return jsonify({"success": True, "items": items})
|
||
|
||
|
||
@api_v1_bp.route("/health", methods=["GET"])
|
||
def health_api():
|
||
return jsonify({"success": True, "status": "ok"})
|
||
|
||
|
||
__all__ = ["api_v1_bp"]
|