基于cloudflare Workers 的AI绘画平台

基于cloudflare Workers 的AI绘画平台

Administrator 0 2025-08-18

效果图

image-Kvkm.png

简介

Cloudflare Workers AI是由 Cloudflare推出的由全球网络上运行由无服务器 GPU 提供支持的机器学习模型。

参考链接

狐狸汉克

开始

打开cloudflare点击

image-QNas.png

点击创建后选择,从 Hello World! 开始,点击开始使用

image-ICDD.png

Worker 名称,不用管,选择部署,选择完部署后点击编辑代码

image-mkHm.png

进入该页面,将原本的worker.js代码全部删除,替换为以下代码

worker.js

import HTML from './index.html';

export default {
  async fetch(request, env) {
    const originalHost = request.headers.get("host");

    // 设置CORS头部
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*', // 允许任何源
      'Access-Control-Allow-Methods': 'GET, POST', // 允许的请求方法
      'Access-Control-Allow-Headers': 'Content-Type' // 允许的请求头
    };

    // 如果这是一个预检请求,则直接返回CORS头部
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: corsHeaders
      });
    }

    // 检查请求方法
    if (request.method === 'POST') {
      // 处理 POST 请求,用于 AI 绘画功能
      const data = await request.json();
      const upload = data.upload ?? false;  // 由后端直接上传到图床,解决奇奇怪怪的网络问题

      let model = '@cf/black-forest-labs/flux-1-schnell'; // 默认模型

      // 检查 prompt 是否存在
      if (!('prompt' in data) || data.prompt.trim() === '') {
        return new Response('Missing prompt', { status: 400, headers: corsHeaders });
      }

      // 检查 model 是否存在,如果没有则使用默认模型
      if ('model' in data) {
        switch(data.model) {
          case 'dreamshaper-8-lcm':
            model = '@cf/lykon/dreamshaper-8-lcm';
            break;
          case 'stable-diffusion-xl-base-1.0':
            model = '@cf/stabilityai/stable-diffusion-xl-base-1.0';
            break;
          case 'stable-diffusion-xl-lightning':
            model = '@cf/bytedance/stable-diffusion-xl-lightning';
            break;
          case 'flux-1-schnell':
            model = '@cf/black-forest-labs/flux-1-schnell';
            break;
          default:
            break;
        }
      }

      let inputs = {
        prompt: data.prompt.trim()
      };

      // 如果模型不是 flux-1-schnell, 则添加 width 和 height
      if (model !== '@cf/black-forest-labs/flux-1-schnell') {
        inputs.width = data.resolution?.width ?? 1024;
        inputs.height = data.resolution?.height ?? 1024;
      } else {
        // 反之添加 num_steps
        inputs.num_steps = 8; // 默认值
      }

      const response = await env.AI.run(model, inputs);


      if (model === '@cf/black-forest-labs/flux-1-schnell') {
        // flux-1-schnell模型返回的是base64编码,其他模型返回的都是二进制的png文件
        // 如果模型是 flux-1-schnell,处理 base64 图片数据
        if (response.image) {
          const base64Image = response.image;
          const blob = await base64ToBlob(base64Image);

          if (upload) {
            const uploadResponse = await uploadToImageHost(blob);
            if (uploadResponse?.src) {
              // 返回图床的图片地址
              return new Response(JSON.stringify({
                imageUrl: `https://pic.foxhank.top${uploadResponse.src}`
              }), {
                status: 200,
                headers: {
                  ...corsHeaders,
                  'content-type': 'application/json'
                }
              });
            } else {
              return new Response(JSON.stringify({
                errors: ['Image upload failed'],
                messages: []
              }), {
                status: 500,
                headers: {
                  ...corsHeaders,
                  'content-type': 'application/json'
                }
              });
            }
          } else {
            // 不上传,直接返回原始图片
            return new Response(blob, {
              headers: {
                ...corsHeaders,
                'content-type': 'image/png'
              }
            });
          }
        } else {
          return new Response(JSON.stringify({
            errors: ['No image found in the response'],
            messages: []
          }), {
            status: 500,
            headers: {
              ...corsHeaders,
              'content-type': 'application/json'
            }
          });
        }
      } else {
          //处理其他模型
        if (upload) {
            //把返回的stream转化为blob
          async function convertReadableStreamToBlob(readableStream) {
            try {
              // 将 ReadableStream 转换为 Blob
              const response = new Response(readableStream);
              const blob = await response.blob();
              return blob;
            } catch (error) {
              console.error('转换 ReadableStream 出错:', error);
              return null;
            }
          }
          // 转换 ReadableStream 为 Blob
          const blob = await convertReadableStreamToBlob(response);

          const uploadResponse = await uploadToImageHost(blob);
          if (uploadResponse?.src) {
            return new Response(JSON.stringify({
              imageUrl: `https://pic.foxhank.top${uploadResponse.src}`
            }), {
              status: 200,
              headers: {
                ...corsHeaders,
                'content-type': 'application/json'
              }
            });
          } else {
            return new Response(JSON.stringify({
              errors: ['Image upload failed'],
              messages: [uploadResponse.text]
            }), {
              status: 500,
              headers: {
                ...corsHeaders,
                'content-type': 'application/json'
              }
            });
          }
        } else {
          // 如果不上传,直接返回原始图片
          return new Response(response, {
            headers: {
              ...corsHeaders,
              'content-type': 'image/png;base64',
            },
          });
        }
      }
    } else {
      // 处理 GET 请求,返回 index.html
      return new Response(HTML.replace(/{{host}}/g, originalHost), {
        status: 200,
        headers: {
          ...corsHeaders, // 合并CORS头部
          "content-type": "text/html"
        }
      });
    }
  }
};

