├── .eslintrc.cjs ├── .gitignore ├── README.md ├── app ├── config.ts ├── context │ └── index.ts ├── entry.client.tsx ├── entry.server.tsx ├── middleware │ ├── auth.ts │ └── cors.ts ├── root.tsx ├── routes │ ├── _index.tsx │ ├── api.generate-image.tsx │ ├── api.image.tsx │ ├── api.models.tsx │ ├── generate-image.tsx │ ├── getme.tsx │ └── idioms │ │ ├── api.receive-data.ts │ │ └── game.tsx ├── services │ └── imageGeneration.ts ├── tailwind.css ├── types.d.ts └── utils │ └── error.ts ├── functions └── [[path]].ts ├── load-context.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── .assetsignore ├── _headers ├── _routes.json ├── favicon.ico ├── logo-dark.png └── logo-light.png ├── tailwind.config.ts ├── tsconfig.json ├── vite.config.ts ├── worker-configuration.d.ts └── wrangler.toml /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is intended to be a basic starting point for linting in your app. 3 | * It relies on recommended configs out of the box for simplicity, but you can 4 | * and should modify this configuration to best suit your team's needs. 5 | */ 6 | 7 | /** @type {import('eslint').Linter.Config} */ 8 | module.exports = { 9 | root: true, 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | env: { 18 | browser: true, 19 | commonjs: true, 20 | es6: true, 21 | }, 22 | ignorePatterns: ["!**/.server", "!**/.client"], 23 | 24 | // Base config 25 | extends: ["eslint:recommended"], 26 | 27 | overrides: [ 28 | // React 29 | { 30 | files: ["**/*.{js,jsx,ts,tsx}"], 31 | plugins: ["react", "jsx-a11y"], 32 | extends: [ 33 | "plugin:react/recommended", 34 | "plugin:react/jsx-runtime", 35 | "plugin:react-hooks/recommended", 36 | "plugin:jsx-a11y/recommended", 37 | ], 38 | settings: { 39 | react: { 40 | version: "detect", 41 | }, 42 | formComponents: ["Form"], 43 | linkComponents: [ 44 | { name: "Link", linkAttribute: "to" }, 45 | { name: "NavLink", linkAttribute: "to" }, 46 | ], 47 | "import/resolver": { 48 | typescript: {}, 49 | }, 50 | }, 51 | }, 52 | 53 | // Typescript 54 | { 55 | files: ["**/*.{ts,tsx}"], 56 | plugins: ["@typescript-eslint", "import"], 57 | parser: "@typescript-eslint/parser", 58 | settings: { 59 | "import/internal-regex": "^~/", 60 | "import/resolver": { 61 | node: { 62 | extensions: [".ts", ".tsx"], 63 | }, 64 | typescript: { 65 | alwaysTryTypes: true, 66 | }, 67 | }, 68 | }, 69 | extends: [ 70 | "plugin:@typescript-eslint/recommended", 71 | "plugin:import/recommended", 72 | "plugin:import/typescript", 73 | ], 74 | }, 75 | 76 | // Node 77 | { 78 | files: [".eslintrc.cjs"], 79 | env: { 80 | node: true, 81 | }, 82 | }, 83 | ], 84 | }; 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | .dev.vars 7 | 8 | .wrangler 9 | 10 | old-workers-app.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CF Flux Remix 2 | 3 | CF Flux Remix 是一个基于 Cloudflare Workers 和 Remix 框架的图像生成应用。它利用 Cloudflare 的 AI 模型来生成图像,并提供了一个用户友好的界面和 API 接口来与这些模型进行交互。 4 | 5 | ## 功能特点 6 | 7 | - 使用 Cloudflare 的 AI 模型生成图像【免费】 8 | - 支持多种图像生成模型,包括 Flux 和标准模型 9 | - 提供 API 接口以便集成到其他应用中 10 | - 支持提示词翻译和优化 11 | - 一键部署 12 | - 响应式设计,现代设计 13 | - 图片生成不受限制(不经审查),你懂的 14 | 15 | ## 快速开始 16 | 17 | ### 前置条件 18 | 19 | - CloudFlare账号 20 | - Github/Gitlab账号 21 | 22 | ## 安装 23 | ### [视频教程](https://www.bilibili.com/video/BV1Wz2NYyEmW/) 24 | 25 | 1. 克隆(Fork)仓库: 26 | ```bash 27 | https://github.com/yourusername/cf-flux-remix 28 | ``` 29 | 30 | 2. 完成部署: 31 | 在CloudFlare中操作 32 | ```bash 33 | 1、新建一个worker 34 | 名称为 free-flux . 注意此名称必须与Github中的Wrangler.toml文件中的名称一致。 35 | 36 | 2、worker后台设置中绑定Github仓库 37 | 绑定Fork的本仓库 38 | 39 | 3、填入构建命令等 40 | 构建命令(可选): pnpm i 41 | 部署命令: pnpm run deploy 42 | 43 | 4、触发CF Workers中部署 44 | 随便改动一下仓库readme文件,提交后自动触发部署 45 | 46 | 5、部署完成 47 | 部署完成后打开相应网站来使用,API使用看下面的说明 48 | 记得在worker后台设置环境变量,替换自己的CF账号ID及API令牌 49 | API令牌要有Workers AI 的读取及编辑权限。 50 | 51 | ``` 52 | 53 | ### 开发 54 | 55 | 运行开发服务器: 56 | ``` 57 | pnpm run dev 58 | ``` 59 | 60 | ### 构建和部署 61 | 62 | 1. 另一种方法部署到 Cloudflare Workers: 63 | ``` 64 | pnpm run deploy 65 | ``` 66 | 67 | ## 环境变量 68 | 69 | 在 `wrangler.toml` 文件中设置以下程序变量: 70 | 71 | - `API_KEY`: API 密钥,用于身份验证 72 | - `CF_ACCOUNT_LIST`: Cloudflare 账户列表,JSON 格式 73 | - `CF_TRANSLATE_MODEL`: 翻译模型 ID 74 | - `CF_IS_TRANSLATE`: 是否启用翻译功能 75 | - `USE_EXTERNAL_API`: 是否使用外部 API 76 | - `EXTERNAL_API`: 外部 API 地址 77 | - `EXTERNAL_MODEL`: 外部模型 ID 78 | - `EXTERNAL_API_KEY`: 外部 API 密钥 79 | - `FLUX_NUM_STEPS`: Flux 模型的步数 80 | - `CUSTOMER_MODEL_MAP`: 模型映射,JSON 格式 81 | 82 | ## API 文档 83 | 84 | ### 生成图像 85 | 86 | - 端点:`/api/image` 87 | - 方法:POST 88 | - 请求头: 89 | - `Authorization: Bearer your_api_key_here` 90 | - `Content-Type: application/json` 91 | - 请求体: 92 | ```json 93 | { 94 | "messages": [{"role": "user", "content": "图像描述"}], 95 | "model": "模型ID", 96 | "stream": false 97 | } 98 | ``` 99 | - 响应: 100 | ```json 101 | { 102 | "prompt": "原始提示词", 103 | "translatedPrompt": "翻译后的提示词", 104 | "image": "生成的图像数据(Base64编码)" 105 | } 106 | ``` 107 | 108 | ### 获取可用模型 109 | 110 | - 端点:`/api/models` 111 | - 方法:GET 112 | - 请求头(可选): 113 | - `Authorization: Bearer your_api_key_here` 114 | - 响应: 115 | ```json 116 | { 117 | "models": [ 118 | {"id": "DS-8-CF", "name": "DreamShaper 8"}, 119 | {"id": "SD-XL-Bash-CF", "name": "Stable Diffusion XL Base"}, 120 | {"id": "SD-XL-Lightning-CF", "name": "Stable Diffusion XL Lightning"}, 121 | {"id": "FLUX.1-Schnell-CF", "name": "Flux 1 Schnell"} 122 | ] 123 | } 124 | ``` 125 | 126 | ## 使用示例 127 | 128 | ### 使用 cURL 生成图像 129 | ``` 130 | bash 131 | curl -X POST https://your-worker-url.workers.dev/api/image \ 132 | -H "Authorization: Bearer your_api_key_here" \ 133 | -H "Content-Type: application/json" \ 134 | -d '{ 135 | "messages": [{"role": "user", "content": "一只可爱的猫咪"}], 136 | "model": "DS-8-CF" 137 | }' 138 | ``` 139 | 140 | ### 使用 Python 请求 API 141 | ``` 142 | python 143 | import requests 144 | import json 145 | url = "https://your-worker-url.workers.dev/api/image" 146 | headers = { 147 | "Authorization": "Bearer your_api_key_here", 148 | "Content-Type": "application/json" 149 | } 150 | data = { 151 | "messages": [{"role": "user", "content": "一只可爱的猫咪"}], 152 | "model": "DS-8-CF" 153 | } 154 | response = requests.post(url, headers=headers, data=json.dumps(data)) 155 | result = response.json() 156 | print(f"原始提示词: {result['prompt']}") 157 | print(f"翻译后的提示词: {result['translatedPrompt']}") 158 | print(f"生成的图像数据: {result['image'][:50]}...") # 只打印前50个字符 159 | ``` 160 | 161 | ## 贡献 162 | 163 | 欢迎提交 Pull Requests 来改进这个项目。对于重大更改,请先开 issue 讨论您想要改变的内容。 164 | 165 | ## 许可证 166 | 167 | 本项目采用 MIT 许可证。详情请见 [LICENSE](LICENSE) 文件。 168 | 169 | ## 常见问题 170 | 171 | 1. Q: 如何添加新的模型? 172 | A: 在 `wrangler.toml` 文件中的 `CUSTOMER_MODEL_MAP` 变量中添加新的模型 ID 和对应的 Cloudflare AI 模型路径。 173 | 174 | 2. Q: 如何禁用翻译功能? 175 | A: 在 `wrangler.toml` 文件中将 `CF_IS_TRANSLATE` 设置为 "false"。 176 | 177 | 3. Q: 如何调整 Flux 模型的步数? 178 | A: 修改 `wrangler.toml` 文件中的 `FLUX_NUM_STEPS` 值。 179 | 180 | ## 故障排除 181 | 182 | 如果遇到问题,请检查以下几点: 183 | 184 | 1. 确保所有环境变量都已正确设置。 185 | 2. 检查 Cloudflare 账户和 API 令牌是否有效。 186 | 3. 确保使用的模型 ID 在 `CUSTOMER_MODEL_MAP` 中存在。 187 | 4. 查看 Cloudflare Workers 的日志以获取更详细的错误信息。 188 | 5. The name in `wrangler.toml` must match the name of your Worker. 189 | 190 | 如果问题仍然存在,请开一个 issue 并提供详细的错误信息和复现步骤。 191 | 192 | ### [视频教程](https://www.bilibili.com/video/BV1Wz2NYyEmW/) -------------------------------------------------------------------------------- /app/config.ts: -------------------------------------------------------------------------------- 1 | type CustomerModelMap = { 2 | [key: string]: string; 3 | }; 4 | 5 | type CFAccount = { 6 | account_id: string; 7 | token: string; 8 | }; 9 | 10 | export type Config = { 11 | API_KEY: string; 12 | CF_ACCOUNT_LIST: { account_id: string; token: string }[]; 13 | CF_TRANSLATE_MODEL: string; 14 | CF_IS_TRANSLATE: boolean; 15 | USE_EXTERNAL_API: boolean; 16 | EXTERNAL_API: string; 17 | EXTERNAL_MODEL: string; 18 | EXTERNAL_API_KEY: string; 19 | FLUX_NUM_STEPS: number; 20 | CUSTOMER_MODEL_MAP: { [key: string]: string }; 21 | getme: string; 22 | }; 23 | 24 | export function getConfig(env: any): Config { 25 | return { 26 | API_KEY: env.API_KEY || "", 27 | CF_ACCOUNT_LIST: JSON.parse(env.CF_ACCOUNT_LIST || "[]"), 28 | CF_TRANSLATE_MODEL: env.CF_TRANSLATE_MODEL || "", 29 | CF_IS_TRANSLATE: env.CF_IS_TRANSLATE === "true", 30 | USE_EXTERNAL_API: env.USE_EXTERNAL_API === "true", 31 | EXTERNAL_API: env.EXTERNAL_API || "", 32 | EXTERNAL_MODEL: env.EXTERNAL_MODEL || "", 33 | EXTERNAL_API_KEY: env.EXTERNAL_API_KEY || "", 34 | FLUX_NUM_STEPS: parseInt(env.FLUX_NUM_STEPS || "6", 10), 35 | CUSTOMER_MODEL_MAP: JSON.parse(env.CUSTOMER_MODEL_MAP || "{}"), 36 | getme: env.getme || "", 37 | }; 38 | } -------------------------------------------------------------------------------- /app/context/index.ts: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext } from "@remix-run/cloudflare"; 2 | import { ImageGenerationService } from "../services/imageGeneration"; 3 | import { getConfig } from "../config"; 4 | 5 | export function createAppContext(context: AppLoadContext) { 6 | const config = getConfig(context.cloudflare.env); 7 | return { 8 | imageGenerationService: new ImageGenerationService(config), 9 | config, 10 | // 其他服务... 11 | }; 12 | } 13 | 14 | export type AppContext = ReturnType; -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | 7 | import type { EntryContext } from "@remix-run/cloudflare"; 8 | import { RemixServer } from "@remix-run/react"; 9 | import * as isbot from "isbot"; 10 | import { renderToReadableStream } from "react-dom/server"; 11 | // 移除 CONFIG 和 initConfig 的导入 12 | // import { CONFIG, initConfig } from "./config"; 13 | 14 | export default async function handleRequest( 15 | request: Request, 16 | responseStatusCode: number, 17 | responseHeaders: Headers, 18 | remixContext: EntryContext, 19 | loadContext: any 20 | ) { 21 | // 移除这些行 22 | // console.log("Initializing config in entry.server.tsx"); 23 | // initConfig(loadContext.cloudflare.env); 24 | // console.log("Config initialized:", JSON.stringify(CONFIG, null, 2)); 25 | 26 | let body; 27 | try { 28 | body = await renderToReadableStream( 29 | , 30 | { 31 | signal: request.signal, 32 | onError(error: unknown) { 33 | console.error("Rendering error:", error); 34 | responseStatusCode = 500; 35 | }, 36 | } 37 | ); 38 | } catch (error) { 39 | console.error("Failed to render to stream:", error); 40 | return new Response("Internal Server Error", { status: 500 }); 41 | } 42 | 43 | if (isbot.isbot(request.headers.get("user-agent"))) { 44 | try { 45 | await body.allReady; 46 | } catch (error) { 47 | console.error("Failed to wait for body to be ready:", error); 48 | return new Response("Internal Server Error", { status: 500 }); 49 | } 50 | } 51 | 52 | responseHeaders.set("Content-Type", "text/html"); 53 | return new Response(body, { 54 | headers: responseHeaders, 55 | status: responseStatusCode, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /app/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction, ActionFunction } from "@remix-run/cloudflare"; 2 | import { json } from "@remix-run/cloudflare"; 3 | import { getConfig } from "../config"; 4 | 5 | export function withAuth(fn: LoaderFunction | ActionFunction): LoaderFunction | ActionFunction { 6 | return async (args) => { 7 | const { request, context } = args; 8 | const config = getConfig(context.cloudflare.env); 9 | 10 | // 如果是 GET 请求,不进行身份验证 11 | if (request.method === "GET") { 12 | return fn(args); 13 | } 14 | 15 | const authHeader = request.headers.get("Authorization"); 16 | console.log("Auth header:", authHeader); 17 | console.log("config.API_KEY:", config.API_KEY); 18 | if (!authHeader || !authHeader.startsWith("Bearer ") || authHeader.split(" ")[1] !== config.API_KEY) { 19 | console.warn("Unauthorized access attempt. Expected API_KEY:", config.API_KEY); 20 | console.warn("Received API_KEY:", authHeader ? authHeader.split(" ")[1] : "No API key provided"); 21 | return json({ error: "Unauthorized" }, { status: 401 }); 22 | } 23 | return fn(args); 24 | }; 25 | } -------------------------------------------------------------------------------- /app/middleware/cors.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction, ActionFunction } from "@remix-run/cloudflare"; 2 | 3 | export function withCors(fn: LoaderFunction | ActionFunction): LoaderFunction | ActionFunction { 4 | return async (args) => { 5 | const response = await fn(args); 6 | if (response instanceof Response) { 7 | response.headers.set("Access-Control-Allow-Origin", "*"); 8 | response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); 9 | response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); 10 | } 11 | return response; 12 | }; 13 | } -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { LinksFunction } from "@remix-run/cloudflare"; 2 | import { 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "@remix-run/react"; 9 | 10 | import "./tailwind.css"; 11 | 12 | export const links: LinksFunction = () => [ 13 | // 移除 Google Fonts 相关链接 14 | ]; 15 | 16 | export function Layout({ children }: { children: React.ReactNode }) { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {children} 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default function App() { 35 | return ; 36 | } 37 | -------------------------------------------------------------------------------- /app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { json, type LoaderFunction } from "@remix-run/cloudflare"; 2 | import { useLoaderData } from "@remix-run/react"; 3 | import { createAppContext } from "../context"; 4 | import { AppError } from "../utils/error"; 5 | import { Link } from "@remix-run/react"; 6 | 7 | export const loader: LoaderFunction = async ({ context }) => { 8 | console.log("Loader started"); 9 | const appContext = createAppContext(context); 10 | const { imageGenerationService, config } = appContext; 11 | 12 | let cfAiStatus = "未连接"; 13 | let configStatus = { 14 | API_KEY: config.API_KEY ? "已设置" : "未设置", 15 | CF_TRANSLATE_MODEL: config.CF_TRANSLATE_MODEL, 16 | CF_ACCOUNT_LIST: config.CF_ACCOUNT_LIST.length > 0 ? "已设置" : "未设置", 17 | CUSTOMER_MODEL_MAP: Object.keys(config.CUSTOMER_MODEL_MAP).length > 0 ? "已设置" : "未设置", 18 | }; 19 | 20 | try { 21 | await imageGenerationService.testCfAiConnection(); 22 | cfAiStatus = "已连接"; 23 | } catch (error) { 24 | console.error("CF AI 连接测试失败:", error); 25 | cfAiStatus = error instanceof AppError ? `连接失败: ${error.message}` : "连接失败: 未知错误"; 26 | } 27 | 28 | console.log("Loader completed"); 29 | return json({ cfAiStatus, configStatus }); 30 | }; 31 | 32 | export default function Index() { 33 | return ( 34 |
35 |
36 |

