AI免费部署文生图

🚀 部署简介

通过 Cloudflare Workers AI 提供的免费算力,我们可以零成本搭建一个属于自己的文生图(Text-to-Image)工具。本教程将带你一步步完成从代码复制到域名绑定的全过程。


🛠️ 部署步骤

第一阶段:项目初始化

  1. 准备域名:建议准备一个二级域名(如 ai.138gl.com)。您可以使用已有的域名或注册免费域名。
  2. 登录平台:访问并登录 Cloudflare 控制台
  3. 创建 Worker:进入 Workers 和 Pages,创建一个新的 Hello World 项目。

第二阶段:代码配置
注入核心逻辑 (worker.js):

在 Cloudflare Workers 的代码编辑器中,找到默认的 worker.js。
操作:全选并删除编辑器内的所有原有代码。
代码来源:你可以🔗 点击此处跳转 GitHub 复制,或者展开下方代码块直接复制:

📄 点击展开 worker.js 完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
/**
* @author: kared
* @create_date: 2025-05-10 21:15:59
* @last_editors: kared
* @last_edit_time: 2025-05-11 01:25:36
* @description: This Cloudflare Worker script handles image generation.
*/

// import html template
import HTML from './index.html';

// Available models list
const AVAILABLE_MODELS = [
{
id: 'stable-diffusion-xl-base-1.0',
name: 'Stable Diffusion XL Base 1.0',
description: 'Stability AI SDXL 文生图模型',
key: '@cf/stabilityai/stable-diffusion-xl-base-1.0',
requiresImage: false
},
{
id: 'flux-1-schnell',
name: 'FLUX.1 [schnell]',
description: '精确细节表现的高性能文生图模型',
key: '@cf/black-forest-labs/flux-1-schnell',
requiresImage: false
},
{
id: 'dreamshaper-8-lcm',
name: 'DreamShaper 8 LCM',
description: '增强图像真实感的 SD 微调模型',
key: '@cf/lykon/dreamshaper-8-lcm',
requiresImage: false
},
{
id: 'stable-diffusion-xl-lightning',
name: 'Stable Diffusion XL Lightning',
description: '更加高效的文生图模型',
key: '@cf/bytedance/stable-diffusion-xl-lightning',
requiresImage: false
},
{
id: 'stable-diffusion-v1-5-img2img',
name: 'Stable Diffusion v1.5 图生图',
description: '将输入图像风格化或变换(需要提供图像URL)',
key: '@cf/runwayml/stable-diffusion-v1-5-img2img',
requiresImage: true
},
{
id: 'stable-diffusion-v1-5-inpainting',
name: 'Stable Diffusion v1.5 局部重绘',
description: '根据遮罩对局部区域进行重绘(需要图像URL,可选遮罩URL)',
key: '@cf/runwayml/stable-diffusion-v1-5-inpainting',
requiresImage: true,
requiresMask: true
}
];

// Random prompts list
const RANDOM_PROMPTS = [
'cyberpunk cat samurai graphic art, blood splattered, beautiful colors',
'1girl, solo, outdoors, camping, night, mountains, nature, stars, moon, tent, twin ponytails, green eyes, cheerful, happy, backpack, sleeping bag, camping stove, water bottle, mountain boots, gloves, sweater, hat, flashlight,forest, rocks, river, wood, smoke, shadows, contrast, clear sky, constellations, Milky Way',
'masterpiece, best quality, amazing quality, very aesthetic, high resolution, ultra-detailed, absurdres, newest, scenery, anime, anime coloring, (dappled sunlight:1.2), rim light, backlit, dramatic shadow, 1girl, long blonde hair, blue eyes, shiny eyes, parted lips, medium breasts, puffy sleeve white dress, forest, flowers, white butterfly, looking at viewer',
'frost_glass, masterpiece, best quality, absurdres, cute girl wearing red Christmas dress, holding small reindeer, hug, braided ponytail, sidelocks, hairclip, hair ornaments, green eyes, (snowy forest, moonlight, Christmas trees), (sparkles, sparkling clothes), frosted, snow, aurora, moon, night, sharp focus, highly detailed, abstract, flowing',
'1girl, hatsune miku, white pupils, power elements, microphone, vibrant blue color palette, abstract,abstract background, dreamlike atmosphere, delicate linework, wind-swept hair, energy, masterpiece, best quality, amazing quality',
'cyberpunk cat(neon lights:1.3) clutter,ultra detailed, ctrash, chaotic, low light, contrast, dark, rain ,at night ,cinematic , dystopic, broken ground, tunnels, skyscrapers',
'Cyberpunk catgirl with purple hair, wearing leather and latex outfit with pink and purple cheetah print, holding a hand gun, black latex brassiere, glowing blue eyes with purple tech sunglasses, tail, large breasts, glowing techwear clothes, handguns, black leather jacket, tight shiny leather pants, cyberpunk alley background, Cyb3rWar3, Cyberware',
'a wide aerial view of a floating elven city in the sky, with two elven figures walking side by side across a glowing skybridge, the bridge arching between tall crystal towers, surrounded by clouds and golden light, majestic and serene atmosphere, vivid style, magical fantasy architecture',
'masterpiece, newest, absurdres,incredibly absurdres, best quality, amazing quality, very aesthetic, 1girl, very long hair, blonde, multi-tied hair, center-flap bangs, sunset, cumulonimbus cloud, old tree,sitting in tree, dark blue track suit, adidas, simple bird',
'beautiful girl, breasts, curvy, looking down scope, looking away from viewer, laying on the ground, laying ontop of jacket, aiming a sniper rifle, dark braided hair, backwards hat, armor, sleeveless, arm sleeve tattoos, muscle tone, dogtags, sweaty, foreshortening, depth of field, at night, night, alpine, lightly snowing, dusting of snow, Closeup, detailed face, freckles',
];

// Passwords for authentication
// demo: const PASSWORDS = ['P@ssw0rd']
const PASSWORDS = ['admin123']


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

// CORS Headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};

if (request.method === 'OPTIONS') {
return new Response(null, {
headers: corsHeaders
});
}