// 将 Base64 图片数据转换为 Blob
async function base64ToBlob(base64) {
    const base64Data = base64.replace(/^data:image\/\w+;base64,/, '');
    const byteString = atob(base64Data);
    const arrayBuffer = new ArrayBuffer(byteString.length);
    const uint8Array = new Uint8Array(arrayBuffer);
    for (let i = 0; i < byteString.length; i++) {
        uint8Array[i] = byteString.charCodeAt(i);
    }
    return new Blob([uint8Array], { type: 'image/png' });
}

// 上传图片到图床
async function uploadToImageHost(blob) {
  const formData = new FormData();
  formData.append('file', blob, 'image.png');

  try {
    const response = await fetch('https://pic.foxhank.top/upload', {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) {
      const errorMessage = await response.text(); // 等待并获取错误信息
      throw new Error(`HTTP error! status: ${response.status}, message: ${errorMessage}`);
    }

    const jsonResponse = await response.json();
    return jsonResponse[0]; // 图床返回地址里面是 src 字段
  } catch (error) {
    console.error(error); // 记录错误信息
    return null;
  }
}


按住ctrl+shift+e

image-UFfO.png

在该目录下添加index.html

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>AI绘画</title>
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
    <meta content="使用先进的AI技术,轻松创作独特的数字艺术作品。探索无限可能,释放你的创意潜能。" name="description">
    <meta content="yes" name="apple-mobile-web-app-capable">
    <meta content="black" name="apple-mobile-web-app-status-bar-style">
    <meta content="AI绘画" name="apple-mobile-web-app-title">
    <!--页面logo-->
    <!--    <link rel="icon" href="https://xx.com" type="image/png">-->
    <!--    <link rel="apple-touch-icon" href="https://xx.com">-->

    <link rel="stylesheet" href="new_styles.css">
    <style>
        html, body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow-x: hidden;
            font-family: 'Arial', sans-serif;
            background-color: #f4f4f4;
            color: #333;
            transition: background-color 0.3s, color 0.3s;
            position: relative;
            box-sizing: border-box;
        }

        .dark-theme {
            --background-color: #1e272e;
            --text-color: #ecf0f1;
            background: linear-gradient(to bottom, #1e272e, #2c3e50);
            color: #ecf0f1;
        }

        .container {
            display: flex;
            justify-content: center;
            align-items: flex-start;
            min-height: 100vh;
            padding: 10px;
            box-sizing: border-box;
            width: 100%;
        }

        .card {
            background-color: #fff;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            padding: 1.5rem;
            width: 100%;
            max-width: 700px;
            text-align: center;
            transition: background-color 0.3s, box-shadow 0.3s;
            position: relative;
            box-sizing: border-box;
        }

        h1 {
            color: #3498db;
            margin-bottom: 1rem;
            transition: color 0.3s;
        }

        select,
        input[type="text"],
        input[type="number"] {
            padding: 0.75rem;
            margin-bottom: 1rem;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 1rem;
            width: 100%;
            transition: all 0.3s ease;
            box-sizing: border-box;
        }

        select:focus,
        input[type="text"]:focus,
        input[type="number"]:focus {
            outline: none;
            border-color: #3498db;
            box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
        }

        .image-container {
            height: 300px;
            margin-bottom: 1rem;
            border-radius: 5px;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            border: 2px dashed #ddd;
            transition: border-color 0.3s;
        }

        .image-container img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
            transition: opacity 0.5s;
        }

        .loading-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(255, 255, 255, 0.8);
            display: none;
            justify-content: center;
            align-items: center;
            z-index: 10;
        }

        .loading-spinner {
            border: 5px solid #f3f3f3;
            border-top: 5px solid #3498db;
            border-radius: 50%;
            width: 50px;
            height: 50px;
            animation: spin 2s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .button-group {
            display: flex;
            justify-content: space-around;
            gap: 10px;
            flex-wrap: wrap;
        }

        .submit-btn,
        .download-btn,
        .history-btn {
            background-color: #3498db;
            color: white;
            border: none;
            cursor: pointer;
            transition: background-color 0.3s;
            padding: 0.75rem;
            border-radius: 5px;
            flex: 1;
            min-width: 100px;
        }

        .submit-btn:hover,
        .download-btn:hover,
        .history-btn:hover {
            background-color: #2980b9;
        }

        .submit-btn.active {
            background-color: #2ecc71;
        }

        .submit-btn.active:hover {
            background-color: #27ae60;
        }

        .footer {
            margin-top: 2rem;
            font-size: 0.9rem;
            color: #777;
            transition: color 0.3s;
            background-color: var(--background-color);
            color: var(--text-color);
            padding: 1rem;
            border-radius: 5px;
        }

        .footer a {
            color: #3498db;
            text-decoration: none;
        }

        .modal {
            display: none;
            position: fixed;
            z-index: 1;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0, 0, 0, 0.4);
        }

        .modal-content {
            background-color: #fefefe;
            margin: 10% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 90%;
            max-width: 600px;
            border-radius: 10px;
            box-sizing: border-box;
        }

        .close {
            color: #aaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
        }

        .close:hover,
        .close:focus {
            color: black;
            text-decoration: none;
            cursor: pointer;
        }

        .history-item {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
            padding: 10px;
            background-color: #f9f9f9;
            border-radius: 5px;
            flex-wrap: wrap;
        }

        .history-item img {
            width: 50px;
            height: 50px;
            object-fit: cover;
            margin-right: 10px;
            border-radius: 5px;
        }

        .history-item-buttons {
            margin-left: auto;
        }

        .history-item-buttons button {
            margin-left: 5px;
            padding: 5px 10px;
            font-size: 0.8rem;
        }

        .redraw-btn {
            background-color: #3498db;
            color: white;
        }

        .delete-btn {
            background-color: #e74c3c;
            color: white;
        }

        .clear-history-btn {
            background-color: #e74c3c;
            color: white;
            margin-top: 10px;
        }

        .theme-toggle {
            position: absolute;
            top: 10px;
            right: 10px;
            background: none;
            border: none;
            font-size: 1.5rem;
            cursor: pointer;
            transition: transform 0.3s ease;
        }

        .theme-toggle:hover {
            transform: scale(1.2);
        }

        .tooltip {
            position: relative;
            display: inline-block;
        }

        .tooltip .tooltiptext {
            visibility: hidden;
            width: 120px;
            background-color: #555;
            color: #fff;
            text-align: center;
            border-radius: 6px;
            padding: 5px;
            position: absolute;
            z-index: 1;
            bottom: 125%;
            left: 50%;
            margin-left: -60px;
            opacity: 0;
            transition: opacity 0.3s;
        }

        .tooltip:hover .tooltiptext {
            visibility: visible;
            opacity: 1;
        }

        @media (max-width: 768px) {
            .card {
                padding: 1rem;
            }

            h1 {
                font-size: 1.8rem;
            }

            select, input[type="text"], input[type="number"], button {
                font-size: 0.9rem;
            }

            .image-container {
                height: 250px;
            }

            .modal-content {
                width: 95%;
                margin: 5% auto;
            }
        }

        @media (max-width: 480px) {
            .card {
                border-radius: 0;
            }

            h1 {
                font-size: 1.5rem;
            }

            .button-group {
                flex-direction: column;
            }

            .submit-btn, .download-btn, .history-btn {
                margin: 5px 0;
                width: 100%;
            }

            .size-options {
                flex-direction: column;
                align-items: flex-start;
            }

            .size-options select {
                width: 100%;
                margin-bottom: 0.5rem;
            }

            .custom-size-inputs {
                flex-direction: row;
                flex-wrap: wrap;
                width: 100%;
            }

            .custom-size-inputs input[type="number"] {
                width: 45%;
                margin-right: 0.5rem;
            }
        }

        .input-group {
            display: flex;
            align-items: center;
            width: 100%;
            margin-bottom: 1rem;
            flex-wrap: wrap;
        }

        .input-group input[type="text"] {
            width: 70%;
            padding: 0.75rem;
            margin-right: 0.5rem;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 1rem;
            transition: all 0.3s ease;
            box-sizing: border-box;
        }

        .random-btn {
            padding: 0.75rem 1rem;
            border-radius: 5px;
            cursor: pointer;
            font-size: 1rem;
            transition: all 0.3s ease;
            border: none;
            outline: none;
            background-color: #3498db;
            color: white;
            white-space: nowrap;
            flex-shrink: 0;
            width: 25%;
        }

        .random-btn:hover {
            background-color: #2980b9;
        }

        .size-options {
            display: flex;
            align-items: center;
            width: 100%;
            margin-bottom: 1rem;
        }

        .size-options label {
            margin-right: 1rem;
            white-space: nowrap;
            flex-shrink: 0;
            color: #333; /* Fixed color to prevent change in dark mode */
        }

        .size-options select {
            width: 40%;
            margin-right: 1rem;
            flex-shrink: 0;
        }

        .custom-size-inputs {
            display: flex;
            align-items: center;
            flex-grow: 1;
        }

        .custom-size-inputs label {
            margin-right: 0.5rem;
            white-space: nowrap;
            flex-shrink: 0;
            color: #333; /* Fixed color to prevent change in dark mode */
        }

        .custom-size-inputs input[type="number"] {
            width: 80px;
            padding: 0.75rem;
            margin-right: 0.5rem;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 1rem;
            transition: all 0.3s ease;
            box-sizing: border-box;
        }

        /* Ensure labels remain #333 in dark mode */
        .dark-theme .size-options label,
        .dark-theme .custom-size-inputs label {
            color: #333;
        }
    </style>
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            const submitButton = document.getElementById('submitButton');
            const promptInput = document.getElementById('prompt');
            const downloadBtn = document.getElementById('downloadBtn');
            const historyBtn = document.getElementById('historyBtn');
            const modal = document.getElementById('historyModal');
            const closeBtn = document.getElementsByClassName('close')[0];
            const historyList = document.getElementById('historyList');
            const clearHistoryBtn = document.getElementById('clearHistoryBtn');
            const themeToggle = document.getElementById('themeToggle');
            let history = JSON.parse(localStorage.getItem('aiDrawingHistory')) || [];

            function updateHistory(prompt, imageUrl) {
                history.push({prompt: prompt, imageUrl: imageUrl, timestamp: new Date()});
                localStorage.setItem('aiDrawingHistory', JSON.stringify(history));
                renderHistory();
            }

            const prompts = ['1girl,solo,cute,in glass,atmosphere_X,best quality,beautiful,extremely detailed,masterpiece,very aesthetic',
                'a girl,,nahida,light,full body,symbol eye, nahida,1girl,fair_skin,in summer,day,in a meadow,sky,cirrus_fibratus,intense shadows,blonde hair,pleated_dress,collared_shirt,blue eyes,long hair,fang,smile',
                '((best quality)), ((masterpiece)),A Chinese couple in Hanfu embracing on an arch bridge, a sky full of rose petals, a romantic atmosphere, a huge moon, colorful clouds, ethereal, reflections of water, a mirage, a breeze,(Chinese ink style)',
                'simple background, flower, signature, no humans, sparkle, leaf, plant, white flower, black background, still life, embroidery',
                ' 1 girl,(orange light effect),hair ornament,jewelry,looking at viewer,flower,floating hair,water,underwater,air bubble,submerged, 80sDBA style',
                'masterpiece,best quality,high quality,loli,1girl, solo, long hair, looking at viewer, blush, bangs, thighhighs, dress, ribbon, brown eyes, very long hair, closed mouth, standing, full body, yellow eyes, white hair, short sleeves, outdoors, sky,no shoes, day, puffy sleeves, looking back, cloud, from behind, white dress, white thighhighs, red ribbon, tree, blue sky, puffy short sleeves, petals, cherry blossoms, skirt hold,',
                ' 1 girl,Clothes in the shape of snowflake,render,technology, (best quality) (masterpiece), (highly in detailed), 4K,Official art, unit 8 k wallpaper, ultra detailed, masterpiece, best quality, extremely detailed,CG,low saturation, as style, line art',
                ' best quality,masterpiece,sculpture,wonderland,,chinese fairy tales,an old man,boiling tea,drink tea,a painting of history floating and curved in the background,mountain,white cloud,chinese style courtyard,pavilion,chinese tea mountains,, Chinese architecture, trees,,white hair ,',
                ' 1girl, absurdres, arrow_(symbol), ata-zhubo, bicycle, billboard, black_eyes, black_footwear, black_hair, blue_sky, bridge, building, car, cardigan, city, cityscape, commentary_request, crosswalk, day, fire_hydrant, folding_bicycle, grey_cardigan,',
                'Steep stone walls towered into the sky, thunderous waves crashed against the river bank, and the waves stirred up like thousands of piles of white snow.',
                '1girl, solo, elf, golden eyes, glowing eyes, slit_pupils, silver hair, green gradient hair, long hair, blunt bangs, brown capelet, frilled shirt, long sleeves, green brooch, pouch, belt, brown gloves, upper body, (chibi:0.4), (close-up), (broken:1.3),  half-closed eye, expressionless, from side,  depth of field, fallen leaves, side light, gingko, tree, masterpiece,bestquality, line art,',
                'flower, outdoors, sky, tree, no humans, window, bird, building, scenery, house,oil painting style',
                ' (masterpiece,top quality,best quality,official art,beautiful and aesthetic:1.2),gf-hd,1girl,loli,solo,long hair,lovely smilie,(wink),(blazer,white shirt,white blouse:2),cozy,(lace details),v,robinSR,love heart',
                ' moon,outdoors,full moon,night,flower,cherry blossoms,sky,tree,pink flower flying around,night sky,no humans,masterpiece,illustration,extremely fine and beautiful,perfect details,stream,',
                'comic,bestquality, masterpiece, super details, fine fabrics, highly detailed eyes and face, extremely fine and detailed, perfect details, 1 girl, solo, long hair, bangs, rosy cheeks, pearl hair clips, strawberry blonde tresses, strawberry-shaped stud earrings, sweet lolita-style dress with berry prints, holding a basket of fresh strawberries, whimsical garden setting, sunny and bright',
                '(comic),a girl, soft colors, long curly hair, ocean blue hair, delicate flower crown, shimmering hazel eyes, gentle smile, bohemian style dress, sunny beach background, headshot',
                'bestquality, masterpiece, super details, fine fabrics, high detailed eyes and detailed face, comic, extremely fine and detailed, perfect details, 1girl, solo, long hair, bangs, rose pink eyes, long sleeves, frilly pastel dress, lace accessory, sweet smile, holding a pink macaron, cotton candy pink hair, hair ribbons, soft pink and white dress, fairy tale garden, pink flowers, balloons'
            ];

            function getRandomPrompt() {
                const randomIndex = Math.floor(Math.random() * prompts.length);
                return prompts[randomIndex];
            }

            const randomButton = document.getElementById('randomButton');
            randomButton.addEventListener('click', function () {
                const randomPrompt = getRandomPrompt();
                promptInput.value = randomPrompt;
                promptInput.dispatchEvent(new Event('input'));
            });

            function setupSizeOptions() {
                const sizeSelect = document.getElementById('size-select');
                const widthInput = document.getElementById('width-input');
                const heightInput = document.getElementById('height-input');

                widthInput.addEventListener('input', function () {
                    if (parseInt(this.value) > 1024) this.value = 1024;
                });

                heightInput.addEventListener('input', function () {
                    if (parseInt(this.value) > 1024) this.value = 1024;
                });

                sizeSelect.addEventListener('change', function () {
                    const selectedValue = this.value;
                    if (selectedValue === 'custom') {
                        widthInput.disabled = false;
                        heightInput.disabled = false;
                        return;
                    }
                    widthInput.disabled = true;
                    heightInput.disabled = true;

                    switch (selectedValue) {
                        case '16x9-horizontal':
                            widthInput.value = 1024;
                            heightInput.value = 576;
                            break;
                        case '16x9-vertical':
                            widthInput.value = 576;
                            heightInput.value = 1024;
                            break;
                        case '4x3-horizontal':
                            widthInput.value = 1024;
                            heightInput.value = 768;
                            break;
                        case '4x3-vertical':
                            widthInput.value = 768;
                            heightInput.value = 1024;
                            break;
                        case '1x1':
                            widthInput.value = 1024;
                            heightInput.value = 1024;
                            break;
                        default:
                            widthInput.value = 1024;
                            heightInput.value = 576;
                    }
                });
            }

            setupSizeOptions();

            function renderHistory() {
                historyList.innerHTML = '';
                history.forEach((item, index) => {
                    const historyItem = document.createElement('div');
                    historyItem.className = 'history-item';
                    historyItem.innerHTML = `
                        <img src="${item.imageUrl}" alt="${item.prompt}">
                        <div>
                            <span>${item.prompt}</span>
                            <br>
                            <small>${new Date(item.timestamp).toLocaleString()}</small>
                        </div>
                        <div class="history-item-buttons">
                            <button class="redraw-btn tooltip" data-index="${index}">重绘<span class="tooltiptext">使用此提示词重新生成图片</span></button>
                            <button class="delete-btn tooltip" data-index="${index}">删除<span class="tooltiptext">从历史记录中删除此项</span></button>
                        </div>
                    `;
                    historyList.appendChild(historyItem);
                });
            }

            function deleteHistoryItem(index) {
                history.splice(index, 1);
                localStorage.setItem('aiDrawingHistory', JSON.stringify(history));
                renderHistory();
            }

            function clearHistory() {
                if (confirm('确定要清空所有历史记录吗?此操作不可撤销。')) {
                    history = [];
                    localStorage.removeItem('aiDrawingHistory');
                    renderHistory();
                }
            }

            async function generateImage(prompt, width, height) {
                submitButton.disabled = true;
                submitButton.textContent = '正在创作...';
                document.querySelector('.loading-overlay').style.display = 'flex';

                const model = document.getElementById('model').value;
                const resolution = { width: parseInt(width), height: parseInt(height) };

                try {
                    const controller = new AbortController();
                    const signal = controller.signal;

                    setTimeout(() => controller.abort(), 300000);

                    const response = await fetch(`https://aidraw.foxhank.top`, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ model, prompt, resolution, upload: true }),
                        signal
                    });

                    if (!response.ok) throw new Error(`请求失败:${response.status} ${response.statusText}`);
                    const responseData = await response.json();
                    const imageUrl = responseData.imageUrl;

                    document.getElementById('aiImage').src = imageUrl;
                    updateHistory(prompt, imageUrl);
                    downloadBtn.style.display = 'block';
                } catch (error) {
                    if (error.name === 'AbortError') {
                        alert('服务器连接超时,请稍后重试。');
                    } else {
                        console.error('Error:', error);
                        alert('生成过程中发生错误,请重试。\n错误:' + error.message + '\n');
                    }
                } finally {
                    submitButton.textContent = '开始创作';
                    submitButton.disabled = false;
                    document.querySelector('.loading-overlay').style.display = 'none';
                }
            }

            promptInput.addEventListener('input', function () {
                if (this.value.trim() !== '') {
                    submitButton.classList.add('active');
                } else {
                    submitButton.classList.remove('active');
                }
            });

            submitButton.addEventListener('click', function (event) {
                event.preventDefault();
                if (promptInput.value.trim() === '') {
                    alert('请输入描述词');
                    return;
                }
                generateImage(promptInput.value.trim(), document.getElementById('width-input').value, document.getElementById('height-input').value);
            });

            downloadBtn.addEventListener('click', function () {
                const image = document.getElementById('aiImage');
                const link = document.createElement('a');
                link.href = image.src;
                link.download = `ai-artwork-${new Date().toISOString()}.png`;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            });

            historyBtn.onclick = function () {
                modal.style.display = 'block';
                renderHistory();
            };

            closeBtn.onclick = function () {
                modal.style.display = 'none';
            };

            window.onclick = function (event) {
                if (event.target == modal) modal.style.display = 'none';
            };

            historyList.addEventListener('click', function (event) {
                if (event.target.classList.contains('redraw-btn')) {
                    const index = event.target.getAttribute('data-index');
                    const prompt = history[index].prompt;
                    promptInput.value = prompt;
                    modal.style.display = 'none';
                    generateImage(prompt, document.getElementById('width-input').value, document.getElementById('height-input').value);
                } else if (event.target.classList.contains('delete-btn')) {
                    const index = event.target.getAttribute('data-index');
                    deleteHistoryItem(index);
                }
            });

            clearHistoryBtn.addEventListener('click', clearHistory);

            themeToggle.addEventListener('click', function () {
                document.body.classList.toggle('dark-theme');
                themeToggle.textContent = document.body.classList.contains('dark-theme') ? '🌞' : '🌙';
                localStorage.setItem('theme', document.body.classList.contains('dark-theme') ? 'dark' : 'light');
            });

            if (localStorage.getItem('theme') === 'dark') {
                document.body.classList.add('dark-theme');
                themeToggle.textContent = '🌞';
            }

            document.addEventListener('keydown', function (event) {
                if (event.ctrlKey && event.key === 'Enter') submitButton.click();
            });

            const dropZone = document.querySelector('.image-container');
            dropZone.addEventListener('dragover', (e) => {
                e.preventDefault();
                dropZone.style.border = '2px dashed #4facfe';
            });

            dropZone.addEventListener('dragleave', () => {
                dropZone.style.border = 'none';
            });

            dropZone.addEventListener('drop', (e) => {
                e.preventDefault();
                dropZone.style.border = 'none';
                const file = e.dataTransfer.files[0];
                if (file && file.type.startsWith('image/')) {
                    const reader = new FileReader();
                    reader.onload = (e) => document.getElementById('aiImage').src = e.target.result;
                    reader.readAsDataURL(file);
                }
            });
        });
    </script>
