1. HTML 设置面板 (index.html:55-63)
新增 API格式 下拉选择框:
- raw - 原始API格式(使用 /v1/images/generations 端点)
- openai - OpenAI格式(使用 /v1/chat/completions 端点)
2. JavaScript 核心逻辑 (script.js)
设置字段 (script.js:10)
- currentSettings 新增 apiFormat: 'raw' 字段
API 服务 (script.js:685-916)
- testConnection: 根据 API 格式选择 /models 或 /v1/models 端点
- generateImage:
- 原始格式: 使用 prompt 请求体,端点 /v1/images/generations
- OpenAI格式: 使用 messages 请求体,端点 /v1/chat/completions
- extractImagesFromContent: 新增方法,支持从 Chat 响应中提取图像:
- ✅ Markdown 图片语法 
- ✅ Base64 图像数据
- ✅ 通用图像 URL(放宽限制)
- ✅ JSON 格式内容(包括代码块)
- ✅ 多模态内容数组
设置持久化 (script.js:1591, 1771, 1909)
- loadSettings / saveSettings 支持 apiFormat
- 自动保存监听器包含 apiFormat
连接测试 (script.js:2027, 2037)
- app.testConnection 传递 apiFormat 参数
This commit is contained in:
@@ -52,6 +52,15 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="apiFormat" class="form-label">API格式</label>
|
||||
<select class="form-select" id="apiFormat">
|
||||
<option value="raw">原始API格式</option>
|
||||
<option value="openai">OpenAI格式</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="responseFormat" class="form-label">响应格式</label>
|
||||
|
||||
207
script.js
207
script.js
@@ -7,6 +7,7 @@ let generatedImages = [];
|
||||
let currentSettings = {
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
apiFormat: 'raw', // 'raw' 原始API格式, 'openai' OpenAI Chat格式
|
||||
model: 'grok-imagine-1.0',
|
||||
timeout: 600,
|
||||
proxy: '',
|
||||
@@ -681,9 +682,12 @@ const indexedDBStorage = {
|
||||
// API服务
|
||||
const apiService = {
|
||||
// 测试连接
|
||||
testConnection: async function(apiKey, baseUrl) {
|
||||
testConnection: async function(apiKey, baseUrl, apiFormat) {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/models`, {
|
||||
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
|
||||
// 根据 API 格式选择不同的测试端点
|
||||
const modelsEndpoint = apiFormat === 'openai' ? '/v1/models' : '/models';
|
||||
const response = await fetch(`${normalizedBaseUrl}${modelsEndpoint}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
@@ -707,19 +711,41 @@ const apiService = {
|
||||
console.warn('当前 API 不支持图像编辑,将忽略上传的图像');
|
||||
}
|
||||
|
||||
// 构建请求 payload(符合 OpenAI 图像生成 API 格式)
|
||||
const payload = {
|
||||
model: settings.model,
|
||||
prompt: message,
|
||||
n: settings.n || 1,
|
||||
response_format: settings.responseFormat || 'b64_json'
|
||||
};
|
||||
const apiFormat = settings.apiFormat || 'raw';
|
||||
const normalizedBaseUrl = settings.baseUrl.replace(/\/+$/, '');
|
||||
|
||||
// 根据 API 格式构建不同的请求
|
||||
let endpoint, payload;
|
||||
|
||||
if (apiFormat === 'openai') {
|
||||
// OpenAI Chat Completions 格式
|
||||
endpoint = `${normalizedBaseUrl}/v1/chat/completions`;
|
||||
payload = {
|
||||
model: settings.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: message
|
||||
}
|
||||
],
|
||||
n: settings.n || 1
|
||||
};
|
||||
} else {
|
||||
// 原始 API 格式 (图像生成端点)
|
||||
endpoint = `${normalizedBaseUrl}/v1/images/generations`;
|
||||
payload = {
|
||||
model: settings.model,
|
||||
prompt: message,
|
||||
n: settings.n || 1,
|
||||
response_format: settings.responseFormat || 'b64_json'
|
||||
};
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), settings.timeout * 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${settings.baseUrl}/v1/images/generations`, {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${settings.apiKey}`,
|
||||
@@ -738,32 +764,47 @@ const apiService = {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 解析响应(OpenAI 图像生成 API 格式)
|
||||
const images = [];
|
||||
// 根据 API 格式解析响应
|
||||
const resultImages = [];
|
||||
let hasError = false;
|
||||
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
data.data.forEach(item => {
|
||||
if (item.b64_json) {
|
||||
// 检测 API 返回的错误标记
|
||||
if (item.b64_json === 'error' || item.b64_json === 'Error') {
|
||||
hasError = true;
|
||||
return;
|
||||
if (apiFormat === 'openai') {
|
||||
// 解析 OpenAI Chat Completions 响应
|
||||
if (data.choices && Array.isArray(data.choices)) {
|
||||
data.choices.forEach(choice => {
|
||||
const content = choice.message?.content;
|
||||
if (content) {
|
||||
// 尝试从 content 中提取图像
|
||||
const extractedImages = this.extractImagesFromContent(content);
|
||||
resultImages.push(...extractedImages);
|
||||
}
|
||||
// base64 格式响应
|
||||
const base64Data = `data:image/png;base64,${item.b64_json}`;
|
||||
const blobUrl = utils.base64ToBlobUrl(base64Data);
|
||||
images.push(blobUrl);
|
||||
} else if (item.url) {
|
||||
// 检测 URL 格式的错误标记
|
||||
if (item.url === 'error' || item.url === 'Error') {
|
||||
hasError = true;
|
||||
return;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 解析原始 API 响应(OpenAI 图像生成 API 格式)
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
data.data.forEach(item => {
|
||||
if (item.b64_json) {
|
||||
// 检测 API 返回的错误标记
|
||||
if (item.b64_json === 'error' || item.b64_json === 'Error') {
|
||||
hasError = true;
|
||||
return;
|
||||
}
|
||||
// base64 格式响应
|
||||
const base64Data = `data:image/png;base64,${item.b64_json}`;
|
||||
const blobUrl = utils.base64ToBlobUrl(base64Data);
|
||||
resultImages.push(blobUrl);
|
||||
} else if (item.url) {
|
||||
// 检测 URL 格式的错误标记
|
||||
if (item.url === 'error' || item.url === 'Error') {
|
||||
hasError = true;
|
||||
return;
|
||||
}
|
||||
// URL 格式响应
|
||||
resultImages.push(item.url);
|
||||
}
|
||||
// URL 格式响应
|
||||
images.push(item.url);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果检测到错误标记,抛出错误以触发重试
|
||||
@@ -773,14 +814,105 @@ const apiService = {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: `已生成 ${images.length} 张图像`,
|
||||
images: images,
|
||||
content: `已生成 ${resultImages.length} 张图像`,
|
||||
images: resultImages,
|
||||
usage: data.usage || null
|
||||
};
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 从 OpenAI Chat 响应内容中提取图像
|
||||
extractImagesFromContent: function(content) {
|
||||
const images = [];
|
||||
|
||||
// 如果 content 是字符串,尝试解析
|
||||
if (typeof content === 'string') {
|
||||
// 优先匹配 Markdown 图片语法 
|
||||
const markdownImageRegex = /!\[[^\]]*\]\(([^)]+)\)/g;
|
||||
let mdMatch;
|
||||
while ((mdMatch = markdownImageRegex.exec(content)) !== null) {
|
||||
const url = mdMatch[1].trim();
|
||||
if (url && !images.includes(url)) {
|
||||
images.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试匹配 base64 图像数据
|
||||
const base64Regex = /data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+/g;
|
||||
const base64Matches = content.match(base64Regex);
|
||||
if (base64Matches) {
|
||||
base64Matches.forEach(match => {
|
||||
if (!images.includes(match)) {
|
||||
const blobUrl = utils.base64ToBlobUrl(match);
|
||||
images.push(blobUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 尝试匹配通用图像 URL(放宽限制,支持无扩展名的签名URL)
|
||||
const urlRegex = /https?:\/\/[^\s"'<>\)]+/gi;
|
||||
const urlMatches = content.match(urlRegex);
|
||||
if (urlMatches) {
|
||||
urlMatches.forEach(url => {
|
||||
// 清理 URL 末尾可能的标点符号
|
||||
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
|
||||
// 检查是否像图像 URL(包含常见图像关键词或扩展名)
|
||||
const isImageUrl = /\.(png|jpg|jpeg|gif|webp|bmp|svg)(\?|$)/i.test(cleanUrl) ||
|
||||
/image|img|photo|picture|generated|upload/i.test(cleanUrl);
|
||||
if (isImageUrl && !images.includes(cleanUrl)) {
|
||||
images.push(cleanUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 尝试解析 JSON 格式的内容(包括 Markdown 代码块中的 JSON)
|
||||
let jsonStr = content;
|
||||
// 尝试提取 Markdown 代码块中的 JSON
|
||||
const codeBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
if (codeBlockMatch) {
|
||||
jsonStr = codeBlockMatch[1].trim();
|
||||
}
|
||||
try {
|
||||
const jsonContent = JSON.parse(jsonStr);
|
||||
if (jsonContent.url && !images.includes(jsonContent.url)) {
|
||||
images.push(jsonContent.url);
|
||||
}
|
||||
if (jsonContent.b64_json) {
|
||||
const base64Data = `data:image/png;base64,${jsonContent.b64_json}`;
|
||||
const blobUrl = utils.base64ToBlobUrl(base64Data);
|
||||
images.push(blobUrl);
|
||||
}
|
||||
if (Array.isArray(jsonContent.data)) {
|
||||
jsonContent.data.forEach(item => {
|
||||
if (item.url && !images.includes(item.url)) images.push(item.url);
|
||||
if (item.b64_json) {
|
||||
const base64Data = `data:image/png;base64,${item.b64_json}`;
|
||||
const blobUrl = utils.base64ToBlobUrl(base64Data);
|
||||
images.push(blobUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 不是 JSON 格式,忽略
|
||||
}
|
||||
} else if (Array.isArray(content)) {
|
||||
// 处理多模态内容数组
|
||||
content.forEach(part => {
|
||||
if (part.type === 'image_url' && part.image_url?.url) {
|
||||
images.push(part.image_url.url);
|
||||
}
|
||||
// 处理 text 类型中可能包含的图像
|
||||
if (part.type === 'text' && part.text) {
|
||||
const textImages = this.extractImagesFromContent(part.text);
|
||||
images.push(...textImages);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1587,6 +1719,7 @@ const uiController = {
|
||||
// 更新UI
|
||||
document.getElementById('apiKey').value = currentSettings.apiKey;
|
||||
document.getElementById('baseUrl').value = currentSettings.baseUrl;
|
||||
document.getElementById('apiFormat').value = currentSettings.apiFormat || 'raw';
|
||||
document.getElementById('model').value = currentSettings.model;
|
||||
document.getElementById('timeout').value = currentSettings.timeout;
|
||||
document.getElementById('proxy').value = currentSettings.proxy;
|
||||
@@ -1766,6 +1899,7 @@ const uiController = {
|
||||
try {
|
||||
currentSettings.apiKey = document.getElementById('apiKey').value;
|
||||
currentSettings.baseUrl = document.getElementById('baseUrl').value;
|
||||
currentSettings.apiFormat = document.getElementById('apiFormat').value;
|
||||
currentSettings.model = document.getElementById('model').value;
|
||||
currentSettings.timeout = parseInt(document.getElementById('timeout').value);
|
||||
currentSettings.proxy = document.getElementById('proxy').value;
|
||||
@@ -1903,7 +2037,7 @@ const app = {
|
||||
}
|
||||
|
||||
// 设置变化时自动保存
|
||||
const inputs = ['apiKey', 'baseUrl', 'model', 'timeout', 'proxy'];
|
||||
const inputs = ['apiKey', 'baseUrl', 'apiFormat', 'model', 'timeout', 'proxy'];
|
||||
inputs.forEach(id => {
|
||||
document.getElementById(id).addEventListener('change', utils.debounce(() => {
|
||||
uiController.saveSettings();
|
||||
@@ -1920,6 +2054,7 @@ const app = {
|
||||
testConnection: async function() {
|
||||
const apiKey = document.getElementById('apiKey').value;
|
||||
const baseUrl = document.getElementById('baseUrl').value;
|
||||
const apiFormat = document.getElementById('apiFormat').value;
|
||||
|
||||
if (!apiKey) {
|
||||
utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning');
|
||||
@@ -1929,7 +2064,7 @@ const app = {
|
||||
uiController.showLoading(true);
|
||||
|
||||
try {
|
||||
const result = await apiService.testConnection(apiKey, baseUrl);
|
||||
const result = await apiService.testConnection(apiKey, baseUrl, apiFormat);
|
||||
|
||||
if (result.success) {
|
||||
uiController.updateConnectionStatus(true);
|
||||
|
||||
Reference in New Issue
Block a user