从 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,模型可以随便定义