CF Flux Remix

37 | 74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /app/routes/api.generate-image.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionFunction } from "@remix-run/cloudflare"; 2 | import { json } from "@remix-run/cloudflare"; 3 | import { createAppContext } from "../context"; 4 | import { withAuth } from "../middleware/auth"; 5 | import { withCors } from "../middleware/cors"; 6 | import { handleError } from "../utils/error"; 7 | 8 | export const action: ActionFunction = withCors(withAuth(async ({ request, context }) => { 9 | try { 10 | const appContext = createAppContext(context); 11 | const { imageGenerationService } = appContext; 12 | 13 | const data = await request.json(); 14 | const { prompt, model } = data; 15 | 16 | if (!prompt || !model) { 17 | return json({ error: "Missing prompt or model" }, { status: 400 }); 18 | } 19 | 20 | const result = await imageGenerationService.generateImage(prompt, model); 21 | 22 | return json(result); 23 | } catch (error) { 24 | return handleError(error); 25 | } 26 | })); -------------------------------------------------------------------------------- /app/routes/api.image.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionFunction, LoaderFunction } from "@remix-run/cloudflare"; 2 | import { json } from "@remix-run/cloudflare"; 3 | import { createAppContext } from "../context"; 4 | import { handleError } from "../utils/error"; 5 | import { withAuth } from "../middleware/auth"; 6 | import { withCors } from "../middleware/cors"; 7 | 8 | export const loader: LoaderFunction = () => { 9 | return json({ error: "此 API 端点仅支持 POST 请求" }, { status: 405 }); 10 | }; 11 | 12 | export const action: ActionFunction = withCors(withAuth(async ({ request, context }) => { 13 | const appContext = createAppContext(context); 14 | const { imageGenerationService, config } = appContext; 15 | 16 | console.log("API request received"); 17 | console.log("Config:", JSON.stringify(config, null, 2)); 18 | 19 | try { 20 | const data = await request.json(); 21 | const { messages, model: requestedModel, stream } = data; 22 | const userMessage = messages.find((msg: any) => msg.role === "user")?.content; 23 | 24 | if (!userMessage) { 25 | return json({ error: "未找到用户消息" }, { status: 400 }); 26 | } 27 | 28 | const modelId = requestedModel || Object.keys(config.CUSTOMER_MODEL_MAP)[0]; 29 | const model = config.CUSTOMER_MODEL_MAP[modelId]; 30 | 31 | if (!model) { 32 | return json({ error: "无效的模型" }, { status: 400 }); 33 | } 34 | 35 | const result = await imageGenerationService.generateImage(userMessage, model); 36 | 37 | return json(result); 38 | } catch (error) { 39 | return handleError(error); 40 | } 41 | })); -------------------------------------------------------------------------------- /app/routes/api.models.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/cloudflare"; 2 | import { json } from "@remix-run/cloudflare"; 3 | import { withAuth } from "../middleware/auth"; 4 | import { withCors } from "../middleware/cors"; 5 | import { createAppContext } from "../context"; 6 | 7 | export const loader: LoaderFunction = withCors(withAuth(async ({ context }) => { 8 | const appContext = createAppContext(context); 9 | const { config } = appContext; 10 | 11 | const models = Object.entries(config.CUSTOMER_MODEL_MAP).map(([id, path]) => ({ id, path })); 12 | 13 | return json({ models }); 14 | })); -------------------------------------------------------------------------------- /app/routes/generate-image.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ChangeEvent, FormEvent } from "react"; 2 | import { useState } from "react"; 3 | import { json } from "@remix-run/cloudflare"; 4 | import { useActionData, Form, useNavigation, useLoaderData } from "@remix-run/react"; 5 | import type { ActionFunction, LoaderFunction } from "@remix-run/cloudflare"; 6 | import { createAppContext } from "../context"; 7 | 8 | export const loader: LoaderFunction = async ({ context }) => { 9 | const appContext = createAppContext(context); 10 | const { config } = appContext; 11 | const models = Object.entries(config.CUSTOMER_MODEL_MAP).map(([id, path]) => ({ id, path })); 12 | return json({ models, config }); 13 | }; 14 | 15 | export const action: ActionFunction = async ({ request, context }: { request: Request; context: any }) => { 16 | const appContext = createAppContext(context); 17 | const { imageGenerationService, config } = appContext; 18 | 19 | console.log("Generate image action started"); 20 | console.log("Config:", JSON.stringify(config, null, 2)); 21 | 22 | const formData = await request.formData(); 23 | const prompt = formData.get("prompt") as string; 24 | const enhance = formData.get("enhance") === "true"; 25 | const modelId = formData.get("model") as string; 26 | const size = formData.get("size") as string; 27 | const numSteps = parseInt(formData.get("numSteps") as string, 10); 28 | 29 | console.log("Form data:", { prompt, enhance, modelId, size, numSteps }); 30 | 31 | if (!prompt) { 32 | return json({ error: "未找到提示词" }, { status: 400 }); 33 | } 34 | 35 | const model = config.CUSTOMER_MODEL_MAP[modelId]; 36 | if (!model) { 37 | return json({ error: "无效的模型" }, { status: 400 }); 38 | } 39 | 40 | try { 41 | const result = await imageGenerationService.generateImage( 42 | enhance ? `---tl ${prompt}` : prompt, 43 | model, 44 | size, 45 | numSteps 46 | ); 47 | console.log("Image generation successful"); 48 | return json(result); 49 | } catch (error) { 50 | console.error("生成图片时出错:", error); 51 | if (error instanceof AppError) { 52 | return json({ error: `生成图片失败: ${error.message}` }, { status: error.status || 500 }); 53 | } 54 | return json({ error: "生成图片失败: 未知错误" }, { status: 500 }); 55 | } 56 | }; 57 | 58 | const GenerateImage: FC = () => { 59 | const { models, config } = useLoaderData(); 60 | const [prompt, setPrompt] = useState(""); 61 | const [enhance, setEnhance] = useState(false); 62 | const [model, setModel] = useState(config.CUSTOMER_MODEL_MAP["FLUX.1-Schnell-CF"]); 63 | const [size, setSize] = useState("1024x1024"); 64 | const [numSteps, setNumSteps] = useState(config.FLUX_NUM_STEPS); 65 | const actionData = useActionData(); 66 | const navigation = useNavigation(); 67 | 68 | const isSubmitting = navigation.state === "submitting"; 69 | 70 | const handleEnhanceToggle = () => { 71 | setEnhance(!enhance); 72 | }; 73 | 74 | const handleReset = () => { 75 | setPrompt(""); 76 | setEnhance(false); 77 | setModel(config.CUSTOMER_MODEL_MAP["FLUX.1-Schnell-CF"]); 78 | setSize("1024x1024"); 79 | setNumSteps(config.FLUX_NUM_STEPS); 80 | }; 81 | 82 | const handlePromptChange = (e: ChangeEvent) => { 83 | setPrompt(e.target.value); 84 | }; 85 | 86 | const handleSubmit = (e: FormEvent) => { 87 | if (isSubmitting) { 88 | e.preventDefault(); 89 | } 90 | }; 91 | 92 | const handleModelChange = (e: ChangeEvent) => { 93 | setModel(e.target.value); 94 | }; 95 | 96 | return ( 97 |
98 |
99 |