</head>
<body>
<div class="container">
    <div class="card">
        <h1>AI绘画创作平台</h1>
        <div class="image-container">
            <img alt="AI生成的图片" id="aiImage" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==">
            <div class="loading-overlay">
                <div class="loading-spinner"></div>
            </div>
        </div>
        <select id="model">
            <option value="dreamshaper-8-lcm">DreamShaper 8 LCM(容易出黑图)</option>
            <option value="stable-diffusion-xl-base-1.0">Stable Diffusion XL Base 1.0(效果较好)</option>
            <option value="stable-diffusion-xl-lightning">Stable Diffusion XL Lightning(效果一般 速度快)</option>
            <option selected value="flux-1-schnell">flux-1-schnell(推荐用这个)</option>
        </select>

        <div class="input-group">
            <input id="prompt" placeholder="请输入你想要创作的画面描述..." type="text">
            <button class="random-btn" id="randomButton">随机提示词</button>
        </div>
        <div class="size-options">
            <label for="size-select">尺寸:</label>
            <select id="size-select">
                <option value="16x9-horizontal">横屏 16:9</option>
                <option value="16x9-vertical">竖屏 16:9</option>
                <option value="4x3-horizontal">横屏 4:3</option>
                <option value="4x3-vertical">竖屏 4:3</option>
                <option value="1x1">正方形 1:1</option>
                <option value="custom">自定义</option>
            </select>
            <div class="custom-size-inputs">
                <label for="width-input">宽度</label>
                <input disabled id="width-input" max="1024" min="1" placeholder="宽度" type="number" value="1024">
                <label for="height-input">高度</label>
                <input disabled id="height-input" max="1024" min="1" placeholder="高度" type="number" value="576">
            </div>
        </div>
        <div class="button-group">
            <button class="submit-btn" id="submitButton" type="button">开始创作</button>
            <button class="download-btn" id="downloadBtn" style="display: none;" type="button">下载图片</button>
            <button class="history-btn" id="historyBtn" type="button">历史记录</button>
        </div>
        <footer class="footer">
            <p>前端修改自佬友小黑紫一枚:<a href="https://linux.do/u/jiu1/" target="_blank">https://linux.do/u/jiu1/</a></p>
            <p>项目部署于:<a href="https://workers.cloudflare.com/" target="_blank">Cloudflare Workers</a></p>
        </footer>
    </div>
</div>

<div class="modal" id="historyModal">
    <div class="modal-content">
        <span class="close">×</span>
        <h2>历史记录</h2>
        <div id="historyList"></div>
        <button class="clear-history-btn" id="clearHistoryBtn">清空历史记录</button>
    </div>
</div>

<button class="theme-toggle" id="themeToggle">🌙</button>
</body>
</html>

完成后点击部署即可

END