try {
const url = new URL(request.url);
const path = url.pathname;

// process api requests
if (path === '/api/models') {
// get available models list
return new Response(JSON.stringify(AVAILABLE_MODELS), {
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
});
} else if (path === '/api/prompts') {
// get random prompts list
return new Response(JSON.stringify(RANDOM_PROMPTS), {
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
});
} else if (path === '/api/config') {
// expose minimal config to client
return new Response(JSON.stringify({ require_password: PASSWORDS.length > 0 }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
} else if (path === '/api/auth' && request.method === 'POST') {
// perform password authentication and set cookie
const data = await request.json().catch(() => ({}));
const ok = PASSWORDS.length === 0 ? true : (data && typeof data.password === 'string' && PASSWORDS.includes(data.password));
if (!ok) {
return new Response(JSON.stringify({ error: '密码错误' }), {
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
const cookie = `auth=1; Path=/; Max-Age=${7 * 24 * 3600}; HttpOnly; SameSite=Lax; Secure`;
return new Response(JSON.stringify({ success: true }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Set-Cookie': cookie }
});
} else if (request.method === 'POST') {
// process POST request for image generation
const data = await request.json();

// Check if password is required and valid (Cookie or request body)
const cookieHeader = request.headers.get('cookie') || '';
const authedByCookie = /(?:^|;\s*)auth=1(?:;|$)/.test(cookieHeader);
const authedByBody = data && typeof data.password === 'string' && PASSWORDS.includes(data.password);
if (PASSWORDS.length > 0 && !(authedByCookie || authedByBody)) {
return new Response(JSON.stringify({ error: '需要正确的访问密码' }), {
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}

if ('prompt' in data && 'model' in data) {
const selectedModel = AVAILABLE_MODELS.find(m => m.id === data.model);
if (!selectedModel) {
return new Response(JSON.stringify({ error: 'Model is invalid' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}

const model = selectedModel.key;
let inputs = {};
const fetchImageToBytes = async (url, label) => {
const resp = await fetch(url);
if (!resp.ok) {
return { error: `${label}获取失败,HTTP ${resp.status}` };
}
const ct = resp.headers.get('content-type') || '';
if (!ct.startsWith('image/')) {
return { error: `${label}不是图片资源,content-type=${ct}` };
}
const cl = parseInt(resp.headers.get('content-length') || '0', 10);
// 设定 10MB 上限,避免大文件触发内部错误
if (cl && cl > 10 * 1024 * 1024) {
return { error: `${label}体积过大(${(cl/1024/1024).toFixed(2)}MB),请不超过10MB` };
}
const bytes = new Uint8Array(await resp.arrayBuffer());
return { bytes, contentType: ct, size: bytes.length };
};
const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
const sanitizeDimension = (val, def = 512) => {
let v = typeof val === 'number' ? val : def;
v = clamp(v, 256, 2048);
// 四舍五入到最近的64倍数
v = Math.round(v / 64) * 64;
return v;
};

// Input parameter processing
if (data.model === 'flux-1-schnell') {
let steps = data.num_steps || 6;
if (steps >= 8) steps = 8;
else if (steps <= 4) steps = 4;

// Only prompt and steps
inputs = {
prompt: data.prompt || 'cyberpunk cat',
steps: steps
};
} else if (
data.model === 'stable-diffusion-v1-5-img2img' ||
data.model === 'stable-diffusion-v1-5-inpainting'
) {
// 图生图 / 局部重绘需要图像URL
if (!data.image_url) {
return new Response(JSON.stringify({ error: '该模型需要提供 image_url 参数(输入图像 URL)' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}

// 拉取输入图像/遮罩为二进制并校验
const imageResult = await fetchImageToBytes(data.image_url, '输入图像');
if (imageResult.error) {
return new Response(JSON.stringify({ error: imageResult.error }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}

let maskBytes = undefined;
if (data.model === 'stable-diffusion-v1-5-inpainting') {
if (!data.mask_url) {
return new Response(JSON.stringify({ error: '该模型需要提供 mask_url 参数(遮罩图像 URL)' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
const maskResult = await fetchImageToBytes(data.mask_url, '遮罩图像');
if (maskResult.error) {
return new Response(JSON.stringify({ error: maskResult.error }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
maskBytes = maskResult.bytes;
}

// 兼容一些模型对字段命名的要求:有的需要 mask_image
inputs = {
prompt: data.prompt || 'cyberpunk cat',
negative_prompt: data.negative_prompt || '',
// 建议使用更小的分辨率,避免 3001 内部错误
height: sanitizeDimension(parseInt(data.height, 10) || 512, 512),
width: sanitizeDimension(parseInt(data.width, 10) || 512, 512),
num_steps: clamp(parseInt(data.num_steps, 10) || 20, 1, 50),
strength: clamp(parseFloat(data.strength ?? 0.8), 0.0, 1.0),
guidance: clamp(parseFloat(data.guidance ?? 7.5), 0.0, 30.0),
seed: data.seed || parseInt((Math.random() * 1024 * 1024).toString(), 10),
image: [...imageResult.bytes],
...(maskBytes ? { mask: [...maskBytes], mask_image: [...maskBytes] } : {})
};
} else {
// Default input parameters
inputs = {
prompt: data.prompt || 'cyberpunk cat',
negative_prompt: data.negative_prompt || '',
height: data.height || 1024,
width: data.width || 1024,
num_steps: data.num_steps || 20,
strength: data.strength || 0.1,
guidance: data.guidance || 7.5,
seed: data.seed || parseInt((Math.random() * 1024 * 1024).toString(), 10),
};
}

console.log(`Generating image with ${model} and prompt: ${inputs.prompt.substring(0, 50)}...`);

try {
const numOutputs = clamp(parseInt(data.num_outputs, 10) || 1, 1, 8);
const generateOnce = async (seedOffset = 0) => {
const localInputs = { ...inputs };
if (typeof localInputs.seed === 'number') localInputs.seed = localInputs.seed + seedOffset;
const t0 = Date.now();
const res = await env.AI.run(model, localInputs);
const t1 = Date.now();
return { res, seconds: (t1 - t0) / 1000 };
};

// helper: convert bytes to base64
const bytesToBase64 = (bytes) => {
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
const sub = bytes.subarray(i, i + chunk);
binary += String.fromCharCode.apply(null, sub);
}
return btoa(binary);
};

if (numOutputs > 1) {
const tasks = Array.from({ length: numOutputs }, (_, i) => generateOnce(i));
const results = await Promise.all(tasks);
const secondsAvg = results.reduce((s, r) => s + r.seconds, 0) / results.length;

const images = [];
for (const { res } of results) {
if (data.model === 'flux-1-schnell') {
const json = typeof res === 'object' ? res : JSON.parse(res);
if (!json.image) throw new Error('Invalid response from FLUX: missing image');
images.push(`data:image/png;base64,${json.image}`);
} else {
// binary bytes -> base64
let bytes;
if (res instanceof Uint8Array) bytes = res;
else if (res && typeof res === 'object' && typeof res.byteLength === 'number') bytes = new Uint8Array(res);
else bytes = new Uint8Array(await new Response(res).arrayBuffer());
images.push(`data:image/png;base64,${bytesToBase64(bytes)}`);
}
}

return new Response(JSON.stringify({ images }), {
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Used-Model': selectedModel.id,
'X-Server-Seconds': secondsAvg.toFixed(3),
}
});
}

const { res: response, seconds: serverSeconds } = await generateOnce(0);

// Processing the response of the flux-1-schnell model
if (data.model === 'flux-1-schnell') {
let jsonResponse;

if (typeof response === 'object') {
jsonResponse = response;
} else {
try {
jsonResponse = JSON.parse(response);
} catch (e) {
console.error('Failed to parse JSON response:', e);
return new Response(JSON.stringify({
error: 'Failed to parse response',
details: e.message
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
}

if (!jsonResponse.image) {
return new Response(JSON.stringify({
error: 'Invalid response format',
details: 'Image data not found in response'
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}

try {
// Convert from base64 to binary data
const binaryString = atob(jsonResponse.image);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}

// Returns binary data in PNG format
return new Response(bytes, {
headers: {
...corsHeaders,
'content-type': 'image/png',
'X-Used-Model': selectedModel.id,
...(inputs.seed ? { 'X-Seed': String(inputs.seed) } : {}),
'X-Image-Bytes': String(bytes.length),
'X-Server-Seconds': serverSeconds.toFixed(3),
},
});
} catch (e) {
console.error('Failed to convert base64 to binary:', e);
return new Response(JSON.stringify({
error: 'Failed to process image data',
details: e.message
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
} else {
// Return the response directly (binary)
let imageByteSize = undefined;
try {
if (response && typeof response === 'object') {
if (response instanceof Uint8Array) imageByteSize = response.length;
// ArrayBuffer has byteLength
if (typeof response.byteLength === 'number') imageByteSize = response.byteLength;
}
} catch (_) {}

return new Response(response, {
headers: {
...corsHeaders,
'content-type': 'image/png',
'X-Used-Model': selectedModel.id,
...(inputs.seed ? { 'X-Seed': String(inputs.seed) } : {}),
...(imageByteSize ? { 'X-Image-Bytes': String(imageByteSize) } : {}),
'X-Server-Seconds': serverSeconds.toFixed(3),
},
});
}
} catch (aiError) {
console.error('AI generation error:', aiError);
return new Response(JSON.stringify({
error: 'Image generation failed',
details: aiError && (aiError.message || aiError.toString()),
model: selectedModel.id
}), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
} else {
return new Response(JSON.stringify({ error: 'Missing required parameter: prompt or model' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
} else if (path.endsWith('.html') || path === '/') {
// redirect to index.html for HTML requests
return new Response(HTML.replace(/{{host}}/g, originalHost), {
status: 200,
headers: {
...corsHeaders,
"content-type": "text/html"
}
});
} else {
return new Response('Not Found', { status: 404 });
}
} catch (error) {
console.error('Worker error:', error);
return new Response(JSON.stringify({ error: 'Internal server error', details: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
});
}
},
};

  1. 设置访问密码

    • worker.js 中找到以下位置(约第 75 行):
    1
    2
    // 💡 提示:在下面这行修改你的访问密码
    const PASSWORDS = ['admin123'];
    • 关键步骤:'admin123' 替换为你自定义的密码,防止他人滥用你的算力额度。(注意保留单引号)。
  2. 添加前端界面 (index.html)

    • 在 Workers 编辑器左侧文件栏,点击 “+” 或右键点击 “New File”
    • 文件名:务必填入 index.html(全小写)。
    • 🔗 点击此处跳转 GitHub 复制 ,或者展开下方代码块复制:
📄 点击展开 index.html 完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
<!DOCTYPE html>
<html lang="zh" class="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基于 CloudFlare 的在线文生图服务</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link rel="icon" type="image/x-icon" href="https://cdn.jsdelivr.net/gh/huarzone/Text2img-Cloudflare-Workers@main/public/favicon.ico">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js"></script>
<style>
:root {
--primary: #5046e5;
--primary-light: #6e67eb;
--primary-dark: #4338ca;
--secondary: #f0f4f8;
--text: #1a202c;
--text-light: #4a5568;
--background: #ffffff;
--card-bg: #f7fafc;
--border: #e2e8f0;
--success: #10b981;
--error: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
}

.dark {
--primary: #6e67eb;
--primary-light: #8a84ee;
--primary-dark: #5046e5;
--secondary: #2d3748;
--text: #f7fafc;
--text-light: #cbd5e0;
--background: #111827;
--card-bg: #1f2937;
--border: #374151;
--success: #10b981;
--error: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
}

body {
background-color: var(--background);
color: var(--text);
transition: background-color 0.3s ease, color 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

.btn {
padding: 1rem 1.5rem;
border-radius: 0.375rem;
transition: all 0.3s;
}

.btn:focus {
outline: none;
}

.btn-primary {
background-color: var(--primary);
color: white;
}

.btn-primary:hover {
background-color: var(--primary-light);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

.btn-primary:active {
background-color: var(--primary-dark);
transform: translateY(0);
}

.btn-secondary {
background-color: var(--secondary);
color: var(--text);
border: 1px solid var(--border);
}

.btn-secondary:hover {
background-color: var(--border);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

.btn-secondary:active {
transform: translateY(0);
}

.card {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}

.card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

input, select, textarea {
background-color: var(--background);
color: var(--text);
border: 1px solid var(--border);
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
transition: all 0.3s ease;
width: 100%;
}

input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(80, 70, 229, 0.1);
}

.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
border-radius: 5px;
background: var(--border);
outline: none;
margin: 10px 0;
}

.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
transition: all 0.2s ease;
}

.slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 0 3px rgba(80, 70, 229, 0.2);
}

.slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
transition: all 0.2s ease;
border: none;
}

.slider::-moz-range-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 0 3px rgba(80, 70, 229, 0.2);
}

.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}

.loading-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.6);
border-radius: 0.5rem;
z-index: 10;
backdrop-filter: blur(4px);
}

.image-container {
aspect-ratio: 1 / 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--card-bg);
position: relative;
border-radius: 0.5rem;
max-height: 400px; /* 添加最大高度限制 */
margin: 0 auto; /* 居中显示 */
width: 100%; /* 保持宽度响应式 */
}

#imageStatus {
position: absolute;
bottom: 1rem;
left: 1rem;
z-index: 20;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}

.param-badge {
background-color: var(--secondary);
color: var(--text);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
display: inline-block;
border: 1px solid var(--border);
}

.fade-in {
animation: fadeIn 0.5s ease-in-out;
}

@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

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

.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: var(--card-bg);
color: var(--text);
text-align: center;
border-radius: 6px;
padding: 8px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border);
font-size: 0.75rem;
}

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

.hidden {
display: none !important;
}

@media (max-width: 768px) {
.mobile-flex-col {
flex-direction: column;
}

.container {
padding-left: 1rem;
padding-right: 1rem;
}

.image-container {
max-height: 400px; /* 移动端适当减小最大高度 */
}
}

@media (min-width: 1024px) {
.container {
max-width: 1200px;
}
}
</style>
</head>
<body class="min-h-screen py-4">
<div class="container mx-auto px-4 py-4 max-w-6xl">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl md:text-3xl font-bold flex items-center">
🐳&nbsp;在线文生图服务
</h1>
<div class="flex items-center space-x-2">
<button id="themeToggle" class="btn btn-secondary p-2 h-10 w-10 flex items-center justify-center" aria-label="切换暗色主题">
<i class="fa-solid fa-moon"></i>
</button>
<button id="github" class="btn btn-secondary p-2 h-10 w-10 flex items-center justify-center" aria-label="项目地址" onclick="window.open('https://github.com/huarzone/Text2img-Cloudflare-Workers', '_blank')">
<i class="fa-brands fa-github"></i>
</button>
</div>
</div>

<div class="flex flex-col lg:flex-row gap-6 mobile-flex-col">
<!-- 左侧控制面板 -->
<div class="w-full lg:w-2/5 space-y-4">
<div class="card p-4 space-y-4 fade-in">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold flex items-center">
<i class="fa-solid fa-sliders mr-2 text-primary"></i>
基本设置
</h2>
<button id="randomButton" class="btn btn-secondary text-sm py-1 px-3 flex items-center">
<i class="fa-solid fa-dice mr-1"></i> 随机提示词
</button>
</div>

<div>
<label for="password" class="block text-sm font-medium mb-1 flex items-center">
<i class="fa-solid fa-key mr-1 text-xs"></i> 访问密码
</label>
<input type="password" id="password" placeholder="请输入访问密码" class="w-full" value="admin123">
</div>

<div>
<label for="prompt" class="block text-sm font-medium mb-1 flex items-center">
<i class="fa-solid fa-wand-magic-sparkles mr-1 text-xs"></i> 正向提示词
</label>
<textarea id="prompt" rows="3" placeholder="描述您想要生成的图像内容及样式..." class="w-full">cyberpunk cat</textarea>
</div>

<div>
<label for="negative_prompt" class="block text-sm font-medium mb-1 flex items-center">
<i class="fa-solid fa-ban mr-1 text-xs"></i> 反向提示词
</label>
<textarea id="negative_prompt" rows="2" placeholder="描述在生成的图像中要避免的元素文本..." class="w-full"></textarea>
</div>

<div>
<label for="model" class="block text-sm font-medium mb-1 flex items-center">
<i class="fa-solid fa-robot mr-1 text-xs"></i> 文生图模型
</label>
<select id="model" class="w-full">
<option value="loading" disabled selected>加载中...</option>
</select>
<p class="text-xs text-gray-500 mt-2">部分模型需要提供图片URL或遮罩URL(见下方输入框)。</p>
</div>

<div id="img2imgInputs" class="hidden space-y-2">
<div>
<label for="image_url" class="block text-sm font-medium mb-1 flex items-center">
<i class="fa-solid fa-image mr-1 text-xs"></i> 输入图像URL(必填)
</label>
<input type="url" id="image_url" placeholder="https://example.com/your-image.png" class="w-full">
</div>
<div>
<label for="mask_url" class="block text-sm font-medium mb-1 flex items-center">
<i class="fa-solid fa-mask mr-1 text-xs"></i> 遮罩图像URL(必填,inpainting 使用)
</label>
<input type="url" id="mask_url" placeholder="https://example.com/your-mask.png" class="w-full">
</div>
</div>
</div>

<div class="card p-4 space-y-4 fade-in">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold flex items-center">
<i class="fa-solid fa-gear mr-2 text-primary"></i>
高级选项
</h2>
<button id="toggleAdvanced" class="text-xs btn btn-secondary py-1 px-3 flex items-center">
<i class="fa-solid fa-chevron-down mr-1" id="advancedIcon"></i> 显示/隐藏
</button>
</div>

<div id="advancedOptions" class="space-y-3 hidden">
<div>
<div class="flex justify-between items-center">
<label for="width" class="block text-sm font-medium flex items-center">
<i class="fa-solid fa-arrows-left-right mr-1 text-xs"></i> 图像宽度
</label>
<span id="widthValue" class="text-sm font-mono">1024px</span>
</div>
<input type="range" id="width" min="256" max="2048" step="64" value="1024" class="slider w-full">
</div>

<div>
<div class="flex justify-between items-center">
<label for="height" class="block text-sm font-medium flex items-center">
<i class="fa-solid fa-arrows-up-down mr-1 text-xs"></i> 图像高度
</label>
<span id="heightValue" class="text-sm font-mono">1024px</span>
</div>
<input type="range" id="height" min="256" max="2048" step="64" value="1024" class="slider w-full">
</div>

<div>
<div class="flex justify-between items-center">
<label for="num_steps" class="block text-sm font-medium flex items-center tooltip">
<i class="fa-solid fa-shoe-prints mr-1 text-xs"></i> 迭代步数
<span class="tooltiptext">更高的步数通常会产生更精细的细节,但需要更长的处理时间</span>
</label>
<span id="num_stepsValue" class="text-sm font-mono">20</span>
</div>
<input type="range" id="num_steps" min="1" max="20" step="1" value="20" class="slider w-full">
</div>

<div>
<div class="flex justify-between items-center">
<label for="guidance" class="block text-sm font-medium flex items-center tooltip">
<i class="fa-solid fa-compass mr-1 text-xs"></i> 引导系数
<span class="tooltiptext">控制生成图像与提示词的匹配程度,较高的值会更严格遵循提示词</span>
</label>
<span id="guidanceValue" class="text-sm font-mono">7.5</span>
</div>
<input type="range" id="guidance" min="0" max="30" step="0.5" value="7.5" class="slider w-full">
</div>

<div>
<label for="seed" class="block text-sm font-medium mb-1 flex items-center tooltip">
<i class="fa-solid fa-seedling mr-1 text-xs"></i> 随机种子
<span class="tooltiptext">使用相同的种子值可以在其他参数相同的情况下生成相似的图像</span>
</label>
<div class="flex gap-2">
<input type="number" id="seed" placeholder="随机种子值" class="w-full">
<button id="randomSeed" class="btn btn-secondary text-sm py-1 px-3">
<i class="fa-solid fa-random"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-1">留空则随机生成</p>
</div>
</div>
</div>

<button id="submitButton" class="btn btn-primary w-full py-3 flex items-center justify-center">
<i class="fa-solid fa-wand-magic-sparkles mr-2"></i> 生成图像
</button>
</div>

<!-- 右侧图像展示 -->
<div class="w-full lg:w-3/5">
<div class="card h-full p-4 space-y-4 fade-in">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold flex items-center">
<i class="fa-solid fa-image mr-2 text-primary"></i>
生成结果
</h2>
<div class="flex space-x-2">
<button id="copyParamsButton" class="btn btn-secondary text-sm py-1 px-3 hidden">
<i class="fa-solid fa-copy mr-1"></i> 复制参数
</button>
<button id="downloadButton" class="btn btn-secondary text-sm py-1 px-3 hidden">
<i class="fa-solid fa-download mr-1"></i> 下载图像
</button>
<button id="downloadZipButton" class="btn btn-secondary text-sm py-1 px-3 hidden">
<i class="fa-solid fa-file-zipper mr-1"></i> 下载ZIP
</button>
</div>
</div>

<div class="image-container card">
<div id="loadingOverlay" class="loading-mask hidden">
<div class="text-center w-full px-6">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-white"></div>
<p class="text-white mt-3 font-medium">生成中,请稍候...</p>
<p class="text-white text-sm mt-1">这可能需要几秒到几十秒不等</p>
<div id="progressBarContainer" class="w-full mt-4">
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div id="progressBar" class="bg-blue-500 h-2.5 rounded-full" style="width: 0%"></div>
</div>
<div class="flex justify-between text-xs mt-1 text-white">
<span id="progressText">0%</span>
<span id="progressExtra">估算</span>
</div>
</div>
</div>
</div>
<div id="initialPrompt" class="text-center text-gray-400 dark:text-gray-600">
<i class="fa-solid fa-image-portrait text-4xl mb-2"></i>
<p>点击生成按钮开始创建图像</p>
</div>
<span id="imageStatus" class="bg-gray-300 text-gray-700 hidden">状态</span>
<img id="aiImage" class="max-h-full max-w-full rounded hidden" alt="生成的图像">
<div id="imageGallery" class="grid grid-cols-4 gap-2 mt-3 hidden"></div>
</div>

<div id="imageInfo" class="space-y-3 mt-2">
<div class="grid grid-cols-2 gap-3">
<div class="text-sm flex items-center">
<span class="font-medium flex items-center">
<i class="fa-regular fa-clock mr-1 text-xs"></i> 生成时间:
</span>
<span id="generationTime" class="ml-1">-</span>
</div>
<div class="text-sm flex items-center">
<span class="font-medium flex items-center">
<i class="fa-solid fa-microchip mr-1 text-xs"></i> 使用模型:
</span>
<span id="usedModel" class="ml-1">-</span>
</div>
<div class="text-sm flex items-center">
<span class="font-medium flex items-center">
<i class="fa-solid fa-bolt mr-1 text-xs"></i> 算力估计:
</span>
<span id="computeInfo" class="ml-1">-</span>
<span class="text-xs text-gray-500 ml-2">(it/s)</span>
</div>
<div class="text-sm flex items-center">
<span class="font-medium flex items-center">
<i class="fa-solid fa-database mr-1 text-xs"></i> 输出大小:
</span>
<span id="imageSize" class="ml-1">-</span>
</div>
</div>

<div id="allParamsContainer" class="hidden mt-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<h3 class="text-sm font-medium mb-2 flex items-center">
<i class="fa-solid fa-list-check mr-1"></i> 所有参数
</h3>
<div id="allParams" class="flex flex-wrap"></div>
</div>

<div id="imageMeta" class="hidden mt-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<h3 class="text-sm font-medium mb-2 flex items-center">
<i class="fa-solid fa-table mr-1"></i> 每张图片信息
</h3>
<table class="w-full text-xs">
<thead>
<tr class="text-left">
<th class="py-1">#</th>
<th class="py-1">尺寸</th>
<th class="py-1">大小</th>
</tr>
</thead>
<tbody id="imageMetaBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function () {
// 初始化模型列表
let availableModels = [];
let randomPromptsList = [];
let currentImageParams = {};
let currentImagesArray = null; // 多图时的 dataURL 数组

// 加载模型列表
async function loadModels() {
try {
const response = await fetch('/api/models');
if (!response.ok) {
throw new Error('加载模型列表失败');
}

availableModels = await response.json();
const modelSelect = document.getElementById('model');

// 清空当前选项
modelSelect.innerHTML = '';

// 添加新选项
availableModels.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = `${model.name} - ${model.description}`;
option.dataset.requiresImage = model.requiresImage ? '1' : '0';
modelSelect.appendChild(option);
});

// 默认选择第二个模型(通常是更好的模型)
if (availableModels.length > 1) {
modelSelect.value = availableModels[1].id;
}
} catch (error) {
console.error('加载模型列表错误:', error);
showStatus('加载模型列表失败', 'error');
}
}

// 加载随机提示词
async function loadRandomPrompts() {
try {
const response = await fetch('/api/prompts');
if (!response.ok) {
throw new Error('加载提示词失败');
}

randomPromptsList = await response.json();
} catch (error) {
console.error('加载提示词错误:', error);
randomPromptsList = ['未能加载提示词列表,请重试或手动输入'];
}
}

// 初始化加载资源
loadModels();
loadRandomPrompts();
// 检查是否需要登录
(async () => {
try {
const res = await fetch('/api/config');
if (res.ok) {
const cfg = await res.json();
if (cfg.require_password) {
showLogin();
}
}
} catch (_) {}
})();

// 主题切换功能相关代码
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const moonIcon = `<i class="fa-solid fa-moon"></i>`;
const sunIcon = `<i class="fa-solid fa-sun"></i>`;

// 检查系统主题或存储的主题并设置初始状态
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
html.classList.add('dark');
themeToggle.innerHTML = sunIcon;
themeToggle.setAttribute('aria-label', '切换亮色主题');
} else {
html.classList.remove('dark');
themeToggle.innerHTML = moonIcon;
themeToggle.setAttribute('aria-label', '切换暗色主题');
}

themeToggle.addEventListener('click', function() {
if (html.classList.contains('dark')) {
html.classList.remove('dark');
localStorage.theme = 'light';
themeToggle.innerHTML = moonIcon;
themeToggle.setAttribute('aria-label', '切换暗色主题');
} else {
html.classList.add('dark');
localStorage.theme = 'dark';
themeToggle.innerHTML = sunIcon;
themeToggle.setAttribute('aria-label', '切换亮色主题');
}
});

// 高级选项切换
const toggleAdvanced = document.getElementById('toggleAdvanced');
const advancedOptions = document.getElementById('advancedOptions');
const advancedIcon = document.getElementById('advancedIcon');

toggleAdvanced.addEventListener('click', function() {
if (advancedOptions.classList.contains('hidden')) {
advancedOptions.classList.remove('hidden');
advancedIcon.classList.remove('fa-chevron-down');
advancedIcon.classList.add('fa-chevron-up');
} else {
advancedOptions.classList.add('hidden');
advancedIcon.classList.remove('fa-chevron-up');
advancedIcon.classList.add('fa-chevron-down');
}
});

// 模型切换时显示/隐藏图生图字段
const modelSelectEl = document.getElementById('model');
const img2imgInputs = document.getElementById('img2imgInputs');
if (modelSelectEl && img2imgInputs) {
modelSelectEl.addEventListener('change', function() {
const selected = availableModels.find(m => m.id === this.value);
if (selected && selected.requiresImage) {
img2imgInputs.classList.remove('hidden');
} else {
img2imgInputs.classList.add('hidden');
}
});
}

// 滑块值显示
const sliders = ['width', 'height', 'num_steps', 'guidance'];
sliders.forEach(id => {
const slider = document.getElementById(id);
const valueDisplay = document.getElementById(`${id}Value`);

slider.addEventListener('input', function() {
if (id === 'width' || id === 'height') {
valueDisplay.textContent = `${this.value}px`;
} else if (id === 'guidance') {
valueDisplay.textContent = parseFloat(this.value).toFixed(2);
} else {
valueDisplay.textContent = this.value;
}
});
});

// 生成数量控件
const countContainer = document.createElement('div');
countContainer.className = 'mt-2';
countContainer.innerHTML = `
<label for="num_outputs" class="block text-sm font-medium mb-1 flex items-center">
<i class="fa-solid fa-layer-group mr-1 text-xs"></i> 生成数量
</label>
<select id="num_outputs" class="w-full">
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
</select>
`;
document.querySelector('.card.p-4.space-y-4.fade-in').appendChild(countContainer);

// 随机种子
document.getElementById('randomSeed').addEventListener('click', function() {
const randomSeed = Math.floor(Math.random() * 4294967295);
document.getElementById('seed').value = randomSeed;
});

// 随机提示词
document.getElementById('randomButton').addEventListener('click', function() {
if (randomPromptsList.length > 0) {
const randomIndex = Math.floor(Math.random() * randomPromptsList.length);
document.getElementById('prompt').value = randomPromptsList[randomIndex];
} else {
showStatus('提示词列表未加载,请稍后再试', 'error');
}
});

// 复制参数
document.getElementById('copyParamsButton').addEventListener('click', function() {
if (!currentImageParams) return;

// 创建参数文本
let paramsText = '--- AI绘图创作生成参数 ---\n';
for (const [key, value] of Object.entries(currentImageParams)) {
if (key === 'password') continue; // 不复制密码
paramsText += `${formatParamName(key)}: ${value}\n`;
}

// 复制到剪贴板
navigator.clipboard.writeText(paramsText)
.then(() => {
showStatus('参数已复制到剪贴板', 'success');
})
.catch(err => {
console.error('复制失败:', err);
showStatus('复制参数失败', 'error');
});
});

// 格式化参数名称
function formatParamName(name) {
const nameMap = {
'prompt': '正向提示词',
'negative_prompt': '反向提示词',
'model': '文生图模型',
'width': '图像宽度',
'height': '图像高度',
'num_steps': '迭代步数',
'guidance': '引导系数',
'seed': '随机种子'
};
return nameMap[name] || name;
}

// 下载图像
document.getElementById('downloadButton').addEventListener('click', async function() {
const img = document.getElementById('aiImage');
const gallery = document.getElementById('imageGallery');
let src = '';
if (!gallery.classList.contains('hidden') && currentImagesArray && currentImagesArray.length > 0) {
src = currentImagesArray[0];
} else {
src = img.src;
}
if (!src) {
showStatus('没有可下载的图像', 'error');
return;
}

try {
// 从图像数据创建blob
const response = await fetch(src);
const blob = await response.blob();

// 创建下载链接
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;

// 生成文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const model = document.getElementById('usedModel').textContent || 'ai-image';
link.download = `${model}-${timestamp}.png`;

// 触发下载
document.body.appendChild(link);
link.click();

// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(url);

showStatus('图像下载成功', 'success');
} catch (error) {
console.error('下载图像错误:', error);
showStatus('下载图像失败', 'error');
}
});

// 下载 ZIP(多图)
document.getElementById('downloadZipButton').addEventListener('click', async function() {
if (!currentImagesArray || currentImagesArray.length === 0) {
showStatus('没有可打包的图片', 'error');
return;
}
try {
const zip = new JSZip();
const model = document.getElementById('usedModel').textContent || 'ai-image';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
for (let i = 0; i < currentImagesArray.length; i++) {
const dataURL = currentImagesArray[i];
const base64 = dataURL.split(',')[1];
zip.file(`${model}-${timestamp}-${i+1}.png`, base64, { base64: true });
}
const blob = await zip.generateAsync({ type: 'blob' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${model}-${timestamp}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
showStatus('ZIP 下载已开始', 'success');
} catch (e) {
console.error('ZIP打包失败:', e);
showStatus('打包ZIP失败', 'error');
}
});

// 提交生成请求
// 复用全局的计时器与控制器,避免闪烁
let progressTimer = null;
let pendingController = null;

document.getElementById('submitButton').addEventListener('click', async function() {
// 显示加载中状态
const loadingOverlay = document.getElementById('loadingOverlay');
const initialPrompt = document.getElementById('initialPrompt');
const aiImage = document.getElementById('aiImage');
const progressBarContainer = document.getElementById('progressBarContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const progressExtra = document.getElementById('progressExtra');

if (!loadingOverlay || !initialPrompt || !aiImage) {
console.error('必要的DOM元素未找到');
return;
}

// 隐藏初始提示和图像
initialPrompt.classList.add('hidden');
aiImage.classList.add('hidden');
loadingOverlay.classList.remove('hidden');

// 隐藏之前的提示和按钮,并清理上一次状态
const imageStatus = document.getElementById('imageStatus');
const copyParamsButton = document.getElementById('copyParamsButton');
const downloadButton = document.getElementById('downloadButton');

if (imageStatus) imageStatus.classList.add('hidden');
if (copyParamsButton) copyParamsButton.classList.add('hidden');
if (downloadButton) downloadButton.classList.add('hidden');
if (progressTimer) { clearInterval(progressTimer); progressTimer = null; }
if (pendingController) { try { pendingController.abort(); } catch(_){} pendingController = null; }

// 获取参数(初始草案)
const rawParams = {
password: document.getElementById('password')?.value || '',
prompt: document.getElementById('prompt')?.value || '',
negative_prompt: document.getElementById('negative_prompt')?.value || '',
model: document.getElementById('model')?.value,
width: parseInt(document.getElementById('width')?.value) || 1024,
height: parseInt(document.getElementById('height')?.value) || 1024,
num_steps: parseInt(document.getElementById('num_steps')?.value) || 20,
guidance: parseFloat(document.getElementById('guidance')?.value) || 7.5,
seed: parseInt(document.getElementById('seed')?.value) || Math.floor(Math.random() * 4294967295),
image_url: document.getElementById('image_url')?.value || '',
mask_url: document.getElementById('mask_url')?.value || '',
num_outputs: parseInt(document.getElementById('num_outputs')?.value) || 1
};
// 基于模型特性,清洗出最终要发送/展示的参数
const selectedModelMeta = availableModels.find(m => m.id === rawParams.model);
const params = {
password: rawParams.password,
prompt: rawParams.prompt,
negative_prompt: rawParams.negative_prompt,
model: rawParams.model,
width: rawParams.width,
height: rawParams.height,
num_steps: rawParams.num_steps,
guidance: rawParams.guidance,
seed: rawParams.seed,
num_outputs: rawParams.num_outputs
};
if (selectedModelMeta?.requiresImage) {
params.image_url = rawParams.image_url;
}
if (selectedModelMeta?.requiresMask) {
params.mask_url = rawParams.mask_url;
}

// 保存当前参数
currentImageParams = {...params};

// 前端必填校验(依据模型元信息)
if (selectedModelMeta && selectedModelMeta.requiresImage) {
if (!rawParams.image_url) {
showStatus('该模型需要提供输入图像URL', 'error');
loadingOverlay.classList.add('hidden');
initialPrompt.classList.remove('hidden');
return;
}
}
if (selectedModelMeta && selectedModelMeta.requiresMask) {
if (!rawParams.mask_url) {
showStatus('局部重绘模型需要提供遮罩URL', 'error');
loadingOverlay.classList.add('hidden');
initialPrompt.classList.remove('hidden');
return;
}
}

try {
// 发送请求
const startTime = performance.now();

// 启动拟真进度条
let progress = 0;
if (progressBarContainer && progressBar && progressText) {
progressBarContainer.classList.remove('hidden');
progress = 0;
progressBar.style.width = '0%';
progressText.textContent = '0%';
if (progressExtra) progressExtra.textContent = '估算';
const steps = params.num_steps || 20;
const baseMs = Math.max(4000, Math.min(20000, steps * 600));
const startTs = Date.now();
progressTimer = setInterval(() => {
const elapsed = Date.now() - startTs;
const ratio = Math.min(0.95, elapsed / baseMs); // 最高到95%,等待真实完成再封顶
const cur = Math.floor(ratio * 100);
if (cur > progress) {
progress = cur;
progressBar.style.width = `${progress}%`;
progressText.textContent = `${progress}%`;
}
}, 150);
}

// 60秒超时控制
pendingController = new AbortController();
const timeoutId = setTimeout(() => pendingController && pendingController.abort('timeout'), 60000);

const response = await fetch('/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'image/*'
},
body: JSON.stringify(params),
signal: pendingController.signal
});
clearTimeout(timeoutId);

if (!response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const errorData = await response.json();
const msg = errorData.error || errorData.message || '生成失败';
const details = errorData.details ? `(${errorData.details})` : '';
throw new Error(`${msg}${details}`);
} else {
const errorText = await response.text();
console.error('服务器错误:', errorText);
throw new Error('生成失败');
}
}

const respType = response.headers.get('content-type') || '';
const serverSecondsHeader = response.headers.get('X-Server-Seconds');
let serverSeconds = serverSecondsHeader ? parseFloat(serverSecondsHeader) : null;

let base64Image = '';
let imageBlob = null;
let imagesArray = null;

if (respType.includes('application/json')) {
const json = await response.json();
if (Array.isArray(json.images)) {
imagesArray = json.images;
base64Image = imagesArray[0];
} else {
throw new Error('响应格式错误');
}
} else {
imageBlob = await response.blob();
base64Image = await blobToBase64(imageBlob);
}
const endTime = performance.now();
const generationTime = ((endTime - startTime) / 1000).toFixed(2);

// 设置图像信息并显示图像
const gallery = document.getElementById('imageGallery');
gallery.innerHTML = '';
if (imagesArray && imagesArray.length > 1) {
aiImage.classList.add('hidden');
gallery.classList.remove('hidden');
currentImagesArray = imagesArray.slice();
imagesArray.forEach((src, idx) => {
const wrap = document.createElement('div');
wrap.className = 'relative group';

const img = document.createElement('img');
img.src = src;
img.alt = `生成的图像 ${idx+1}`;
img.className = 'w-full h-auto rounded';
img.style.cursor = 'zoom-in';
img.addEventListener('click', () => openImageModal(src));

// 悬浮操作条
const bar = document.createElement('div');
bar.className = 'absolute bottom-1 left-1 right-1 hidden group-hover:flex bg-black bg-opacity-50 text-white text-xs rounded px-1 py-0.5 gap-1 justify-center';

const btnZoom = document.createElement('button');
btnZoom.innerHTML = '<i class="fa-solid fa-magnifying-glass-plus"></i>';
btnZoom.title = '放大';
btnZoom.onclick = (e) => { e.stopPropagation(); openImageModal(src); };

const btnCopy = document.createElement('button');
btnCopy.innerHTML = '<i class="fa-solid fa-copy"></i>';
btnCopy.title = '复制到剪贴板';
btnCopy.onclick = async (e) => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(src);
showStatus('图片已复制到剪贴板', 'success');
} catch (_) {
showStatus('复制失败', 'error');
}
};

const btnDl = document.createElement('button');
btnDl.innerHTML = '<i class="fa-solid fa-download"></i>';
btnDl.title = '下载此图';
btnDl.onclick = async (e) => {
e.stopPropagation();
try {
const response = await fetch(src);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
const model = document.getElementById('usedModel').textContent || 'ai-image';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
link.href = url;
link.download = `${model}-${timestamp}-${idx+1}.png`;
document.body.appendChild(link); link.click(); document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (_) {
showStatus('下载失败', 'error');
}
};

bar.appendChild(btnZoom);
bar.appendChild(btnCopy);
bar.appendChild(btnDl);

wrap.appendChild(img);
wrap.appendChild(bar);
gallery.appendChild(wrap);
});
} else {
gallery.classList.add('hidden');
currentImagesArray = null;
aiImage.src = base64Image;
}

// 统一的UI完成逻辑(不依赖 onload 也能执行一次)
const finalize = () => {
// 图像加载完成后更新UI
loadingOverlay.classList.add('hidden');
if (!imagesArray || imagesArray.length <= 1) {
aiImage.classList.remove('hidden');
}

// 安全地更新信息显示
const elements = {
generationTime: document.getElementById('generationTime'),
usedModel: document.getElementById('usedModel'),
computeInfo: document.getElementById('computeInfo'),
imageSize: document.getElementById('imageSize')
};

if (elements.generationTime) {
elements.generationTime.textContent = `${generationTime}秒`;
}
if (elements.usedModel) {
elements.usedModel.textContent = getModelNameById(params.model);
}

// 从响应头提取信息
const usedModelHeader = response.headers.get('X-Used-Model');
const usedModelName = usedModelHeader ? getModelNameById(usedModelHeader) : getModelNameById(params.model);
if (elements.usedModel) elements.usedModel.textContent = usedModelName;

const bytesStr = response.headers.get('X-Image-Bytes');
if (elements.imageSize) {
if (bytesStr) {
const bytes = parseInt(bytesStr, 10);
elements.imageSize.textContent = formatBytes(bytes);
} else if (imageBlob) {
// 回退:用blob大小
elements.imageSize.textContent = formatBytes(imageBlob.size);
} else if (imagesArray && imagesArray[0]) {
elements.imageSize.textContent = formatBytes(dataURLBytes(imagesArray[0]));
}
}

// 计算真实 it/s:迭代步数 / 实际耗时
const itPerSec = (() => {
const steps = Number(params.num_steps) || 0;
const seconds = serverSeconds && serverSeconds > 0 ? serverSeconds : (parseFloat(generationTime) || 0);
if (steps > 0 && seconds > 0) {
return (steps / seconds).toFixed(2);
}
return '-';
})();
if (elements.computeInfo) {
elements.computeInfo.textContent = `${itPerSec}`;
}

// 更新所有参数面板(隐藏不相关字段)
updateParamsDisplay(params);

// 填充每张图片的尺寸与大小
const metaPanel = document.getElementById('imageMeta');
const metaBody = document.getElementById('imageMetaBody');
if (imagesArray && imagesArray.length > 0 && metaPanel && metaBody) {
metaBody.innerHTML = '';
const loadPromises = imagesArray.map((src, i) => new Promise((resolve) => {
const probe = new Image();
probe.onload = () => {
const tr = document.createElement('tr');
const sizeBytes = dataURLBytes(src);
tr.innerHTML = `<td class=\"py-1 pr-3\">${i+1}</td><td class=\"py-1 pr-3\">${probe.width}×${probe.height}</td><td class=\"py-1\">${formatBytes(sizeBytes)}</td>`;
metaBody.appendChild(tr);
resolve();
};
probe.onerror = () => resolve();
probe.src = src;
}));
Promise.all(loadPromises).then(() => metaPanel.classList.remove('hidden'));
} else {
const panel = document.getElementById('imageMeta');
if (panel) panel.classList.add('hidden');
}

// 进度条收口到100%
if (progressBarContainer && progressBar && progressText) {
if (progressTimer) clearInterval(progressTimer);
progressBar.style.width = '100%';
progressText.textContent = '100%';
setTimeout(() => progressBarContainer.classList.add('hidden'), 800);
}

// 显示状态和操作按钮
showStatus(imagesArray ? `生成成功(${imagesArray.length} 张)` : '生成成功', 'success');
if (copyParamsButton) copyParamsButton.classList.remove('hidden');
if (downloadButton) downloadButton.classList.remove('hidden');
const downloadZipButton = document.getElementById('downloadZipButton');
if (downloadZipButton) {
if (imagesArray && imagesArray.length > 1) downloadZipButton.classList.remove('hidden');
else downloadZipButton.classList.add('hidden');
}
};

// 触发 finalize
if (imagesArray && imagesArray.length > 1) {
finalize();
} else {
aiImage.onload = finalize;
}

} catch (error) {
console.error('生成图像错误:', error);
if (error && (error.name === 'AbortError' || error.message === 'timeout')) {
showStatus('生成超时,请尝试更换模型或降低宽高后重试', 'error');
} else {
showStatus(error.message || '生成失败', 'error');
}
// 显示初始提示
initialPrompt.classList.remove('hidden');
aiImage.classList.add('hidden');
} finally {
loadingOverlay.classList.add('hidden');
if (progressTimer) { clearInterval(progressTimer); progressTimer = null; }
if (progressBarContainer) progressBarContainer.classList.add('hidden');
pendingController = null;
}
});

// 将Blob转换为Base64
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}

// 字节转可读字符串
function formatBytes(bytes) {
if (!bytes && bytes !== 0) return '-';
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const value = (bytes / Math.pow(1024, i)).toFixed(2);
return `${value} ${sizes[i]}`;
}

// 计算 dataURL 的字节大小(粗略)
function dataURLBytes(dataURL) {
try {
const base64 = dataURL.split(',')[1] || '';
// base64 长度 * 3/4 约等于字节数
return Math.floor((base64.length * 3) / 4);
} catch (_) {
return 0;
}
}

// 简易图片放大预览
function openImageModal(src) {
const existing = document.getElementById('imgModal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.id = 'imgModal';
modal.style.position = 'fixed';
modal.style.inset = '0';
modal.style.background = 'rgba(0,0,0,0.7)';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
modal.style.zIndex = '1000';
modal.innerHTML = `
<div class="max-w-5xl max-h-[90vh] p-2">
<img src="${src}" class="rounded shadow-lg max-h-[85vh] mx-auto" />
<div class="text-center mt-2">
<button id="closeImgModal" class="btn btn-secondary">关闭</button>
</div>
</div>`;
document.body.appendChild(modal);
document.getElementById('closeImgModal').onclick = () => modal.remove();
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
}

// 通过ID获取模型名称
function getModelNameById(id) {
const model = availableModels.find(m => m.id === id);
return model ? model.name : id;
}

// 更新参数显示
function updateParamsDisplay(params) {
const allParamsContainer = document.getElementById('allParamsContainer');
const allParamsElement = document.getElementById('allParams');

if (!allParamsContainer || !allParamsElement) return;

// 清空现有参数
allParamsElement.innerHTML = '';

// 添加新参数
for (const [key, value] of Object.entries(params)) {
if (key === 'password') continue; // 不显示密码

const paramName = formatParamName(key);
const paramValue = value;

// 创建参数徽章
const badge = document.createElement('div');
badge.className = 'param-badge';
badge.innerHTML = `<span class="font-medium">${paramName}:</span> ${paramValue}`;

allParamsElement.appendChild(badge);
}

// 显示参数容器
allParamsContainer.classList.remove('hidden');
}

// 显示状态提示
function showStatus(message, type = 'info') {
const statusElement = document.getElementById('imageStatus');
if (!statusElement) return;

// 设置样式
statusElement.className = '';
switch (type) {
case 'success':
statusElement.classList.add('bg-green-100', 'text-green-800', 'dark:bg-green-900', 'dark:text-green-100');
break;
case 'error':
statusElement.classList.add('bg-red-100', 'text-red-800', 'dark:bg-red-900', 'dark:text-red-100');
break;
case 'warning':
statusElement.classList.add('bg-yellow-100', 'text-yellow-800', 'dark:bg-yellow-900', 'dark:text-yellow-100');
break;
default:
statusElement.classList.add('bg-blue-100', 'text-blue-800', 'dark:bg-blue-900', 'dark:text-blue-100');
}

// 设置消息
statusElement.textContent = message;

// 显示
statusElement.classList.remove('hidden');

// 5秒后自动隐藏
setTimeout(() => {
statusElement.classList.add('hidden');
}, 5000);
}
});

// 登录遮罩及逻辑
function showLogin() {
if (document.getElementById('loginOverlay')) return;
const overlay = document.createElement('div');
overlay.id = 'loginOverlay';
overlay.style.position = 'fixed';
overlay.style.inset = '0';
overlay.style.backdropFilter = 'blur(3px)';
overlay.style.background = 'rgba(0,0,0,0.4)';
overlay.style.zIndex = '1000';
overlay.innerHTML = `
<div class="w-full h-full flex items-center justify-center">
<div class="card p-6 w-96 bg-white dark:bg-gray-800">
<h3 class="text-lg font-semibold mb-4">请输入访问密码</h3>
<input type="password" id="loginPassword" class="w-full" placeholder="访问密码" />
<button id="loginButton" class="btn btn-primary w-full mt-4">登录</button>
<p id="loginError" class="text-red-500 text-sm mt-2 hidden">密码错误</p>
</div>
</div>`;
document.body.appendChild(overlay);
document.getElementById('loginPassword').focus();
const submitLogin = async () => {
const pwd = (document.getElementById('loginPassword').value || '').trim();
try {
const resp = await fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pwd })
});
if (!resp.ok) {
document.getElementById('loginError').classList.remove('hidden');
return;
}
overlay.remove();
} catch (_) {
document.getElementById('loginError').classList.remove('hidden');
}
};
document.getElementById('loginButton').addEventListener('click', submitLogin);
document.getElementById('loginPassword').addEventListener('keydown', (e) => {
if (e.key === 'Enter') submitLogin();
});
}
</script>
</body>
</html>
7. **保存部署**:点击右上角的蓝色 **“部署”** 按钮。

第三阶段:资源绑定(关键)

  1. 进入设置:返回 Workers 项目主页,点击 设置 (Settings) -> 变量 (Variables)
  2. 添加 AI 绑定
    • 在下方找到 “Workers AI 绑定” 部分,点击添加绑定。
    • 名称通常保持默认,保存即可。

第四阶段:域名接入

  1. 添加自定义域:在项目页点击 设置 -> 触发器 (Triggers) -> 添加自定义域
  2. 填写域名:输入您的域名(例如 ai.138gl.com),点击添加。
  3. 解析生效:等待 Cloudflare 完成 DNS 解析后,即可通过该域名直接访问并开始 AI 绘图!

💡 使用小贴士

  • 登录方式:访问域名后,输入您在第 5 步设置的密码即可登录。
  • 免费额度:Cloudflare Workers AI 目前有每日免费额度限制,建议个人学习使用,请勿滥用。

[!TIP]
如果部署过程中遇到 Environment Variable 相关错误,请检查第 9 步的 AI 绑定是否成功完成。


💬 留言反馈
如果在部署过程中遇到任何报错,欢迎在下方评论区贴出错误信息,我会协助您解决!