100 | 白嫖 CF 的 Flux 生成图片 101 |

102 |
103 |
104 | 107 | 117 |
118 |
119 | 122 | 135 |
136 |
137 | 140 | 151 |
152 |
153 | 156 | setNumSteps(parseInt(e.target.value, 10))} 162 | min="4" 163 | max="8" 164 | className="w-full px-5 py-3 rounded-xl border border-transparent focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-white bg-opacity-20 text-white transition duration-300 ease-in-out hover:bg-opacity-30" 165 | /> 166 |
167 |
168 | 177 | 178 | 186 | 194 |
195 |
196 | {actionData && actionData.image && ( 197 |
198 |

生成的图片:

199 | Generated Image 200 |
201 | )} 202 | {/* Decorative Elements */} 203 |
204 |
205 |
206 |
207 | ); 208 | }; 209 | 210 | export default GenerateImage; -------------------------------------------------------------------------------- /app/routes/getme.tsx: -------------------------------------------------------------------------------- 1 | import { json, type LoaderFunction, type ActionFunction } from "@remix-run/cloudflare"; 2 | import { useLoaderData, useActionData, Form } from "@remix-run/react"; 3 | import { createAppContext } from "../context"; 4 | import { withAuth } from "../middleware/auth"; 5 | import { useState, useEffect } from "react"; 6 | 7 | export const loader: LoaderFunction = withAuth(async ({ context }) => { 8 | const appContext = createAppContext(context); 9 | const { config } = appContext; 10 | const env = context.cloudflare.env; 11 | 12 | const envVariables = { 13 | API_KEY: config.API_KEY ? config.API_KEY : "未设置", 14 | CF_ACCOUNT_LIST: config.CF_ACCOUNT_LIST.length > 0 ? "已设置" : "未设置", 15 | CF_TRANSLATE_MODEL: config.CF_TRANSLATE_MODEL, 16 | CF_IS_TRANSLATE: config.CF_IS_TRANSLATE ? "true" : "false", 17 | USE_EXTERNAL_API: config.USE_EXTERNAL_API ? "true" : "false", 18 | EXTERNAL_API: config.EXTERNAL_API, 19 | EXTERNAL_MODEL: config.EXTERNAL_MODEL, 20 | EXTERNAL_API_KEY: config.EXTERNAL_API_KEY ? "已设置" : "未设置", 21 | FLUX_NUM_STEPS: config.FLUX_NUM_STEPS.toString(), 22 | CUSTOMER_MODEL_MAP: Object.keys(config.CUSTOMER_MODEL_MAP).length > 0 ? "已设置" : "未设置", 23 | getme: env.getme || "未设置", 24 | vars_apikey: env.API_KEY 25 | }; 26 | 27 | return json({ envVariables, apiKey: config.API_KEY }); 28 | }); 29 | 30 | export const action: ActionFunction = withAuth(async ({ request, context }) => { 31 | const formData = await request.formData(); 32 | const apiKey = formData.get("apiKey") as string; 33 | const config = createAppContext(context).config; 34 | 35 | if (apiKey === config.API_KEY) { 36 | return json({ success: true }); 37 | } else { 38 | return json({ error: "API 密钥不正确" }, { status: 401 }); 39 | } 40 | }); 41 | 42 | export default function GetMe() { 43 | const loaderData = useLoaderData(); 44 | const actionData = useActionData(); 45 | const [isAuthenticated, setIsAuthenticated] = useState(false); 46 | 47 | useEffect(() => { 48 | if (actionData?.success) { 49 | setIsAuthenticated(true); 50 | } 51 | }, [actionData]); 52 | 53 | if (!isAuthenticated) { 54 | return ( 55 |
56 |
57 |

验证 API 密钥

58 |
59 | 66 | 72 |
73 | {actionData?.error && ( 74 |

{actionData.error}

75 | )} 76 |
77 |
78 | ); 79 | } 80 | 81 | return ( 82 |
83 |
84 |

Workers 环境变量

85 |
86 | {Object.entries(loaderData.envVariables).map(([key, value]) => ( 87 |
88 |

{key}:

89 |

{value}

90 |
91 | ))} 92 |
93 |
94 |
95 | ); 96 | } -------------------------------------------------------------------------------- /app/routes/idioms/api.receive-data.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/cloudflare"; 2 | import type { ActionFunction } from "@remix-run/cloudflare"; 3 | 4 | export const action: ActionFunction = async ({ request }: { request: Request }) => { 5 | const data = await request.json(); 6 | 7 | // 验证数据格式 8 | if (!data["成语"] || !data["成语解释"] || !data["混淆成语1"] || !data["混淆成语2"] || 9 | !data["图1"] || !data["图2"] || !data["图3"] || !data["图4"]) { 10 | return json({ error: "数据格式不正确。" }, { status: 400 }); 11 | } 12 | 13 | // 这里你可以处理数据,例如存储到数据库 14 | // 为了简单起见,我们直接返回接收到的数据 15 | return json({ message: "数据接收成功", data }); 16 | }; 17 | -------------------------------------------------------------------------------- /app/routes/idioms/game.tsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData, useActionData, Form, useSubmit } from "@remix-run/react"; 2 | import { json } from "@remix-run/cloudflare"; 3 | import type { LoaderFunction, ActionFunction } from "@remix-run/cloudflare"; 4 | import { useState, useEffect } from "react"; 5 | import { createAppContext } from "../../context"; 6 | import type { AppLoadContext } from "@remix-run/cloudflare"; 7 | 8 | type GameData = { 9 | "成语": string; 10 | "成语解释": string; 11 | "混淆成语1": string; 12 | "混淆成语2": string; 13 | "图1": string; 14 | "图2": string; 15 | "图3": string; 16 | "图4": string; 17 | }; 18 | 19 | export const loader: LoaderFunction = async ({ request, context }: { request: Request; context: AppLoadContext }) => { 20 | const url = new URL(request.url); 21 | const isNewGame = url.searchParams.get("new") === "true"; 22 | 23 | if (isNewGame) { 24 | const appContext = createAppContext(context); 25 | const { config } = appContext; 26 | const webhookUrl = config.CHENGYU_WEBHOOK_URL || "https://aigenai-aiflow.hf.space/webhook/chengyu"; 27 | 28 | try { 29 | const response = await fetch(webhookUrl); 30 | if (!response.ok) { 31 | throw new Error(`HTTP error! status: ${response.status}`); 32 | } 33 | const gameData: GameData = await response.json(); 34 | return json(gameData); 35 | } catch (error) { 36 | console.error("获取成语数据失败:", error); 37 | return json({ error: "获取成语数据失败,请稍后再试。" }, { status: 500 }); 38 | } 39 | } 40 | 41 | return json({}); 42 | }; 43 | 44 | export const action: ActionFunction = async ({ request }: { request: Request }) => { 45 | const formData = await request.formData(); 46 | const selectedIdiom = formData.get("selectedIdiom"); 47 | const correctIdiom = formData.get("correctIdiom"); 48 | 49 | if (selectedIdiom === correctIdiom) { 50 | return json({ result: "正确" }); 51 | } else { 52 | return json({ result: "错误" }); 53 | } 54 | }; 55 | 56 | export default function Game() { 57 | const loaderData = useLoaderData(); 58 | const actionData = useActionData(); 59 | const [currentImage, setCurrentImage] = useState(0); 60 | const [gameData, setGameData] = useState(null); 61 | const [gameStarted, setGameStarted] = useState(false); 62 | const [showContinueButton, setShowContinueButton] = useState(false); 63 | const [score, setScore] = useState(0); 64 | const [round, setRound] = useState(0); 65 | const submit = useSubmit(); 66 | 67 | const images = gameData ? [gameData["图1"], gameData["图2"], gameData["图3"], gameData["图4"]] : []; 68 | 69 | useEffect(() => { 70 | if ('成语' in loaderData) { 71 | setGameData(loaderData as GameData); 72 | setGameStarted(true); 73 | setRound((prev) => prev + 1); 74 | } 75 | }, [loaderData]); 76 | 77 | useEffect(() => { 78 | if (gameStarted && images.length > 0) { 79 | const interval = setInterval(() => { 80 | setCurrentImage((prev) => (prev + 1) % images.length); 81 | }, 2000); // 增加到2秒,让用户有更多时间观察图片 82 | return () => clearInterval(interval); 83 | } 84 | }, [gameStarted, images.length]); 85 | 86 | useEffect(() => { 87 | if (actionData?.result) { 88 | setShowContinueButton(true); 89 | if (actionData.result === "正确") { 90 | setScore((prev) => prev + 1); 91 | } 92 | } 93 | }, [actionData]); 94 | 95 | const startNewGame = () => { 96 | submit({ new: "true" }, { method: "get" }); 97 | setShowContinueButton(false); 98 | }; 99 | 100 | if ('error' in loaderData) { 101 | return ( 102 |
103 |
104 |

