喂饭时间到,今天继续来给大家喂饭,让我们简单地糊一个自己的Flux网站,然后分享给你的朋友们。
效果如下:
需要材料如下:
-
siliconflow API key,账号自己注册,aff我都不贴了,你自己去搞定就行,反正现在siliconflow的flux还能白嫖。
-
一个Cloudflare账号。
-
一个托管在Cloudflare的域名。
-
一个还能用的键盘(Ctrl+C, Ctrl+V)。
-
没了吧?应该…
首先获取siliconflow API key,记下来。
然后打开Cloudflare,登录,开始:
创建一个worker,
名字自己改,点击部署。
然后选择“继续处理项目”。
接下来设置环境变量:
点击“设置”—“变量”—“添加变量”。
添加以下变量:
-
API_KEY: 你自己的siliconflow API key。
-
API_URL: https://api.siliconflow.cn/v1/black-forest-labs/FLUX.1-schnell/text-to-image。
-
AUTH_USERNAME: 你设置的用户名。
-
AUTH_PASSWORD: 你想设置的授权密码。
-
TURNSTILE_SITE_KEY: 留空,等一下来填。
-
TURNSTILE_SECRET_KEY: 留空,等一下来填。
然后点击“部署”。
接着点击“触发器”—“添加自定义域”。
输入你想要使用的域名(用来访问flux服务),这个域名必须是在Cloudflare解析的,比如你有一个在Cloudflare解析的域名是woshiqinshiwang.com, 你可以填入flux.woshiqinshiwang.com,Cloudflare会帮你搞定剩下的事情,包括证书。完事了记得点击“添加自定义域”完成部署。
下一步我们来设置一下Turnstile,如果你之前已经为想要使用的域名设置了Turnstile,则可以跳过这一步。
点击 “Turnstile”—“添加站点”。
进入后:
在1中填写你喜欢的名字,随便啦~~
在2中选择你想要使用的域名,记得从点击后出现的下拉菜单中选择,比如上面你添加的自定义域“flux.woshiqinshiwang.com”,那么你就应该在下拉菜单中选择“woshiqinshiwang.com”。
点3部署。
然后你会看到两个key,分别是“站点密钥”跟“密钥”,copy下来,复制到上面对应的变量中。
-
TURNSTILE_SITE_KEY: 对应“站点密钥”。
-
TURNSTILE_SECRET_KEY: 对应“密钥”。
完事了记得点击“部署”。
如果你忘记copy那两个key,可以点击 “Turnstile”—找到你刚才设置的域名点击“settings”就可以看到啦。
最后也是最简单的一步:
- 点击 “workers和pages”—点击“概述”–选择你刚创建的worker。
- 点击“编辑代码”。
- 进入编辑页面,删除原来所有的代码,复制粘贴下面的代码。
// Function to generate a secure random token
function generateToken() {
return crypto.randomUUID();
}
// Function to verify Turnstile token
async function verifyTurnstileToken(token) {
const formData = new FormData();
formData.append('secret', TURNSTILE_SECRET_KEY);
formData.append('response', token);
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const result = await fetch(url, {
body: formData,
method: 'POST',
});
const outcome = await result.json();
return outcome.success;
}
// Function to set a secure cookie
function setCookie(name, value, maxAge = 3600) {
return `${name}=${value}; HttpOnly; Secure; SameSite=Strict; Max-Age=${maxAge}; Path=/`;
}
// Function to get cookie value
function getCookie(request, name) {
const cookieString = request.headers.get('Cookie') || '';
const cookies = cookieString.split(';').map(cookie => cookie.trim());
const targetCookie = cookies.find(cookie => cookie.startsWith(`${name}=`));
return targetCookie ? targetCookie.split('=')[1] : null;
}
// Middleware to check authentication
async function checkAuth(request) {
const sessionToken = getCookie(request, 'session');
if (!sessionToken) return false;
try {
const [username, expirationTime] = atob(sessionToken).split(':');
if (Date.now() > parseInt(expirationTime)) return false;
return username === AUTH_USERNAME;
} catch {
return false;
}
}
// Handle login request
async function handleLogin(request) {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const formData = await request.formData();
const username = formData.get('username');
const password = formData.get('password');
const turnstileResponse = formData.get('cf-turnstile-response');
if (!await verifyTurnstileToken(turnstileResponse)) {
return new Response('Turnstile verification failed', { status: 400 });
}
if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
const expirationTime = Date.now() + 3600000; // 1 hour from now
const sessionToken = btoa(`${username}:${expirationTime}`);
const headers = new Headers();
headers.append('Set-Cookie', setCookie('session', sessionToken));
headers.append('Location', '/');
return new Response(null, { status: 302, headers });
} else {
return new Response('Invalid credentials', { status: 401 });
}
}
// Handle logout request
function handleLogout() {
const headers = new Headers();
headers.append('Set-Cookie', setCookie('session', '', 0)); // Expire the cookie
headers.append('Location', '/login');
return new Response(null, { status: 302, headers });
}
// Generate image
async function generateImage(request) {
try {
const { prompt, width, height, num_inference_steps } = await request.json();
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({ prompt, width, height, num_inference_steps })
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
return response;
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
// Get login page HTML
function getLoginHTML() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Flux.1-schnell Generator</title>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<style>
body {
font-family: 'Roboto', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f4f4f4;
}
form {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
}
h2 {
margin-top: 0;
color: #333;
text-align: center;
}
input {
display: block;
width: 100%;
padding: 0.75rem;
margin: 1rem 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
}
button {
width: 100%;
padding: 0.75rem;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #2980b9;
}
</style>
</head>
<body>
<form method="POST" action="/login">
<h2>Login</h2>
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<div class="cf-turnstile" data-sitekey="${TURNSTILE_SITE_KEY}"></div>
<button type="submit">Login</button>
</form>
</body>
</html>
`;
}
// Get main page HTML
function getMainHTML() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flux.1-schnell Generator</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
:root {
--background-color: #f4f4f4;
--card-background: #ffffff;
--text-color: #333333;
--primary-color: #3498db;
--primary-hover: #2980b9;
--error-color: #e74c3c;
}
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
width: 100%;
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background-color: var(--card-background);
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
color: var(--text-color);
text-align: center;
margin-bottom: 1.5rem;
}
.image-container {
margin-bottom: 2rem;
text-align: center;
background-color: var(--background-color);
padding: 2rem;
border-radius: 8px;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.image-container img {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
input[type="text"] {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.image-ratio-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 1rem;
margin-top: 0.5rem;
}
.image-ratio-option input[type="radio"] {
display: none;
}
.image-ratio-option label {
display: block;
padding: 0.5rem;
text-align: center;
background-color: var(--background-color);
border: 2px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.image-ratio-option input[type="radio"]:checked + label {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.generate-btn {
display: block;
width: 100%;
padding: 1rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.generate-btn:hover {
background-color: var(--primary-hover);
}
.generate-btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.logout-btn {
display: inline-block;
margin-top: 2rem;
padding: 0.8rem 1.5rem;
background-color: var(--error-color);
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.logout-btn:hover {
background-color: #c0392b;
}
#error-message {
color: var(--error-color);
text-align: center;
margin-top: 1rem;
}
.loading {
font-style: italic;
color: #7f8c8d;
}
</style>
</head>
<body>
<div class="container">
<h1>Flux.1-schnell Generator</h1>
<div class="image-container" id="image-container">
<p>Your generated image will appear here</p>
</div>
<form id="image-form">
<div class="form-group">
<label for="prompt">Describe your image:</label>
<input type="text" id="prompt" name="prompt" placeholder="Enter your image description" required>
</div>
<div class="form-group">
<label>Select image ratio:</label>
<div class="image-ratio-group">
<div class="image-ratio-option">
<input type="radio" id="ratio-1-1" name="image_ratio" value="1:1" checked>
<label for="ratio-1-1">1:1</label>
</div>
<div class="image-ratio-option">
<input type="radio" id="ratio-1-2" name="image_ratio" value="1:2">
<label for="ratio-1-2">1:2</label>
</div>
<div class="image-ratio-option">
<input type="radio" id="ratio-3-2" name="image_ratio" value="3:2">
<label for="ratio-3-2">3:2</label>
</div>
<div class="image-ratio-option">
<input type="radio" id="ratio-3-4" name="image_ratio" value="3:4">
<label for="ratio-3-4">3:4</label>
</div>
<div class="image-ratio-option">
<input type="radio" id="ratio-16-9" name="image_ratio" value="16:9">
<label for="ratio-16-9">16:9</label>
</div>
<div class="image-ratio-option">
<input type="radio" id="ratio-9-16" name="image_ratio" value="9:16">
<label for="ratio-9-16">9:16</label>
</div>
</div>
</div>
<button type="submit" class="generate-btn">Generate Image</button>
</form>
<div id="error-message"></div>
<a href="/logout" class="logout-btn">Logout</a>
</div>
<script>
document.getElementById('image-form').addEventListener('submit', async function(event) {
event.preventDefault();
const prompt = document.getElementById('prompt').value;
const imageRatio = document.querySelector('input[name="image_ratio"]:checked').value;
const imageContainer = document.getElementById('image-container');
const errorMessage = document.getElementById('error-message');
const generateButton = document.querySelector('.generate-btn');
const numInferenceSteps = 20;
updateUI('generating');
const { width, height } = getImageDimensions(imageRatio);
try {
const response = await fetch('/generate-image', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
width,
height,
num_inference_steps: numInferenceSteps
})
});
if (!response.ok) {
throw new Error('Failed to generate image. Please try again.');
}
const data = await response.json();
if (data.images && data.images.length > 0) {const imageUrl = data.images[0].url;
imageContainer.innerHTML = '<img src="' + imageUrl + '" alt="Generated Image">';
} else {
throw new Error('Failed to generate image. Please try again.');
}
} catch (error) {
console.error('Error:', error);
errorMessage.textContent = error.message;
imageContainer.innerHTML = '<p>Failed to generate image</p>';
} finally {
updateUI('ready');
}
});
function getImageDimensions(ratio) {
const dimensions = {
'1:1': { width: 1024, height: 1024 },
'1:2': { width: 512, height: 1024 },
'3:2': { width: 768, height: 512 },
'3:4': { width: 768, height: 1024 },
'16:9': { width: 1024, height: 576 },
'9:16': { width: 576, height: 1024 }
};
return dimensions[ratio] || dimensions['1:1'];
}
function updateUI(state) {
const imageContainer = document.getElementById('image-container');
const errorMessage = document.getElementById('error-message');
const generateButton = document.querySelector('.generate-btn');
if (state === 'generating') {
imageContainer.innerHTML = '<p class="loading">Generating your image... This may take a moment.</p>';
errorMessage.textContent = '';
generateButton.disabled = true;
generateButton.textContent = 'Generating...';
} else {
generateButton.disabled = false;
generateButton.textContent = 'Generate Image';
}
}
</script>
</body>
</html>
`;
}
// Main request handler
async function handleRequest(request) {
const url = new URL(request.url);
// Login route
if (url.pathname === '/login') {
if (request.method === 'GET') {
return new Response(getLoginHTML(), {
headers: { 'Content-Type': 'text/html' },
});
} else if (request.method === 'POST') {
return handleLogin(request);
}
}
// Logout route
if (url.pathname === '/logout') {
return handleLogout();
}
// Check authentication for all other routes
const isAuthenticated = await checkAuth(request);
if (!isAuthenticated) {
const headers = new Headers();
headers.append('Location', '/login');
return new Response(null, { status: 302, headers });
}
// Generate image route
if (request.method === 'POST' && url.pathname === '/generate-image') {
return generateImage(request);
}
// Main page route
if (request.method === 'GET' && url.pathname === '/') {
return new Response(getMainHTML(), {
headers: { 'Content-Type': 'text/html' },
});
}
// 404 for all other routes
return new Response('Not Found', { status: 404 });
}
// Event listener
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
- 完事记得点击“部署”。
好了,去爽吧。
这个教程只适合小白,大佬们见笑了,如果你觉得对你有帮助,请给我一个赞吧。