【Cloudflare系列教程】利用 Midjourney-Proxy 绘图

Midjourney-Proxy 转 OpenAI 绘图 继续,原帖使用的 Cloudflare KV 存储,考虑到灵活性,本帖改为使用 Upstash Redis 作为存储空间,免费计划支持10w次请求。

  • 支持放大和变化
  • 支持保持历史记录
  • 支持根据ip地址区分不同用户请求
  • 支持流式输出,动态显示生成进度

简单上手

配置数据库

1.注册账户

访问 Upstash注册页 点击 Create an account 注册一个新账号(需要邮箱邮箱)



2. 创建数据库

点击 Create data base,填写名称选择服务器地区,点击 Next,然后选择 Free 后点击 Create,数据库就创建好了





3. 获取密钥

点击javascript,然后点击红圈显示,复制矩形框的的值


配置 Worker

1. 安装 Wrangler

参考 Cloudflare/Wrangler 文档 进行安装,如果已经安装请注意更新版本

npm install wrangler@latest

2. 创建项目

mkdir MPO && cd MPO && wrangler init MPO

3. 安装插件

npm install @upstash/redis

4. 配置 wrangler.toml

需要注意的是最新版本的 Wrangler 已经不再需要定义 account_id 等参数

#:schema node_modules/wrangler/config-schema.json
name = "MPO"
main = "src/index.js"
compatibility_date = "2024-07-24"
compatibility_flags = ["nodejs_compat"]

[vars]
AUTH_KEY = 'aaaa'
PROXY = ‘https://aa.bb’
MJ_SITE_URL = 'https://cc.dd'
MJ_API_SECRET = 'eeee'
UPSTASH_REDIS_REST_URL = "https://aaaa-bbbb-cccc.upstash.io"
UPSTASH_REDIS_REST_TOKEN = "dddd"
配置项 说明
AUTH_KEY 必填,你的worker鉴权密钥
PROXY 可选,你的discord图片代理地址
MJ_SITE_URL 必填,midjourney-proxy站点地址
MJ_API_SECRET 必填,midjourney-proxy站点密钥
UPSTASH_REDIS_REST_URL 必填,Upstash数据库的 url
UPSTASH_REDIS_REST_TOKEN 必填,Upstash数据库的 token
使用Nginx代理discord图片
location ^~ /discord/{
	proxy_pass https://cdn.discordapp.com/;  
	proxy_connect_timeout 6000;    
	proxy_read_timeout 6000;         
}

5. 发布 Worker

vi ./src/index.js

复制粘贴以下代码

index.js
import { Redis } from "@upstash/redis/cloudflare";

const CONFIG = {
  CORS_HEADERS: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization'
  },
  MODEL_NAME: 'midjourney',
  POLL_INTERVAL: 5000,
  PROGRESS_BAR_LENGTH: 33
};

export default {
  async fetch(request, env) {
    const redis = Redis.fromEnv(env);
    return handleRequest(request, redis, env);
  },
};

async function handleRequest(request, redis, env) {
  const { method, headers, url } = request;
  const requestUrl = new URL(url);

  if (method === 'OPTIONS') return new Response('OK', { headers: CONFIG.CORS_HEADERS });

  const clientIp = getClientIp(headers);
  if (!clientIp) {
    return createErrorResponse('Unable to determine client IP', 400);
  }

  const { AUTH_KEY, PROXY, MJ_SITE_URL, MJ_API_SECRET } = env;

  if (method !== 'POST' || requestUrl.pathname !== '/v1/chat/completions' || headers.get('Authorization') !== `Bearer ${AUTH_KEY}`) {
    return createErrorResponse('Unauthorized', 401);
  }

  try {
    const requestData = await request.json();
    const lastMessage = requestData.messages.pop().content.trim();

    if (lastMessage.startsWith('U') || lastMessage.startsWith('V') || lastMessage.startsWith('#')) {
      const { taskId, index, action } = parseModifyRequest(lastMessage);
      return handleModifyRequest(clientIp, index, action, MJ_SITE_URL, MJ_API_SECRET, redis, taskId, PROXY);
    } else {
      return handleGenerateRequest(clientIp, lastMessage, MJ_SITE_URL, MJ_API_SECRET, redis, PROXY);
    }
  } catch (err) {
    return createErrorResponse('Internal Server Error', 500);
  }
}

function getClientIp(headers) {
  return headers.get('CF-Connecting-IP') || headers.get('x-forwarded-for') || headers.get('X-Real-IP');
}

function parseModifyRequest(message) {
  if (message.startsWith('#')) {
    const [taskId, actions] = message.substring(1).split('-');
    return { taskId, index: actions.substring(1), action: actions[0] === 'U' ? 'UPSCALE' : 'VARIATION' };
  }
  return { taskId: null, index: message.substring(1), action: message.startsWith('U') ? 'UPSCALE' : 'VARIATION' };
}