出错了

105 |

{loaderData.error}

106 | 112 |
113 |
114 | ); 115 | } 116 | 117 | if (!gameStarted) { 118 | return ( 119 |
120 |
121 |

看图猜成语

122 |

准备好挑战你的成语知识了吗?

123 | 129 |
130 |
131 | ); 132 | } 133 | 134 | return ( 135 |
136 |
137 |

看图猜成语

138 |
139 |

得分: {score}

140 |

回合: {round}

141 |
142 | {gameData && ( 143 | <> 144 |
145 | {`成语图${currentImage 150 |
151 | {currentImage + 1} / 4 152 |
153 |
154 |
155 | 156 |
157 | {[gameData["成语"], gameData["混淆成语1"], gameData["混淆成语2"]].sort(() => Math.random() - 0.5).map((idiom, index) => ( 158 | 168 | ))} 169 |
170 |
171 | 172 | )} 173 | {actionData?.result && ( 174 |
175 | {actionData.result === "正确" ? "恭喜你,答对了!" : "很遗憾,答错了!"} 176 |
177 | )} 178 | {actionData?.result === "错误" && ( 179 |
180 | 正确答案: {gameData?.["成语"]} 181 |
182 | )} 183 | {gameData && ( 184 |
185 |

成语解释:

186 |

{gameData["成语解释"]}

187 |
188 | )} 189 | {showContinueButton && ( 190 | 196 | )} 197 |
198 |
199 | ); 200 | } 201 | -------------------------------------------------------------------------------- /app/services/imageGeneration.ts: -------------------------------------------------------------------------------- 1 | import { AppError } from '../utils/error'; 2 | import { Config } from '../config'; 3 | 4 | export class ImageGenerationService { 5 | constructor(private config: Config) {} 6 | 7 | async generateImage(prompt: string, model: string, size: string, numSteps: number): Promise<{ prompt: string, translatedPrompt: string, image: string }> { 8 | console.log("Generating image with params:", { prompt, model, size, numSteps }); 9 | const translatedPrompt = await this.translatePrompt(prompt); 10 | console.log("Translated prompt:", translatedPrompt); 11 | const isFluxModel = model === this.config.CUSTOMER_MODEL_MAP["FLUX.1-Schnell-CF"]; 12 | let imageBase64; 13 | try { 14 | imageBase64 = isFluxModel ? 15 | await this.generateFluxImage(model, translatedPrompt, numSteps) : 16 | await this.generateStandardImage(model, translatedPrompt, size, numSteps); 17 | } catch (error) { 18 | console.error("Error in image generation:", error); 19 | throw error; 20 | } 21 | 22 | return { 23 | prompt, 24 | translatedPrompt, 25 | image: imageBase64 26 | }; 27 | } 28 | 29 | private async translatePrompt(prompt: string): Promise { 30 | if (!this.config.CF_IS_TRANSLATE) { 31 | return prompt; 32 | } 33 | 34 | try { 35 | const response = await this.postRequest(this.config.CF_TRANSLATE_MODEL, { 36 | messages: [ 37 | { 38 | role: "system", 39 | content: `作为 Stable Diffusion Prompt 提示词专家,您将从关键词中创建提示,通常来自 Danbooru 等数据库。 40 | 请遵循以下规则: 41 | 1. 保持原始关键词的顺序。 42 | 2. 将中文关键词翻译成英文。 43 | 3. 添加相关的标签以增强图像质量和细节。 44 | 4. 使用逗号分隔关键词。 45 | 5. 保持简洁,避免重复。 46 | 6. 不要使用 "和" 或 "与" 等连接词。 47 | 7. 保留原始提示中的特殊字符,如 ()[]{}。 48 | 8. 不要添加 NSFW 内容。 49 | 9. 输出格式应为单行文本,不包含换行符。` 50 | }, 51 | { 52 | role: "user", 53 | content: `请优化并翻译以下提示词:${prompt}` 54 | } 55 | ] 56 | }); 57 | 58 | const jsonResponse = await response.json(); 59 | return jsonResponse.result.response.trim(); 60 | } catch (error) { 61 | console.error("翻译提示词时出错:", error); 62 | return prompt; // 如果翻译失败,返回原始提示词 63 | } 64 | } 65 | 66 | private async generateStandardImage(model: string, prompt: string, size: string, numSteps: number): Promise { 67 | const [width, height] = size.split('x').map(Number); 68 | const jsonBody = { prompt, num_steps: numSteps, guidance: 7.5, strength: 1, width, height }; 69 | const response = await this.postRequest(model, jsonBody); 70 | const imageBuffer = await response.arrayBuffer(); 71 | return this.arrayBufferToBase64(imageBuffer); 72 | } 73 | 74 | private async generateFluxImage(model: string, prompt: string, numSteps: number): Promise { 75 | const jsonBody = { prompt, num_steps: numSteps }; 76 | const response = await this.postRequest(model, jsonBody); 77 | const jsonResponse = await response.json(); 78 | if (!jsonResponse.result || !jsonResponse.result.image) { 79 | throw new AppError('Invalid response from Flux model', 500); 80 | } 81 | return jsonResponse.result.image; 82 | } 83 | 84 | private async postRequest(model: string, jsonBody: any): Promise { 85 | const account = this.config.CF_ACCOUNT_LIST[Math.floor(Math.random() * this.config.CF_ACCOUNT_LIST.length)]; 86 | const url = `https://api.cloudflare.com/client/v4/accounts/${account.account_id}/ai/run/${model}`; 87 | const headers = { 88 | 'Authorization': `Bearer ${account.token}`, 89 | 'Content-Type': 'application/json', 90 | }; 91 | 92 | try { 93 | const response = await fetch(url, { 94 | method: 'POST', 95 | headers: headers, 96 | body: JSON.stringify(jsonBody), 97 | }); 98 | 99 | if (!response.ok) { 100 | const errorText = await response.text(); 101 | console.error(`Cloudflare API request failed: ${response.status}`, errorText); 102 | throw new AppError(`Cloudflare API request failed: ${response.status} - ${errorText}`, response.status); 103 | } 104 | 105 | return response; 106 | } catch (error) { 107 | console.error("Error in postRequest:", error); 108 | if (error instanceof AppError) { 109 | throw error; 110 | } 111 | throw new AppError('Failed to connect to Cloudflare API', 500); 112 | } 113 | } 114 | 115 | private arrayBufferToBase64(buffer: ArrayBuffer): string { 116 | const bytes = new Uint8Array(buffer); 117 | const binary = bytes.reduce((acc, byte) => acc + String.fromCharCode(byte), ''); 118 | return btoa(binary); 119 | } 120 | 121 | async testCfAiConnection(): Promise { 122 | const testModel = this.config.CF_TRANSLATE_MODEL; 123 | const testPrompt = "Hello, world!"; 124 | await this.postRequest(testModel, { messages: [{ role: "user", content: testPrompt }] }); 125 | } 126 | } -------------------------------------------------------------------------------- /app/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | @apply bg-white dark:bg-gray-950; 8 | 9 | @media (prefers-color-scheme: dark) { 10 | color-scheme: dark; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/types.d.ts: -------------------------------------------------------------------------------- 1 | interface Env { 2 | API_KEY: string; 3 | CF_ACCOUNT_ID: string; 4 | CF_API_TOKEN: string; 5 | } -------------------------------------------------------------------------------- /app/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/cloudflare"; 2 | 3 | export class AppError extends Error { 4 | constructor(public message: string, public status: number = 500) { 5 | super(message); 6 | this.name = 'AppError'; 7 | } 8 | } 9 | 10 | export function handleError(error: unknown) { 11 | console.error(error); 12 | if (error instanceof AppError) { 13 | return json({ error: error.message }, { status: error.status }); 14 | } 15 | return json({ error: "An unexpected error occurred" }, { status: 500 }); 16 | } -------------------------------------------------------------------------------- /functions/[[path]].ts: -------------------------------------------------------------------------------- 1 | import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore - the server build file is generated by `remix vite:build` 5 | // eslint-disable-next-line import/no-unresolved 6 | import * as build from "../build/server"; 7 | 8 | export const onRequest = createPagesFunctionHandler({ build }); 9 | -------------------------------------------------------------------------------- /load-context.ts: -------------------------------------------------------------------------------- 1 | import { type PlatformProxy } from "wrangler"; 2 | 3 | type Cloudflare = Omit, "dispose">; 4 | 5 | declare module "@remix-run/cloudflare" { 6 | interface AppLoadContext { 7 | cloudflare: Cloudflare; 8 | } 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-remix-app", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "remix vite:build && wrangler pages functions build --outdir build/worker", 8 | "deploy": "pnpm run build && wrangler deploy", 9 | "dev": "remix vite:dev", 10 | "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", 11 | "start": "wrangler pages dev ./build/client", 12 | "typecheck": "tsc", 13 | "typegen": "wrangler types", 14 | "preview": "pnpm run build && wrangler dev", 15 | "cf-typegen": "wrangler types" 16 | }, 17 | "dependencies": { 18 | "@remix-run/cloudflare": "^2.12.0", 19 | "@remix-run/cloudflare-pages": "^2.12.0", 20 | "@remix-run/react": "^2.12.0", 21 | "isbot": "^4.1.0", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0" 24 | }, 25 | "devDependencies": { 26 | "@cloudflare/workers-types": "^4.20241004.0", 27 | "@remix-run/dev": "^2.12.0", 28 | "@types/react": "^18.2.20", 29 | "@types/react-dom": "^18.2.7", 30 | "@typescript-eslint/eslint-plugin": "^6.7.4", 31 | "@typescript-eslint/parser": "^6.7.4", 32 | "autoprefixer": "^10.4.19", 33 | "eslint": "^8.38.0", 34 | "eslint-import-resolver-typescript": "^3.6.1", 35 | "eslint-plugin-import": "^2.28.1", 36 | "eslint-plugin-jsx-a11y": "^6.7.1", 37 | "eslint-plugin-react": "^7.33.2", 38 | "eslint-plugin-react-hooks": "^4.6.0", 39 | "postcss": "^8.4.38", 40 | "tailwindcss": "^3.4.4", 41 | "typescript": "^5.1.6", 42 | "vite": "^5.1.0", 43 | "vite-tsconfig-paths": "^4.2.1", 44 | "wrangler": "3.80.5" 45 | }, 46 | "engines": { 47 | "node": ">=20.0.0" 48 | } 49 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.assetsignore: -------------------------------------------------------------------------------- 1 | _worker.js 2 | _routes.json 3 | _headers 4 | _redirects 5 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /favicon.ico 2 | Cache-Control: public, max-age=3600, s-maxage=3600 3 | /assets/* 4 | Cache-Control: public, max-age=31536000, immutable 5 | -------------------------------------------------------------------------------- /public/_routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "include": ["/*"], 4 | "exclude": ["/favicon.ico", "/assets/*"] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aigem/cf-flux-remix/3e25d0f8c80c60ec9d85e6fc61933d9b6b01887b/public/favicon.ico -------------------------------------------------------------------------------- /public/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aigem/cf-flux-remix/3e25d0f8c80c60ec9d85e6fc61933d9b6b01887b/public/logo-dark.png -------------------------------------------------------------------------------- /public/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aigem/cf-flux-remix/3e25d0f8c80c60ec9d85e6fc61933d9b6b01887b/public/logo-light.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | sans: [ 9 | '"Inter"', 10 | "ui-sans-serif", 11 | "system-ui", 12 | "sans-serif", 13 | '"Apple Color Emoji"', 14 | '"Segoe UI Emoji"', 15 | '"Segoe UI Symbol"', 16 | '"Noto Color Emoji"', 17 | ], 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | } satisfies Config; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.ts", 4 | "**/*.tsx", 5 | "**/.server/**/*.ts", 6 | "**/.server/**/*.tsx", 7 | "**/.client/**/*.ts", 8 | "**/.client/**/*.tsx" 9 | ], 10 | "compilerOptions": { 11 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 12 | "types": [ 13 | "@remix-run/cloudflare", 14 | "vite/client", 15 | "@cloudflare/workers-types/2023-07-01" 16 | ], 17 | "isolatedModules": true, 18 | "esModuleInterop": true, 19 | "jsx": "react-jsx", 20 | "module": "ESNext", 21 | "moduleResolution": "Bundler", 22 | "resolveJsonModule": true, 23 | "target": "ES2022", 24 | "strict": true, 25 | "allowJs": true, 26 | "skipLibCheck": true, 27 | "forceConsistentCasingInFileNames": true, 28 | "baseUrl": ".", 29 | "paths": { 30 | "~/*": ["./app/*"] 31 | }, 32 | 33 | // Vite takes care of building everything, not tsc. 34 | "noEmit": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { vitePlugin as remix } from "@remix-run/dev"; 3 | 4 | export default defineConfig({ 5 | plugins: [remix()], 6 | }); 7 | -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler 2 | // After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` 3 | interface Env { 4 | } 5 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | # Worker 名称,与CF中的Worker名称必须一致 2 | name = "free-flux" 3 | 4 | # 兼容性日期,确保使用最新的 Workers 运行时功能 5 | compatibility_date = "2024-10-04" 6 | 7 | # 主入口文件 8 | main = "./build/worker/index.js" 9 | 10 | # 静态资源目录 11 | assets = { directory = "./build/client" } 12 | 13 | # 启用 Workers 日志 14 | [observability] 15 | enabled = true 16 | 17 | [vars] 18 | CF_ACCOUNT_LIST = '''[{ 19 | "account_id":"你的CloudFlare帐户ID", 20 | "token":"有Workers AI权限的API令牌" 21 | }]''' 22 | API_KEY = "your_api_key_here" 23 | CF_TRANSLATE_MODEL = "@cf/qwen/qwen1.5-14b-chat-awq" 24 | CF_IS_TRANSLATE = "true" 25 | USE_EXTERNAL_API = "false" 26 | EXTERNAL_API = "" 27 | EXTERNAL_MODEL = "" 28 | EXTERNAL_API_KEY = "" 29 | FLUX_NUM_STEPS = "6" 30 | CUSTOMER_MODEL_MAP = '''{ 31 | "FLUX.1-Schnell-CF":"@cf/black-forest-labs/flux-1-schnell", 32 | "DS-8-CF":"@cf/lykon/dreamshaper-8-lcm", 33 | "SD-XL-Bash-CF":"@cf/stabilityai/stable-diffusion-xl-base-1.0", 34 | "SD-XL-Lightning-CF":"@cf/bytedance/stable-diffusion-xl-lightning" 35 | }''' 36 | getme = "测试专用" 37 | --------------------------------------------------------------------------------