从零搭建AI文学创作助手:基于API聚合站+Flask实现智能写诗、小说生成神器

想要一个能写诗、写小说、创作剧本的AI助手?本文手把手教你用Flask和聚合API搭建一个专属的文学创作智能体,支持多轮对话、流式输出、多模型切换,助你突破创作瓶颈。


1. 引言:让AI成为你的文学创作伙伴

文学创作是一件充满灵性的事情,但有时候灵感枯竭、词穷改稿也是常态。如果能有一位懂文学、知韵律、通古今的AI助手随时为你提供灵感、润色词句、续写故事,那该多好?

基于大语言模型的AI助手恰恰可以扮演这个角色。无论是古风七律、现代诗歌,还是短篇小说、剧本大纲,AI都能快速生成初稿,并支持你反复打磨。更重要的是,你可以打造一个完全属于自己的文学创作工具------不受平台限制,数据自己掌控,功能随心定制。

本文将带你从零开始,使用Flask构建后端服务,配合API聚合站(支持GPT-5.5、Gemini等模型),开发一个专为文学创作设计的智能体。它具有以下特点:

特性 说明
多轮对话 记住上下文,连续创作、修改
流式输出 逐字显示,体验流畅
多模型选择 免费Gemini 2.5 Flash Lite / 付费GPT-5.5 XHigh
参数可调 温度、Top P、Max Tokens等
纯Web界面 无需安装,浏览器即用
对话历史 自动保存,支持多会话管理
零联网搜索 专注于语言生成,纯文学创作

本文提供完整后端代码和前端代码,直接复制粘贴即可运行。


2. 技术选型与项目结构

2.1 核心技术栈

层级 技术 说明
后端框架 Flask 轻量级Web框架,支持SSE流式
模型API API聚合站 统一接口,支持GPT-5.5、Gemini等
前端 原生HTML/CSS/JS 无需构建,简单高效
Markdown渲染 marked.js 实时渲染创作内容
数据持久化 JSON文件 对话历史本地存储

2.2 项目文件结构

复制代码
literature_assistant/
├── app.py              # Flask后端主程序
├── templates/
│   └── index.html      # 前端界面
└── .env                # 环境变量(可选,存放API密钥)

3. 准备工作:注册与获取API密钥

本项目的核心是调用大模型API。我们使用API聚合站点击注册),注册后即可获得免费和付费模型额度。

模型 模型名 特点 适用场景
Gemini 2.5 Flash Lite gemini-2.5-flash-lite 免费,速度快 日常对话、快速草稿
GPT-5.5 XHigh gpt-5.5-xhigh 付费,强大推理 复杂文学创作、长篇故事

注册后,在控制台创建API令牌,复制以sk-开头的密钥。


