From 6d015bb56d93bd6dfa43210a4d39444e5ffa9ca9 Mon Sep 17 00:00:00 2001 From: bwstudio Date: Sat, 13 Jun 2026 16:04:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AI=20=E7=94=9F=E6=88=90=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20-=20=E9=A3=8E=E6=A0=BC/=E9=95=BF=E5=BA=A6/=E8=AF=84?= =?UTF-8?q?=E5=88=86/=E5=8E=86=E5=8F=B2/=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 prompt 支持风格选择(6种)和字数范围 - 后端 AI 输出改为 JSON 格式,含评分和推荐理由 - GenerateRequest/Response schema 新增 style/length/score/reason 字段 - 前端新增风格选择、字数范围控件 - 前端新增随机选题按钮 - 前端新增复制内容功能 - 前端新增生成历史记录(会话级 20 条) - 结果卡片展示 AI 评分和推荐理由 --- api/app/routers/generate.py | 100 +++++- api/app/schemas/joke.py | 10 +- web/src/api/generate.js | 13 +- web/src/views/generate/index.vue | 553 ++++++++++++++++++++++--------- 4 files changed, 504 insertions(+), 172 deletions(-) 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 @@

- 智能笑话生成器 + AI 笑话生成

-
- + - {{ s }} - + class="chip" + :class="{ checked: selectedScenarios.includes(s) }" + @click="toggleScenario(s)" + >{{ s }}
- - - -
- +
+ + +
+
+ {{ kw }} - + × + +
+
+ + +
+ +
+ {{ s.label }} +
+
+ + +
+ +
+ {{ l.label }}
- + + +
-

{{ generatedJoke.title }}

+
+

{{ generatedJoke.title }}

+ {{ generatedJoke.score }}/10 +
{{ generatedJoke.content }}
+
+ 💡 {{ generatedJoke.reason }} +
- - 🔄 重新生成 - - + + + +
+
+ + +
+

📄 生成历史 ({{ history.length }})

+
+
+
{{ item.title }}
+
+ {{ item.content.substring(0, 40) }}... + × +
+

📬 我的收藏 ({{ favorites.length }})

-
-
+
+
{{ fav.title }}
{{ fav.content.substring(0, 100) }}...
{{ formatDate(fav.created_at) }} - - 删除 - +
@@ -104,31 +134,48 @@