121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
"""CRUD API for a user's personal custom prompts (常用提示词).
|
|
|
|
Prompts are bound to the requesting user (not stored in the browser), so they
|
|
follow the account across devices/sessions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from uuid import uuid4
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from deerflow.runtime.user_context import get_effective_user_id
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/user-prompts", tags=["user-prompts"])
|
|
|
|
|
|
class UserPromptResponse(BaseModel):
|
|
id: str
|
|
title: str = ""
|
|
content: str
|
|
sort_order: int = 0
|
|
# Optional single-agent association. NULL = global (shown for every agent).
|
|
agent_id: str | None = None
|
|
created_at: datetime | str | None = None
|
|
updated_at: datetime | str | None = None
|
|
|
|
|
|
class UserPromptsListResponse(BaseModel):
|
|
prompts: list[UserPromptResponse]
|
|
|
|
|
|
class UserPromptCreateRequest(BaseModel):
|
|
title: str = Field(default="", max_length=255)
|
|
content: str = Field(..., min_length=1)
|
|
sort_order: int = Field(default=0)
|
|
agent_id: str | None = Field(default=None, max_length=64)
|
|
|
|
|
|
class UserPromptUpdateRequest(BaseModel):
|
|
title: str | None = Field(default=None, max_length=255)
|
|
content: str | None = Field(default=None, min_length=1)
|
|
sort_order: int | None = None
|
|
# Sent on every save; an empty string / null clears the association. Use a
|
|
# sentinel so "field omitted" (don't touch) differs from "set to null".
|
|
agent_id: str | None = Field(default=None, max_length=64)
|
|
|
|
|
|
def _current_user_id(request: Request) -> str:
|
|
user = getattr(request.state, "user", None)
|
|
if user is not None:
|
|
return str(user.id)
|
|
return get_effective_user_id()
|
|
|
|
|
|
def _get_store(request: Request):
|
|
store = getattr(request.app.state, "user_prompt_store", None)
|
|
if store is None:
|
|
raise HTTPException(status_code=503, detail="User prompt store not available")
|
|
return store
|
|
|
|
|
|
@router.get("", response_model=UserPromptsListResponse)
|
|
async def list_user_prompts(request: Request) -> UserPromptsListResponse:
|
|
store = _get_store(request)
|
|
user_id = _current_user_id(request)
|
|
rows = await store.list_for_user(user_id)
|
|
return UserPromptsListResponse(prompts=[UserPromptResponse(**r) for r in rows])
|
|
|
|
|
|
@router.post("", response_model=UserPromptResponse, status_code=201)
|
|
async def create_user_prompt(request: Request, body: UserPromptCreateRequest) -> UserPromptResponse:
|
|
store = _get_store(request)
|
|
user_id = _current_user_id(request)
|
|
row = await store.create(
|
|
user_id,
|
|
{
|
|
"id": uuid4().hex,
|
|
"title": (body.title or "").strip(),
|
|
"content": body.content,
|
|
"sort_order": body.sort_order,
|
|
"agent_id": (body.agent_id or "").strip() or None,
|
|
},
|
|
)
|
|
return UserPromptResponse(**row)
|
|
|
|
|
|
@router.put("/{prompt_id}", response_model=UserPromptResponse)
|
|
async def update_user_prompt(request: Request, prompt_id: str, body: UserPromptUpdateRequest) -> UserPromptResponse:
|
|
store = _get_store(request)
|
|
user_id = _current_user_id(request)
|
|
data: dict = {}
|
|
if body.title is not None:
|
|
data["title"] = body.title.strip()
|
|
if body.content is not None:
|
|
data["content"] = body.content
|
|
if body.sort_order is not None:
|
|
data["sort_order"] = body.sort_order
|
|
# ``agent_id`` is nullable: only act when the caller actually sent the field
|
|
# (``model_fields_set``), so an explicit null clears the association while an
|
|
# omitted field leaves it untouched.
|
|
if "agent_id" in body.model_fields_set:
|
|
data["agent_id"] = (body.agent_id or "").strip() or None
|
|
row = await store.update(prompt_id, user_id, data)
|
|
if row is None:
|
|
raise HTTPException(status_code=404, detail="Prompt not found")
|
|
return UserPromptResponse(**row)
|
|
|
|
|
|
@router.delete("/{prompt_id}", status_code=204)
|
|
async def delete_user_prompt(request: Request, prompt_id: str) -> None:
|
|
store = _get_store(request)
|
|
user_id = _current_user_id(request)
|
|
deleted = await store.delete(prompt_id, user_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=404, detail="Prompt not found")
|