diff --git a/api/app/routers/generate.py b/api/app/routers/generate.py index 8023c66..cdf7ed2 100644 --- a/api/app/routers/generate.py +++ b/api/app/routers/generate.py @@ -1,7 +1,8 @@ """智能笑话生成器 API""" +import json from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from openai import OpenAI from sqlalchemy.orm import Session @@ -21,17 +22,43 @@ GENERATION_PROMPT = """你是一位幽默大师,专门创作轻松搞笑的短 要求: 1. 根据场景和关键词创作一条原创笑话 2. 笑话要有反转或意外结局 -3. 语言简洁,30-150字 -4. 直接输出笑话内容,不需要解释 +3. 语言风格:{style} +4. 字数要求:{length_requirement} +5. 直接输出笑话内容,不需要解释 -格式: -标题:xxx -内容:xxx -""" +请严格按照以下 JSON 格式输出,不要加任何额外说明: +{{ + "title": "笑话标题", + "content": "笑话正文", + "score": 8, + "reason": "这个笑话巧妙结合了场景和关键词,结尾有反转" +}} + +score 是 1-10 的整数评分,reason 是用一句话说明亮点。""" + +STYLE_MAP = { + "cold": "冷幽默 / 无厘头", + "warm": "温馨幽默 / 暖心搞笑", + "twist": "反转 / 神转折", + "pun": "谐音梗 / 文字游戏", + "sketch": "段子 / 吐槽调侃", + "irony": "讽刺幽默 / 黑色幽默", +} + +LENGTH_MAP = { + "short": "30-80字,非常简短", + "medium": "80-150字,正常长度", + "long": "150-300字,可以描述一个小场景", +} -def _build_prompt(scenarios: list[str], keywords: list[str]) -> str: - """构建 AI prompt""" +def _build_prompt( + scenarios: list[str], + keywords: list[str], + style: str = "twist", + length: str = "medium", +) -> str: + """构建 AI prompt,支持风格和长度控制""" parts = [] if scenarios: parts.append(f"场景:{', '.join(scenarios)}") @@ -39,7 +66,11 @@ def _build_prompt(scenarios: list[str], keywords: list[str]) -> str: parts.append(f"关键词:{', '.join(keywords)}") if not parts: parts.append("场景:日常生活的各种趣事(不指定具体场景)") - return GENERATION_PROMPT.format(context="\n".join(parts)) + return GENERATION_PROMPT.format( + context="\n".join(parts), + style=STYLE_MAP.get(style, "反转 / 神转折"), + length_requirement=LENGTH_MAP.get(length, "80-150字,正常长度"), + ) @router.post("", response_model=GenerateResponse) @@ -47,11 +78,10 @@ def generate_joke( req: GenerateRequest, db: Session = Depends(get_db), ): - """调用 AI 生成笑话""" + """调用 AI 生成笑话,支持风格和长度控制""" # 获取激活的 AI 配置 setting = db.query(AiSetting).filter(AiSetting.is_active == True).first() if not setting: - # 如果没有配置,尝试返回默认配置 setting = db.query(AiSetting).first() if not setting or not setting.api_key: @@ -60,22 +90,62 @@ def generate_joke( # 调用 AI try: client = OpenAI(base_url=setting.api_base, api_key=setting.api_key) - prompt = _build_prompt(req.scenarios, req.keywords) + prompt = _build_prompt(req.scenarios, req.keywords, req.style, req.length) response = client.chat.completions.create( model=setting.model_name, messages=[{"role": "user", "content": prompt}], - temperature=setting.temperature, + temperature=req.temperature, max_tokens=setting.max_tokens, ) raw = response.choices[0].message.content - return _parse_response(raw) + return _parse_json_response(raw) + except json.JSONDecodeError: + raise HTTPException(status_code=500, detail="AI 返回格式异常,请重新生成") except Exception as e: raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}") +def _parse_json_response(raw: str | None) -> GenerateResponse: + """解析 AI 返回的 JSON 格式""" + if not raw or not raw.strip(): + return GenerateResponse(title="生成的笑话", content="(内容生成失败,请重新生成)", score=0) + + # 尝试从 markdown 代码块中提取 JSON + raw = raw.strip() + if raw.startswith("```"): + lines = raw.split("\n") + # 去掉第一行 ```json 和最后一行 ``` + if len(lines) >= 3: + raw = "\n".join(lines[1:-1]).strip() + + # 移除可能的尾部分号 + if raw.endswith(","): + raw = raw[:-1] + + try: + data = json.loads(raw) + except json.JSONDecodeError: + # 如果 JSON 解析失败,尝试查找花括号内的内容 + try: + start = raw.index("{") + end = raw.rindex("}") + 1 + raw = raw[start:end] + data = json.loads(raw) + except (ValueError, json.JSONDecodeError): + return _parse_response(raw) + + return GenerateResponse( + title=data.get("title", "生成的笑话"), + content=data.get("content", ""), + score=data.get("score", 0), + reason=data.get("reason", ""), + created_at=datetime.now(), + ) + + def _parse_response(raw: str | None) -> GenerateResponse: """解析 AI 返回内容,提取标题和内容""" # 防御:处理空或 None 输入 diff --git a/api/app/schemas/joke.py b/api/app/schemas/joke.py index 40a1409..a8782d8 100644 --- a/api/app/schemas/joke.py +++ b/api/app/schemas/joke.py @@ -74,12 +74,18 @@ class GenerateRequest(BaseModel): """笑话生成请求""" keywords: list[str] = [] scenarios: list[str] = [] + style: str = "twist" + length: str = "medium" + temperature: float = 0.8 class Config: json_schema_extra = { "example": { "keywords": ["加班"], - "scenarios": ["职场"] + "scenarios": ["职场"], + "style": "twist", + "length": "medium", + "temperature": 0.8, } } @@ -88,6 +94,8 @@ class GenerateResponse(BaseModel): """笑话生成响应""" title: str content: str + score: int = 0 + reason: str = "" created_at: datetime | None = None class Config: diff --git a/web/src/api/generate.js b/web/src/api/generate.js index aff5cbf..a5dd68e 100644 --- a/web/src/api/generate.js +++ b/web/src/api/generate.js @@ -4,11 +4,18 @@ import request from './request' * 调用 AI 生成笑话 * @param {string[]} keywords - 关键词列表 * @param {string[]} scenarios - 场景列表 - * @returns {Promise<{title: string, content: string, created_at: string}>} + * @param {object} opts - 可选参数 + * @param {string} opts.style - 风格: cold/warm/twist/pun/sketch/irony + * @param {string} opts.length - 长度: short/medium/long + * @param {number} opts.temperature - 创造力 0-1 + * @returns {Promise<{title: string, content: string, score: number, reason: string}>} */ -export const generateJoke = (keywords = [], scenarios = []) => { +export const generateJoke = (keywords = [], scenarios = [], opts = {}) => { return request.post('/generate', { keywords, - scenarios + scenarios, + style: opts.style || 'twist', + length: opts.length || 'medium', + temperature: opts.temperature ?? 0.8 }) } \ No newline at end of file diff --git a/web/src/views/generate/index.vue b/web/src/views/generate/index.vue index a9e289d..499babf 100644 --- a/web/src/views/generate/index.vue +++ b/web/src/views/generate/index.vue @@ -2,100 +2,130 @@
{{ generatedJoke.content }}
+