async function handleGenerateRequest(clientIp, prompt, MJ_SITE_URL, MJ_API_SECRET, redis, PROXY) {
  try {
    const taskId = await submitRequest(`${MJ_SITE_URL}/mj/submit/imagine`, { prompt }, MJ_API_SECRET);
    return pollStatusStream(`${MJ_SITE_URL}/mj/task/${taskId}/fetch`, taskId, clientIp, redis, MJ_API_SECRET, true, PROXY);
  } catch (error) {
    return createErrorResponse('Failed to generate image after multiple attempts', 500);
  }
}

async function handleModifyRequest(clientIp, index, action, MJ_SITE_URL, MJ_API_SECRET, redis, previousTaskId, PROXY) {
  const taskId = previousTaskId || await getPreviousTaskId(clientIp, redis);
  if (!taskId || isNaN(index) || index < 1 || index > 4) return createErrorResponse('Invalid request', 400);

  try {
    const newTaskId = await submitRequest(`${MJ_SITE_URL}/mj/submit/change`, { action, index, taskId }, MJ_API_SECRET);
    return pollStatusStream(`${MJ_SITE_URL}/mj/task/${newTaskId}/fetch`, newTaskId, clientIp, redis, MJ_API_SECRET, action === 'VARIATION', PROXY);
  } catch (error) {
    return createErrorResponse('Failed to modify image', 500);
  }
}

async function submitRequest(url, body, MJ_API_SECRET) {
  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'mj-api-secret': MJ_API_SECRET },
    body: JSON.stringify(body)
  });
  const data = await response.json();
  if (!data.result) throw new Error('Response missing result');
  return data.result;
}

async function getPreviousTaskId(clientIp, redis) {
  const kvData = await redis.get(`${clientIp}-000`);
  if (!kvData) throw new Error('No previous task found in storage');
  return kvData.taskId;
}

async function pollStatusStream(url, taskId, clientIp, redis, MJ_API_SECRET, isNew, PROXY) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      try {
        controller.enqueue(encoder.encode(createResponse('progress', `taskId: ${taskId}\n\n`)));

        while (true) {
          await new Promise(r => setTimeout(r, CONFIG.POLL_INTERVAL));
          const data = await fetchStatus(url, MJ_API_SECRET);

          const progressContent = createProgressBar(data.progress);
          controller.enqueue(encoder.encode(createResponse('progress', progressContent)));

          if (data.progress === '100%' && data.imageUrl) {
            const proxyUrl = PROXY ? PROXY + data.imageUrl.replace(/^https?:\/\/[^\/]+/, '') : data.imageUrl;
            const finalResponse = createResponse('image', `\n![](${proxyUrl})`, true);
            controller.enqueue(encoder.encode(finalResponse));
            if (isNew) {
              await redis.set(`${clientIp}-000`, JSON.stringify({ taskId, imgUrl: data.imageUrl }));
            } else {
              await redis.set(`${taskId}`, data.imageUrl);
            }
            controller.close();
            return;
          }
        }
      } catch (error) {
        controller.error(new Error('Streaming error: ' + error.message));
      }
    }
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      ...CONFIG.CORS_HEADERS
    }
  });
}

async function fetchStatus(url, MJ_API_SECRET) {
  const response = await fetch(url, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json', 'mj-api-secret': MJ_API_SECRET }
  });
  return response.json();
}

function createProgressBar(progress) {
  const filledStars = Math.round((parseInt(progress) / 100) * CONFIG.PROGRESS_BAR_LENGTH);
  const emptyStars = CONFIG.PROGRESS_BAR_LENGTH - filledStars;
  return '█'.repeat(filledStars) + '░'.repeat(emptyStars) + ` ${progress}\n`;
}

function createResponse(type, content, isFinished = false) {
  const baseResponse = {
    id: `imggen-${Math.floor(Date.now() / 1000)}`,
    object: 'chat.completion.chunk',
    created: Math.floor(Date.now() / 1000),
    model: CONFIG.MODEL_NAME,
    choices: [{
      index: 0,
      delta: { content },
      finish_reason: isFinished ? 'stop' : null
    }]
  };

  let response = `data: ${JSON.stringify(baseResponse)}\n\n`;
  
  if (isFinished) {
    response += `data: [DONE]\n\n`;
  }

  return response;
}

function createErrorResponse(message, status) {
  return new Response(message, { status, headers: CONFIG.CORS_HEADERS });
}

保存文件后发布

wrangler deploy

配置 OneAPI

创建 OpenAI 渠道,代理填写你的 Worker地址,密钥填写您设置的 AUTH_KEY,模型可以随便定义

参考链接

16 个赞

大佬出新教程了

如何加到大佬wx

R佬太强了

太强了!

太强了R佬

太强啦,R佬!

1 个赞

谢谢分享

2024/07/24 更新

  • 增加了详细教程

太强了 R佬!

3 个赞

学习 谢谢

太厉害了 :clap: :clap:

膜拜