4. 后端完整代码(app.py

后端基于Flask,实现了以下功能:

  • 对话管理(创建、保存、切换会话)
  • 流式调用大模型API
  • 参数配置(温度、max_tokens等)
  • 对话历史持久化
  • 无搜索模块,专注文学创作

完整代码(可直接复制为app.py

python 复制代码
"""
智能体助手 - 文学创作版
基于API聚合站,支持多模型、流式输出、多轮对话
删除联网搜索,专注文字创作
"""

import json
import uuid
import os
import tempfile
import atexit
from datetime import datetime
from flask import Flask, render_template, request, jsonify, Response, stream_with_context
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)

# ============================================================
# 全局配置
# ============================================================
API_BASE_URL = "https://api.aigc.bar/v1"
DEFAULT_MODEL = "gemini-2.5-flash-lite"
AVAILABLE_MODELS = {
    "gemini-2.5-flash-lite": {
        "name": "Gemini 2.5 Flash Lite",
        "group": "free",
        "label": "免费",
        "description": "适合日常创作,响应快",
        "max_tokens_limit": 65535,
        "supports_vision": False
    },
    "gpt-5.5-xhigh": {
        "name": "GPT-5.5 XHigh",
        "group": "paid",
        "label": "最强付费",
        "description": "复杂文学创作、长篇故事",
        "max_tokens_limit": 65535,
        "supports_vision": True
    }
}

# 对话存储(内存+JSON持久化)
conversation_store = {}
TEMP_DIR = os.path.join(tempfile.gettempdir(), "literature_assistant")
os.makedirs(TEMP_DIR, exist_ok=True)
CONVERSATIONS_FILE = os.path.join(TEMP_DIR, "conversations.json")

def save_conversations():
    try:
        with open(CONVERSATIONS_FILE, 'w', encoding='utf-8') as f:
            json.dump(conversation_store, f, ensure_ascii=False, indent=2)
    except Exception as e:
        print(f"[保存对话失败] {e}")

def load_conversations():
    global conversation_store
    try:
        if os.path.exists(CONVERSATIONS_FILE):
            with open(CONVERSATIONS_FILE, 'r', encoding='utf-8') as f:
                conversation_store = json.load(f)
            print(f"[加载对话] 共 {len(conversation_store)} 个会话")
    except Exception as e:
        print(f"[加载对话失败] {e}")
        conversation_store = {}

# 默认文学创作系统提示词
DEFAULT_SYSTEM_PROMPT = """你是一位博学多才、文笔优雅的文学创作大师。你精通各种文学体裁:诗歌(古体诗、现代诗、十四行诗)、小说(短篇、长篇、微小说)、散文、寓言、剧本等。你的语言富有诗意和感染力,词汇丰富,句式多变。你能够根据用户的需求,快速生成高质量的原创文学作品,也能够为用户修改润色文稿、提供创作灵感、续写故事。请保持文学性和艺术性,避免说教和机械回答。"""

def get_openai_client(api_key):
    return OpenAI(api_key=api_key, base_url=API_BASE_URL)

def validate_api_key_format(api_key):
    return api_key and isinstance(api_key, str) and len(api_key.strip()) >= 10

def build_messages(conv_id, user_msg, system_prompt=None):
    messages = []
    final_system = system_prompt if system_prompt else DEFAULT_SYSTEM_PROMPT
    messages.append({"role": "system", "content": final_system})
    if conv_id and conv_id in conversation_store:
        messages.extend(conversation_store[conv_id])
    messages.append({"role": "user", "content": user_msg})
    return messages

def save_message(conv_id, role, content):
    if conv_id not in conversation_store:
        conversation_store[conv_id] = []
    conversation_store[conv_id].append({
        "role": role,
        "content": content,
        "timestamp": datetime.now().isoformat()
    })
    save_conversations()

def friendly_error(error_msg, model=""):
    if "401" in error_msg:
        return "API密钥无效,请检查。获取密钥: https://api.aigc.bar/register?aff=UP4F"
    if "429" in error_msg:
        return "请求频率过高,请稍后再试。"
    if "timeout" in error_msg.lower():
        return "请求超时,请检查网络。"
    return f"错误: {error_msg}"

# ============================================================
# Flask 路由
# ============================================================
@app.route("/")
def index():
    return render_template("index.html")

@app.route("/api/chat", methods=["POST"])
def chat():
    data = request.get_json()
    if not data:
        return jsonify({"error": "请求体为空"}), 400

    api_key = data.get("api_key", "").strip()
    model = data.get("model", DEFAULT_MODEL)
    message = data.get("message", "").strip()
    conv_id = data.get("conversation_id", str(uuid.uuid4()))
    stream = data.get("stream", True)
    system_prompt = data.get("system_prompt", None)
    temperature = float(data.get("temperature", 0.9))   # 文学创作适当调高温度
    max_tokens = int(data.get("max_tokens", 4096))
    top_p = float(data.get("top_p", 1.0))
    frequency_penalty = float(data.get("frequency_penalty", 0.5))
    presence_penalty = float(data.get("presence_penalty", 0.5))

    if not validate_api_key_format(api_key):
        return jsonify({"error": "API密钥无效"}), 401
    if not message:
        return jsonify({"error": "消息不能为空"}), 400
    if model not in AVAILABLE_MODELS:
        return jsonify({"error": f"不支持的模型: {model}"}), 400

    save_message(conv_id, "user", message)
    messages = build_messages(conv_id, message, system_prompt)

    try:
        client = get_openai_client(api_key)
        api_params = {
            "model": model,
            "messages": messages,
            "temperature": max(0, min(2, temperature)),
            "max_tokens": max(1, min(65535, max_tokens)),
            "top_p": max(0, min(1, top_p)),
            "frequency_penalty": max(-2, min(2, frequency_penalty)),
            "presence_penalty": max(-2, min(2, presence_penalty)),
            "stream": stream
        }

        if stream:
            def generate():
                full = ""
                try:
                    response = client.chat.completions.create(**api_params)
                    for chunk in response:
                        if chunk.choices and chunk.choices[0].delta.content:
                            content = chunk.choices[0].delta.content
                            full += content
                            yield f"data: {json.dumps({'type': 'content', 'content': content})}\n\n"
                    save_message(conv_id, "assistant", full)
                    yield f"data: {json.dumps({'type': 'done', 'conversation_id': conv_id})}\n\n"
                except Exception as e:
                    yield f"data: {json.dumps({'type': 'error', 'error': friendly_error(str(e), model)})}\n\n"
            return Response(stream_with_context(generate()), mimetype="text/event-stream", headers={"Cache-Control": "no-cache"})
        else:
            api_params["stream"] = False
            response = client.chat.completions.create(**api_params)
            assistant_msg = response.choices[0].message.content
            save_message(conv_id, "assistant", assistant_msg)
            return jsonify({"type": "complete", "content": assistant_msg, "conversation_id": conv_id})
    except Exception as e:
        return jsonify({"error": friendly_error(str(e), model)}), 500

# ======================= 辅助接口 =======================
@app.route("/api/models", methods=["GET"])
def get_models():
    return jsonify({"models": AVAILABLE_MODELS, "default": DEFAULT_MODEL})

@app.route("/api/conversation/<conv_id>", methods=["GET"])
def get_conversation(conv_id):
    if conv_id in conversation_store:
        return jsonify({"conversation_id": conv_id, "messages": conversation_store[conv_id]})
    return jsonify({"error": "对话不存在"}), 404

@app.route("/api/conversation/<conv_id>", methods=["DELETE"])
def clear_conversation(conv_id):
    if conv_id in conversation_store:
        del conversation_store[conv_id]
        save_conversations()
    return jsonify({"message": "已清空"})

@app.route("/api/conversations", methods=["GET"])
def list_conversations():
    convs = []
    for cid, msgs in conversation_store.items():
        title = "新创作"
        for msg in msgs:
            if msg["role"] == "user":
                title = msg["content"][:50]
                break
        convs.append({"id": cid, "title": title, "message_count": len(msgs), "last_updated": msgs[-1]["timestamp"] if msgs else None})
    return jsonify({"conversations": convs})

@app.route("/api/validate-key", methods=["POST"])
def validate_key():
    data = request.get_json()
    api_key = data.get("api_key", "").strip()
    if not validate_api_key_format(api_key):
        return jsonify({"valid": False, "message": "密钥格式无效"})
    try:
        client = get_openai_client(api_key)
        client.chat.completions.create(
            model=DEFAULT_MODEL,
            messages=[{"role": "user", "content": "Hi"}],
            max_tokens=5
        )
        return jsonify({"valid": True, "message": "密钥有效"})
    except Exception as e:
        if "401" in str(e):
            return jsonify({"valid": False, "message": "密钥无效"})
        return jsonify({"valid": True, "message": "密钥有效但验证出错(可能限流)"})

# 加载已有对话
load_conversations()
atexit.register(save_conversations)

if __name__ == "__main__":
    print("=" * 60)
    print("  文学创作智能体 - 基于API聚合站")
    print("  访问地址:http://localhost:5001")
    print("  支持模型:Gemini 2.5 Flash Lite (免费) / GPT-5.5 XHigh (付费)")
    print("=" * 60)
    app.run(host="0.0.0.0", port=5001, debug=True)

5. 前端完整代码(templates/index.html)

前端是一个现代化的对话界面,支持:

  • 多会话侧边栏
  • 流式Markdown渲染
  • 模型选择与参数调节
  • 复制助手回复
  • 无联网搜索控件

完整代码(保存为templates/index.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文学创作智能体 - AI作家助手</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background: #faf8f5;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }
        /* 顶部工具栏 */
        .toolbar {
            background: white;
            border-bottom: 1px solid #e2e0dd;
            padding: 12px 20px;
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            align-items: center;
            box-shadow: 0 1px 4px rgba(0,0,0,0.04);
        }
        .api-key-section {
            display: flex;
            gap: 8px;
            align-items: center;
            flex: 2;
            min-width: 280px;
        }
        .api-key-section input {
            flex: 1;
            padding: 8px 12px;
            border: 1px solid #ccc;
            border-radius: 6px;
            font-size: 14px;
        }
        .api-key-section button {
            padding: 8px 16px;
            background: #8b5a2b;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
        }
        .api-key-section button:hover { background: #6f451f; }
        .get-key-btn {
            padding: 8px 16px;
            background: #2c7da0;
            color: white;
            text-decoration: none;
            border-radius: 6px;
            font-size: 14px;
        }
        .get-key-btn:hover { background: #1f5e7a; }
        .model-select {
            padding: 8px 12px;
            border-radius: 6px;
            border: 1px solid #ccc;
            font-size: 14px;
        }
        .param-toggle {
            cursor: pointer;
            color: #666;
            font-size: 14px;
            padding: 6px 12px;
            background: #f0ede8;
            border-radius: 20px;
        }
        .param-panel {
            background: #fefcf9;
            padding: 12px 20px;
            border-bottom: 1px solid #e2e0dd;
            display: none;
            flex-wrap: wrap;
            gap: 15px;
            font-size: 14px;
        }
        .param-panel.show { display: flex; }
        .param-item {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .param-item input {
            width: 80px;
            padding: 4px 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        /* 聊天区 */
        .chat-container {
            flex: 1;
            display: flex;
            overflow: hidden;
        }
        .sidebar {
            width: 260px;
            background: #fffefc;
            border-right: 1px solid #e2e0dd;
            display: flex;
            flex-direction: column;
            overflow-y: auto;
        }
        .sidebar-header {
            padding: 16px;
            border-bottom: 1px solid #eee;
            font-weight: bold;
        }
        .new-chat-btn {
            margin: 12px;
            padding: 10px;
            background: #8b5a2b;
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
        }
        .conversation-list { flex: 1; overflow-y: auto; }
        .conv-item {
            padding: 12px 16px;
            border-bottom: 1px solid #eee;
            cursor: pointer;
            font-size: 14px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .conv-item.active {
            background: #f1e9dd;
            border-left: 3px solid #8b5a2b;
        }
        .conv-item:hover { background: #f7f2ea; }
        .main {
            flex: 1;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
        .messages {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .message {
            display: flex;
            gap: 12px;
            max-width: 85%;
            animation: fadeIn 0.2s ease;
        }
        .message.user {
            align-self: flex-end;
            flex-direction: row-reverse;
        }
        .message.assistant {
            align-self: flex-start;
        }
        .avatar {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            background: #8b5a2b;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
            flex-shrink: 0;
        }
        .message.user .avatar {
            background: #9e7b5c;
        }
        .bubble {
            background: white;
            padding: 12px 18px;
            border-radius: 20px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.08);
            line-height: 1.6;
            word-wrap: break-word;
            white-space: pre-wrap;
        }
        .message.user .bubble {
            background: #8b5a2b;
            color: white;
        }
        .input-area {
            background: white;
            border-top: 1px solid #e2e0dd;
            padding: 16px 20px;
            display: flex;
            gap: 12px;
            align-items: flex-end;
        }
        .input-area textarea {
            flex: 1;
            padding: 12px;
            border: 1px solid #ccc;
            border-radius: 24px;
            resize: none;
            font-family: inherit;
            font-size: 14px;
            max-height: 150px;
        }
        .input-area button {
            padding: 10px 28px;
            background: #8b5a2b;
            color: white;
            border: none;
            border-radius: 28px;
            cursor: pointer;
            font-size: 14px;
        }
        .input-area button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        .status {
            font-size: 12px;
            color: #8c7a66;
            padding: 6px 20px;
            background: #fefcf9;
            border-top: 1px solid #eee;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(6px); }
            to { opacity: 1; transform: translateY(0); }
        }
        /* Markdown样式 */
        .md-content pre {
            background: #f4f2ee;
            padding: 12px;
            border-radius: 8px;
            overflow-x: auto;
            margin: 8px 0;
        }
        .md-content code {
            background: #f0ede8;
            padding: 2px 6px;
            border-radius: 4px;
            font-family: monospace;
        }
        .md-content p { margin: 6px 0; }
        .md-content ul, .md-content ol { margin: 6px 0; padding-left: 20px; }
        .md-content blockquote {
            border-left: 4px solid #8b5a2b;
            margin: 8px 0;
            padding-left: 14px;
            color: #5c4b3a;
            font-style: italic;
        }
        .copy-btn {
            position: absolute;
            top: 6px;
            right: 8px;
            padding: 4px 10px;
            background: rgba(0,0,0,0.05);
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 11px;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .message.assistant .bubble { position: relative; }
        .message.assistant .bubble:hover .copy-btn { opacity: 1; }
        @media (max-width: 768px) {
            .sidebar { display: none; }
            .message { max-width: 95%; }
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<div class="toolbar">
    <div class="api-key-section">
        <input type="password" id="apiKey" placeholder="API密钥 (必填)">
        <button id="validateKeyBtn">验证密钥</button>
        <a href="https://api.aigc.bar/register?aff=UP4F" target="_blank" class="get-key-btn">获取密钥</a>
    </div>
    <select id="modelSelect" class="model-select">
        <option value="gemini-2.5-flash-lite">Gemini 2.5 Flash Lite (免费·适合日常)</option>
        <option value="gpt-5.5-xhigh">GPT-5.5 XHigh (付费·深度创作)</option>
    </select>
    <div class="param-toggle" id="paramToggle">⚙️ 创作参数</div>
</div>
<div id="paramPanel" class="param-panel">
    <div class="param-item"><label>系统提示:</label><input type="text" id="systemPrompt" placeholder="自定义创作风格" style="width:200px"></div>
    <div class="param-item"><label>温度:</label><input type="number" id="temperature" value="0.9" step="0.1" min="0" max="2"></div>
    <div class="param-item"><label>Top P:</label><input type="number" id="topP" value="1" step="0.1" min="0" max="1"></div>
    <div class="param-item"><label>输出长度:</label><input type="number" id="maxTokens" value="4096" step="512" min="1" max="65535"></div>
</div>

<div class="chat-container">
    <div class="sidebar">
        <div class="sidebar-header">📖 我的作品集</div>
        <button class="new-chat-btn" id="newChatBtn">✨ 新创作</button>
        <div id="conversationList" class="conversation-list"></div>
    </div>
    <div class="main">
        <div id="messages" class="messages"></div>
        <div class="input-area">
            <textarea id="messageInput" rows="2" placeholder="写一首关于秋天的诗...&#10;或:请续写这个故事...&#10;或:帮我润色这段文字..."></textarea>
            <button id="sendBtn">📝 创作</button>
        </div>
        <div id="status" class="status">✨ 准备好书写灵感了</div>
    </div>
</div>

<script>
    let currentConversationId = null;
    let isWaiting = false;
    let currentMessages = [];

    // DOM元素
    const apiKeyInput = document.getElementById('apiKey');
    const validateKeyBtn = document.getElementById('validateKeyBtn');
    const modelSelect = document.getElementById('modelSelect');
    const paramToggle = document.getElementById('paramToggle');
    const paramPanel = document.getElementById('paramPanel');
    const systemPromptInput = document.getElementById('systemPrompt');
    const temperatureInput = document.getElementById('temperature');
    const topPInput = document.getElementById('topP');
    const maxTokensInput = document.getElementById('maxTokens');
    const newChatBtn = document.getElementById('newChatBtn');
    const conversationListDiv = document.getElementById('conversationList');
    const messagesDiv = document.getElementById('messages');
    const messageInput = document.getElementById('messageInput');
    const sendBtn = document.getElementById('sendBtn');
    const statusDiv = document.getElementById('status');

    function setStatus(text, isError = false) {
        statusDiv.textContent = text;
        if (isError) setTimeout(() => { if (statusDiv.textContent === text) statusDiv.textContent = '✨ 准备好书写灵感了'; }, 3000);
    }

    function getApiKey() { return apiKeyInput.value.trim(); }
    function saveApiKey() { const key = apiKeyInput.value.trim(); if (key) localStorage.setItem('litApiKey', key); }
    function loadApiKey() { const key = localStorage.getItem('litApiKey'); if (key) apiKeyInput.value = key; }
    function saveConversationId(id) { if (id) localStorage.setItem('lastLitConvId', id); }
    function loadLastConversationId() { return localStorage.getItem('lastLitConvId'); }

    async function loadConversations() {
        try {
            const resp = await fetch('/api/conversations');
            const data = await resp.json();
            if (data.conversations) renderConversationList(data.conversations);
        } catch(e) { console.error(e); }
    }

    function renderConversationList(convs) {
        conversationListDiv.innerHTML = '';
        for (const conv of convs) {
            const div = document.createElement('div');
            div.className = 'conv-item';
            if (conv.id === currentConversationId) div.classList.add('active');
            div.textContent = conv.title || '新创作';
            div.addEventListener('click', () => switchConversation(conv.id));
            conversationListDiv.appendChild(div);
        }
    }

    async function switchConversation(convId) {
        if (convId === currentConversationId) return;
        currentConversationId = convId;
        saveConversationId(convId);
        await loadConversationHistory(convId);
        renderMessages();
        highlightActive();
    }

    async function loadConversationHistory(convId) {
        try {
            const resp = await fetch(`/api/conversation/${convId}`);
            if (resp.ok) {
                const data = await resp.json();
                currentMessages = data.messages || [];
            } else currentMessages = [];
        } catch(e) { currentMessages = []; }
    }

    function highlightActive() {
        loadConversations().then(() => {
            document.querySelectorAll('.conv-item').forEach(el => {
                el.classList.remove('active');
                if (el.textContent.includes(currentConversationId)) el.classList.add('active');
            });
        });
    }

    async function newConversation() {
        currentConversationId = null;
        currentMessages = [];
        renderMessages();
        saveConversationId(null);
        await loadConversations();
    }

    function renderMessages() {
        messagesDiv.innerHTML = '';
        for (const msg of currentMessages) addMessageToUI(msg.role, msg.content, false);
        messagesDiv.scrollTop = messagesDiv.scrollHeight;
    }

    function addMessageToUI(role, content, scroll = true) {
        const msgDiv = document.createElement('div');
        msgDiv.className = `message ${role}`;
        const avatar = document.createElement('div');
        avatar.className = 'avatar';
        avatar.textContent = role === 'user' ? '我' : 'AI';
        const bubble = document.createElement('div');
        bubble.className = 'bubble';
        if (role === 'assistant') {
            const contentDiv = document.createElement('div');
            contentDiv.className = 'md-content';
            bubble.appendChild(contentDiv);
            const copyBtn = document.createElement('button');
            copyBtn.className = 'copy-btn';
            copyBtn.textContent = '复制';
            copyBtn.onclick = () => {
                navigator.clipboard.writeText(bubble.dataset.rawContent || '').then(() => {
                    copyBtn.textContent = '已复制';
                    setTimeout(() => copyBtn.textContent = '复制', 2000);
                });
            };
            bubble.appendChild(copyBtn);
            bubble.dataset.rawContent = content || '';
            contentDiv.innerHTML = marked.parse(content || '');
        } else {
            bubble.textContent = content;
        }
        msgDiv.appendChild(avatar);
        msgDiv.appendChild(bubble);
        messagesDiv.appendChild(msgDiv);
        if (scroll) messagesDiv.scrollTop = messagesDiv.scrollHeight;
        return bubble;
    }

    async function sendMessage() {
        if (isWaiting) return;
        const message = messageInput.value.trim();
        if (!message) return;
        const apiKey = getApiKey();
        if (!apiKey) { setStatus('请先填写API密钥', true); return; }

        if (!currentConversationId) {
            currentConversationId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
            saveConversationId(currentConversationId);
            await loadConversations();
        }

        addMessageToUI('user', message);
        currentMessages.push({ role: 'user', content: message, timestamp: new Date().toISOString() });
        messageInput.value = '';
        isWaiting = true;
        sendBtn.disabled = true;
        setStatus('✍️ 灵感迸发中...');

        const tempBubble = addMessageToUI('assistant', '');
        let fullContent = '';

        const payload = {
            api_key: apiKey,
            model: modelSelect.value,
            message: message,
            conversation_id: currentConversationId,
            stream: true,
            system_prompt: systemPromptInput.value || null,
            temperature: parseFloat(temperatureInput.value),
            max_tokens: parseInt(maxTokensInput.value),
            top_p: parseFloat(topPInput.value),
        };

        try {
            const response = await fetch('/api/chat', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
            if (!response.ok) throw new Error((await response.json()).error || '请求失败');

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split('\n');
                buffer = lines.pop();
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        const data = line.slice(6);
                        try {
                            const parsed = JSON.parse(data);
                            if (parsed.type === 'content') {
                                fullContent += parsed.content;
                                tempBubble.dataset.rawContent = fullContent;
                                const mdDiv = tempBubble.querySelector('.md-content');
                                if (mdDiv) mdDiv.innerHTML = marked.parse(fullContent);
                                messagesDiv.scrollTop = messagesDiv.scrollHeight;
                            } else if (parsed.type === 'error') throw new Error(parsed.error);
                            else if (parsed.type === 'done') break;
                        } catch(e) { console.error(e); }
                    }
                }
            }
            currentMessages.push({ role: 'assistant', content: fullContent, timestamp: new Date().toISOString() });
            setStatus('✨ 创作完成');
        } catch(err) {
            const mdDiv = tempBubble.querySelector('.md-content');
            if (mdDiv) mdDiv.innerHTML = `❌ 哎呀,灵感中断了:${err.message}`;
            else tempBubble.innerHTML = `❌ 错误:${err.message}`;
            setStatus(err.message, true);
        } finally {
            isWaiting = false;
            sendBtn.disabled = false;
            messageInput.focus();
            await loadConversations();
        }
    }

    async function validateApiKey() {
        const key = getApiKey();
        if (!key) { setStatus('请输入API密钥', true); return; }
        setStatus('验证中...');
        try {
            const resp = await fetch('/api/validate-key', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ api_key: key })
            });
            const data = await resp.json();
            if (data.valid) setStatus('✅ 密钥有效');
            else setStatus(`❌ ${data.message}`, true);
        } catch(e) { setStatus('验证失败', true); }
    }

    async function init() {
        paramToggle.addEventListener('click', () => paramPanel.classList.toggle('show'));
        validateKeyBtn.addEventListener('click', validateApiKey);
        sendBtn.addEventListener('click', sendMessage);
        newChatBtn.addEventListener('click', newConversation);
        messageInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
        });
        apiKeyInput.addEventListener('change', saveApiKey);
        loadApiKey();
        const lastId = loadLastConversationId();
        if (lastId) {
            currentConversationId = lastId;
            await loadConversationHistory(currentConversationId);
            renderMessages();
        } else {
            currentConversationId = null;
            currentMessages = [];
            renderMessages();
        }
        await loadConversations();
        setStatus('✨ 已准备好与你共创文学佳作');
    }
    init();
</script>
</body>
</html>

6. 使用说明

6.1 安装依赖

bash 复制代码
pip install flask openai python-dotenv

6.2 运行项目

  1. 将上述后端代码保存为 app.py
  2. 创建 templates 文件夹,将前端代码保存为 index.html
  3. 在终端执行 python app.py
  4. 浏览器访问 http://localhost:5001

6.3 使用流程

步骤 操作 说明
1 填入API密钥 API聚合站获取
2 选择模型 日常用免费Gemini,复杂创作用GPT-5.5
3 输入创作指令 例如:"写一首五言绝句,主题为春夜"
4 查看实时生成 流式输出,可调整温度等参数
5 多轮对话 AI会记住上下文,可继续修改完善

7. 文学创作实战示例

7.1 生成一首七言律诗

用户:写一首七言律诗,描绘春日山居,要求对仗工整,意境幽远。

AI(Gemini 2.5 Flash Lite)输出:

复制代码
《春日山居》
空山新雨润苔痕,半亩方塘抱石根。
几树夭桃燃野径,一溪碧水绕柴门。
闲听黄鸟啼深樾,静看青烟起远村。
莫问人间尘俗事,松风吹尽日昏昏。

7.2 续写微小说

用户:深夜,一个古董店老板发现柜子里的青花瓷瓶自动移位了,请续写300字悬疑开头。

AI(GPT-5.5 XHigh)输出:

老周点了支烟,视线却死死钉在那只康熙青花缠枝莲纹瓶上------他记得清楚,傍晚打烊前,自己亲手将它摆在了博古架的第二层,靠左。可此刻,它却稳稳当当地站在了第三层正中央,旁边的珐琅彩碗被挤到了角落......

他壮着胆子伸手摸了一下瓶身,冰凉、光滑,没有一丝温度。正当他松了口气准备收回手时,瓶内突然传来一声极轻极细的叹息,像是隔了一个世纪那么远。

"谁?"老周的声音在空荡荡的店里显得格外尖锐。无人应答,但博古架上第三层的青花瓷瓶,又悄无声息地向左移动了一寸......


8. 总结与拓展

通过本文,我们构建了一个完全属于你自己的文学创作智能体。它没有联网搜索干扰,专注于文字之美,支持多轮对话、参数调节、多模型切换,且所有对话历史自动保存。

如果你想进一步扩展,可以尝试:

  • 添加记忆压缩:当对话超长时自动总结历史,释放上下文。
  • 集成私有素材库:让AI基于你上传的文档进行创作。
  • 优化提示词:为不同文体预设系统提示(如"古诗词专家"、"科幻小说家")。

现在,就去注册API聚合站获取密钥,开始你的AI创作之旅吧!

灵感枯竭的时候,让AI陪你一起点燃文字的火花。下一篇爆款小说,也许就诞生于此。


相关推荐
andafaAPS1 小时前
安达发|医疗器械行业APS排程软件:重构生产效能的生命线
大数据·人工智能·制造·aps排程软件·安达发aps·计划排产软件
2301_766283441 小时前
Golang怎么实现防重复提交_Golang如何用Token机制防止表单重复提交【技巧】
jvm·数据库·python
qq_414256571 小时前
CSS如何实现元素在容器内居中_利用margin-auto技巧
jvm·数据库·python
2401_824222691 小时前
如何用 Transferable 对象零拷贝转移超大数组内存给子线程
jvm·数据库·python
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年4月28日
大数据·人工智能·python·信息可视化·自然语言处理
武科大许志伟1 小时前
课题组学习南京大学陈贵海教授“自演进异构融合的边缘智能计算”的专题学术报告
人工智能
SilentSamsara1 小时前
Python 并发基础:threading/GIL 与 multiprocessing 的选型逻辑
服务器·开发语言·数据库·vscode·python·pycharm
m0_495496411 小时前
如何禁用 Vite 中的热更新(HMR)以避免 React 应用加载中断
jvm·数据库·python
m0_741173331 小时前
MySQL中如何使用CAST实现类型转换_MySQL数据类型转换技巧
jvm·数据库·python