Files
OpenRouter_Image/script.js

1609 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// OpenRouter Image Generator - 主要JavaScript逻辑
// 全局变量
let uploadedImages = [];
let chatHistory = [];
let generatedImages = [];
let currentSettings = {
apiKey: '',
baseUrl: 'https://openrouter.ai/api/v1',
model: 'google/gemini-2.5-flash-image-preview:free',
timeout: 600,
proxy: ''
};
// 配置常量
const CONFIG = {
MAX_FILE_SIZE: 10485760, // 10MB
MAX_FILES_PER_UPLOAD: 1, // 限制为只上传一张图片
SUPPORTED_IMAGE_FORMATS: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
MAX_CHAT_HISTORY: 50, // 减少聊天历史记录数量以节省存储空间
MAX_GENERATED_IMAGES: 20, // 减少存储的图像数量以防止存储空间溢出
STORAGE_KEYS: {
SETTINGS: 'openRouterSettings',
CHAT_HISTORY: 'openRouterChatHistory',
GENERATED_IMAGES: 'openRouterGeneratedImages'
},
INDEXED_DB: {
NAME: 'OpenRouterImageDB',
VERSION: 1,
STORES: {
SETTINGS: 'settings',
CHAT_HISTORY: 'chatHistory',
GENERATED_IMAGES: 'generatedImages'
}
}
};
// 错误消息
const ERROR_MESSAGES = {
NO_API_KEY: '请先输入API Key',
CONNECTION_FAILED: '连接失败请检查网络和API设置',
INVALID_RESPONSE: 'API响应格式错误',
IMAGE_GENERATION_FAILED: '图像生成失败',
FILE_TOO_LARGE: '文件大小超过限制',
UNSUPPORTED_FORMAT: '不支持的文件格式',
UPLOAD_FAILED: '文件上传失败'
};
// 工具函数
const utils = {
// 格式化文件大小
formatFileSize: function(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
// 显示通知
showNotification: function(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// 自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
},
// 验证文件
validateFile: function(file) {
if (file.size > CONFIG.MAX_FILE_SIZE) {
throw new Error(ERROR_MESSAGES.FILE_TOO_LARGE);
}
if (!CONFIG.SUPPORTED_IMAGE_FORMATS.includes(file.type)) {
throw new Error(ERROR_MESSAGES.UNSUPPORTED_FORMAT);
}
return true;
},
// 防抖函数
debounce: function(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// 检查和清理存储空间
checkAndCleanStorage: async function() {
if (!indexedDBStorage.isSupported) {
console.warn('IndexedDB 不支持,无法进行存储检查');
return;
}
try {
// 检查 IndexedDB 存储并清理过期数据
await indexedDBStorage.cleanupOldMessages();
await indexedDBStorage.cleanupOldImages();
console.log('IndexedDB 存储空间检查和清理完成');
} catch (error) {
console.error('存储空间检查失败:', error);
}
},
// 检查存储状态 - 调试用
checkStorageStatus: async function() {
console.log('=== 存储状态检查 ===');
console.log('IndexedDB 支持:', indexedDBStorage.isSupported);
console.log('IndexedDB 数据库:', indexedDBStorage.db);
if (indexedDBStorage.isSupported) {
try {
const settings = await indexedDBStorage.getSettings();
const chatHistory = await indexedDBStorage.getChatHistory();
const images = await indexedDBStorage.getGeneratedImages();
console.log('IndexedDB 数据统计:');
console.log(`- 设置: ${settings ? '已保存' : '未保存'}`);
console.log(`- 聊天记录: ${chatHistory.length}`);
console.log(`- 生成图像: ${images.length}`);
chatHistory.forEach((msg, idx) => {
console.log(` 聊天 ${idx}: ${msg.role} - ${msg.content.substring(0, 50)}...`);
});
images.forEach((img, idx) => {
console.log(` 图像 ${idx}: URL长度 ${img.url ? img.url.length : 0}`);
});
} catch (error) {
console.error('读取 IndexedDB 数据失败:', error);
}
}
},
// 检查事件绑定 - 调试用
checkEventBindings: function() {
console.log('=== 事件绑定检查 ===');
const containers = document.querySelectorAll('.image-clickable-container');
console.log(`找到 ${containers.length} 个可点击图像容器`);
containers.forEach((container, idx) => {
const img = container.querySelector('img');
const tooltip = container.querySelector('.image-preview-tooltip');
console.log(`容器 ${idx}:`, {
src: img ? img.src.substring(0, 50) + '...' : '无图片',
hasTooltip: !!tooltip,
tooltipText: tooltip ? tooltip.textContent : '无',
classList: container.classList.toString()
});
});
// 测试点击第一个容器
if (containers.length > 0) {
console.log('尝试点击第一个图像容器...');
containers[0].click();
}
}
};
// IndexedDB存储管理模块
const indexedDBStorage = {
db: null,
initPromise: null,
isSupported: false,
// 初始化数据库
init: function() {
if (this.initPromise) {
return this.initPromise;
}
this.initPromise = new Promise((resolve, reject) => {
if (!window.indexedDB) {
console.warn('IndexedDB 不支持,将使用 localStorage 作为后备');
this.db = null;
this.isSupported = false;
resolve(false);
return;
}
const request = indexedDB.open(CONFIG.INDEXED_DB.NAME, CONFIG.INDEXED_DB.VERSION);
request.onerror = (event) => {
console.error('IndexedDB 初始化失败:', event.target.error);
this.db = null;
this.isSupported = false;
resolve(false);
};
request.onsuccess = (event) => {
this.db = event.target.result;
this.isSupported = true;
console.log('IndexedDB 初始化成功');
resolve(true);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
console.log('正在创建/升级数据库结构...');
// 创建设置存储
if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.SETTINGS)) {
const settingsStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.SETTINGS, { keyPath: 'id' });
settingsStore.createIndex('timestamp', 'timestamp', { unique: false });
}
// 创建聊天历史存储
if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY)) {
const chatStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY, { keyPath: 'id', autoIncrement: true });
chatStore.createIndex('timestamp', 'timestamp', { unique: false });
chatStore.createIndex('role', 'role', { unique: false });
}
// 创建图像记录存储
if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES)) {
const imagesStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES, { keyPath: 'id', autoIncrement: true });
imagesStore.createIndex('timestamp', 'timestamp', { unique: false });
imagesStore.createIndex('url', 'url', { unique: false });
}
};
});
return this.initPromise;
},
// 保存设置
saveSettings: async function(settings) {
if (!this.isSupported) {
return false;
}
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readwrite');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS);
const data = { id: 'current', ...settings, timestamp: Date.now() };
await new Promise((resolve, reject) => {
const request = store.put(data);
request.onsuccess = () => resolve(true);
request.onerror = () => reject(request.error);
});
return true;
} catch (error) {
console.error('IndexedDB 保存设置失败:', error);
return false;
}
},
// 获取设置
getSettings: async function() {
if (!this.isSupported) {
return null;
}
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readonly');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS);
return new Promise((resolve, reject) => {
const request = store.get('current');
request.onsuccess = () => {
const result = request.result;
if (result) {
// 移除 id 和 timestamp 字段
const { id, timestamp, ...settings } = result;
resolve(settings);
} else {
resolve(null);
}
};
request.onerror = () => reject(request.error);
});
} catch (error) {
console.error('IndexedDB 获取设置失败:', error);
return null;
}
},
// 添加聊天消息
addChatMessage: async function(role, content) {
if (!this.isSupported) {
return false;
}
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readwrite');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY);
const message = {
role: role,
content: content,
timestamp: Date.now()
};
await new Promise((resolve, reject) => {
const request = store.add(message);
request.onsuccess = () => resolve(true);
request.onerror = () => reject(request.error);
});
// 清理旧消息
await this.cleanupOldMessages();
return true;
} catch (error) {
console.error('IndexedDB 添加聊天消息失败:', error);
return false;
}
},
// 获取聊天历史
getChatHistory: async function() {
if (!this.isSupported) {
return [];
}
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readonly');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY);
const index = store.index('timestamp');
return new Promise((resolve, reject) => {
const request = index.getAll();
request.onsuccess = () => {
const messages = request.result || [];
// 按时间戳排序
messages.sort((a, b) => a.timestamp - b.timestamp);
resolve(messages);
};
request.onerror = () => reject(request.error);
});
} catch (error) {
console.error('IndexedDB 获取聊天历史失败:', error);
return [];
}
},
// 添加生成的图像
addGeneratedImage: async function(imageId, imageUrl) {
if (!this.isSupported) {
return false;
}
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readwrite');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES);
const image = {
imageId: imageId,
url: imageUrl,
timestamp: Date.now()
};
await new Promise((resolve, reject) => {
const request = store.add(image);
request.onsuccess = () => resolve(true);
request.onerror = () => reject(request.error);
});
// 清理旧图像
await this.cleanupOldImages();
return true;
} catch (error) {
console.error('IndexedDB 添加图像记录失败:', error);
return false;
}
},
// 获取生成的图像
getGeneratedImages: async function() {
if (!this.isSupported) {
return [];
}
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readonly');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES);
const index = store.index('timestamp');
return new Promise((resolve, reject) => {
const request = index.getAll();
request.onsuccess = () => {
const images = request.result || [];
// 按时间戳排序
images.sort((a, b) => a.timestamp - b.timestamp);
resolve(images.map(img => ({
id: img.imageId,
url: img.url
})));
};
request.onerror = () => reject(request.error);
});
} catch (error) {
console.error('IndexedDB 获取图像记录失败:', error);
return [];
}
},
// 清理旧聊天消息
cleanupOldMessages: async function() {
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readwrite');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY);
const countRequest = store.count();
countRequest.onsuccess = () => {
const count = countRequest.result;
if (count > CONFIG.MAX_CHAT_HISTORY) {
const index = store.index('timestamp');
const request = index.openCursor();
let deleteCount = count - CONFIG.MAX_CHAT_HISTORY;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && deleteCount > 0) {
cursor.delete();
deleteCount--;
cursor.continue();
}
};
}
};
} catch (error) {
console.error('清理旧聊天消息失败:', error);
}
},
// 清理旧图像记录
cleanupOldImages: async function() {
try {
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readwrite');
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES);
const countRequest = store.count();
countRequest.onsuccess = () => {
const count = countRequest.result;
if (count > CONFIG.MAX_GENERATED_IMAGES) {
const index = store.index('timestamp');
const request = index.openCursor();
let deleteCount = count - CONFIG.MAX_GENERATED_IMAGES;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && deleteCount > 0) {
cursor.delete();
deleteCount--;
cursor.continue();
}
};
}
};
} catch (error) {
console.error('清理旧图像记录失败:', error);
}
},
// 清除所有数据
clearAllData: async function() {
if (!this.isSupported) {
return false;
}
try {
const transaction = this.db.transaction([
CONFIG.INDEXED_DB.STORES.SETTINGS,
CONFIG.INDEXED_DB.STORES.CHAT_HISTORY,
CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES
], 'readwrite');
const promises = [
new Promise((resolve) => {
const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS).clear();
request.onsuccess = () => resolve();
}),
new Promise((resolve) => {
const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY).clear();
request.onsuccess = () => resolve();
}),
new Promise((resolve) => {
const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES).clear();
request.onsuccess = () => resolve();
})
];
await Promise.all(promises);
return true;
} catch (error) {
console.error('IndexedDB 清除所有数据失败:', error);
return false;
}
},
// 数据迁移从localStorage到IndexedDB
migrateFromLocalStorage: async function() {
if (!this.isSupported) {
return false;
}
try {
console.log('开始从 localStorage 迁移数据到 IndexedDB...');
// 迁移设置
const savedSettings = localStorage.getItem(CONFIG.STORAGE_KEYS.SETTINGS);
if (savedSettings) {
const settings = JSON.parse(savedSettings);
await this.saveSettings(settings);
console.log('设置已迁移到 IndexedDB');
}
// 迁移聊天历史
const savedChatHistory = localStorage.getItem(CONFIG.STORAGE_KEYS.CHAT_HISTORY);
if (savedChatHistory) {
const messages = JSON.parse(savedChatHistory);
for (const msg of messages) {
await this.addChatMessage(msg.role, msg.content);
}
console.log(`已迁移 ${messages.length} 条聊天记录到 IndexedDB`);
}
// 迁移图像记录
const savedImages = localStorage.getItem(CONFIG.STORAGE_KEYS.GENERATED_IMAGES);
if (savedImages) {
const images = JSON.parse(savedImages);
for (const img of images) {
await this.addGeneratedImage(img.id, img.url);
}
console.log(`已迁移 ${images.length} 条图像记录到 IndexedDB`);
}
console.log('数据迁移完成');
return true;
} catch (error) {
console.error('数据迁移失败:', error);
return false;
}
}
};
// API服务
const apiService = {
// 测试连接
testConnection: async function(apiKey, baseUrl) {
try {
const response = await fetch(`${baseUrl}/models`, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
return { success: true, data: await response.json() };
} else {
return { success: false, error: response.statusText };
}
} catch (error) {
return { success: false, error: error.message };
}
},
// 生成图像
generateImage: async function(message, images, settings) {
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: message }
]
}
];
// 添加上传的图像
images.forEach(img => {
messages[0].content.push({
type: 'image_url',
image_url: { url: img.data }
});
});
const payload = {
model: settings.model,
messages: messages,
stream: false
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), settings.timeout * 1000);
try {
const response = await fetch(`${settings.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${settings.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 解析响应
const choice = data.choices[0];
const messageContent = choice.message.content;
const images = [];
// 提取图像数据
if (choice.message.images) {
choice.message.images.forEach(img => {
images.push(img.image_url.url);
});
}
return {
success: true,
content: messageContent,
images: images,
usage: data.usage
};
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
};
// UI控制器
const uiController = {
// 更新连接状态
updateConnectionStatus: function(connected) {
const statusIndicator = document.getElementById('connectionStatus');
const connectionText = document.getElementById('connectionText');
if (connected) {
statusIndicator.className = 'status-indicator status-connected';
connectionText.textContent = '已连接';
} else {
statusIndicator.className = 'status-indicator status-disconnected';
connectionText.textContent = '未连接';
}
},
// 显示加载状态
showLoading: function(show) {
const loadingSpinner = document.getElementById('loadingSpinner');
loadingSpinner.style.display = show ? 'block' : 'none';
},
// 隐藏传统加载状态,用于批量生成模式
hideTraditionalLoading: function() {
const loadingSpinner = document.getElementById('loadingSpinner');
loadingSpinner.style.display = 'none';
},
// 添加聊天消息
addChatMessage: async function(role, content) {
const chatHistoryDiv = document.getElementById('chatHistory');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${role} fade-in`;
const timestamp = new Date().toLocaleTimeString();
messageDiv.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>${role === 'user' ? '用户' : '助手'}</strong>
<small class="text-muted ms-2">${timestamp}</small>
</div>
</div>
<div class="mt-2">${this.escapeHtml(content)}</div>
`;
chatHistoryDiv.appendChild(messageDiv);
chatHistoryDiv.scrollTop = chatHistoryDiv.scrollHeight;
// 添加到内存中的聊天历史
chatHistory.push({ role: role, content: content });
// 保存到存储 - 优先使用 IndexedDB
await this.saveChatHistory(role, content);
},
// 转义HTML
escapeHtml: function(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
// 添加生成的图像
addGeneratedImage: async function(imageUrl) {
const gallery = document.getElementById('imageGallery');
const imageDiv = document.createElement('div');
imageDiv.className = 'image-item fade-in';
const imageId = Date.now() + Math.random();
imageDiv.innerHTML = `
<img src="${imageUrl}" alt="Generated Image" loading="lazy" class="generated-image">
<div class="image-overlay">
<div class="btn-grid">
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${imageUrl}" title="查看大图">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${imageUrl}" title="下载图像">
<i class="fas fa-download"></i>
</button>
</div>
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${imageUrl}" title="复制URL">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${imageId}" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
gallery.appendChild(imageDiv);
// 添加事件监听器
const viewLargeBtn = imageDiv.querySelector('.view-large-btn');
const downloadBtn = imageDiv.querySelector('.download-btn');
const copyBtn = imageDiv.querySelector('.copy-url-btn');
const removeBtn = imageDiv.querySelector('.remove-btn');
if (viewLargeBtn) {
viewLargeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.viewLargeImage(imageUrl);
});
}
if (downloadBtn) {
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.downloadImage(imageUrl);
});
}
if (copyBtn) {
copyBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.copyImageUrl(imageUrl);
});
}
if (removeBtn) {
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.removeGeneratedImage(imageId);
});
}
// 添加到内存中的图像记录
generatedImages.push({ id: imageId, url: imageUrl });
// 保存到存储 - 优先使用 IndexedDB
await this.saveGeneratedImages(imageId, imageUrl);
},
// 添加占位图像
addImagePlaceholder: function(index, total) {
const gallery = document.getElementById('imageGallery');
const placeholderDiv = document.createElement('div');
placeholderDiv.className = 'image-placeholder fade-in';
placeholderDiv.id = `placeholder-${index}`;
placeholderDiv.innerHTML = `
<div class="image-placeholder-content">
<div class="image-placeholder-spinner"></div>
<i class="fas fa-image image-placeholder-icon"></i>
<div class="image-placeholder-text">
正在生成图像 ${index + 1}/${total}
</div>
</div>
`;
gallery.appendChild(placeholderDiv);
return placeholderDiv;
},
// 替换占位图像为实际图像
replacePlaceholderWithImage: function(placeholderId, imageUrl) {
const placeholder = document.getElementById(placeholderId);
if (!placeholder) return;
const imageId = Date.now() + Math.random();
placeholder.className = 'image-item fade-in';
placeholder.id = '';
placeholder.innerHTML = `
<img src="${imageUrl}" alt="Generated Image" loading="lazy" class="generated-image">
<div class="image-overlay">
<div class="btn-grid">
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${imageUrl}" title="查看大图">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${imageUrl}" title="下载图像">
<i class="fas fa-download"></i>
</button>
</div>
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${imageUrl}" title="复制URL">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${imageId}" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
// 添加事件监听器
const viewLargeBtn = placeholder.querySelector('.view-large-btn');
const downloadBtn = placeholder.querySelector('.download-btn');
const copyBtn = placeholder.querySelector('.copy-url-btn');
const removeBtn = placeholder.querySelector('.remove-btn');
if (viewLargeBtn) {
viewLargeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.viewLargeImage(imageUrl);
});
}
if (downloadBtn) {
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.downloadImage(imageUrl);
});
}
if (copyBtn) {
copyBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.copyImageUrl(imageUrl);
});
}
if (removeBtn) {
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.removeGeneratedImage(imageId);
});
}
// 添加到内存中的图像记录
generatedImages.push({ id: imageId, url: imageUrl });
// 保存到存储 - 优先使用 IndexedDB
this.saveGeneratedImages(imageId, imageUrl);
},
// 显示批量生成状态
showBatchStatus: function(message, show = true) {
let statusDiv = document.getElementById('batchStatus');
if (!statusDiv) {
statusDiv = document.createElement('div');
statusDiv.id = 'batchStatus';
statusDiv.className = 'batch-status';
document.body.appendChild(statusDiv);
}
statusDiv.textContent = message;
if (show) {
statusDiv.classList.add('show');
} else {
statusDiv.classList.remove('show');
}
},
// 新的查看大图功能
viewLargeImage: function(imageUrl) {
try {
const modalElement = document.getElementById('imageViewerModal');
if (!modalElement) {
utils.showNotification('无法打开图像查看:找不到模态框元素', 'danger');
return;
}
// 检查 Bootstrap 是否可用
if (typeof bootstrap === 'undefined') {
utils.showNotification('无法打开图像查看Bootstrap 未加载', 'danger');
return;
}
const modalImage = document.getElementById('viewerModalImage');
const downloadButton = document.getElementById('viewerDownloadImage');
if (!modalImage) {
utils.showNotification('无法打开图像查看:找不到图像元素', 'danger');
return;
}
// 设置图像源
modalImage.src = imageUrl;
modalImage.onerror = function() {
utils.showNotification('图像加载失败', 'danger');
};
// 设置下载按钮功能
if (downloadButton) {
downloadButton.onclick = function() {
app.downloadImage(imageUrl);
};
}
// 创建并显示模态框
const modal = new bootstrap.Modal(modalElement);
modal.show();
} catch (error) {
utils.showNotification(`无法打开图像查看: ${error.message}`, 'danger');
// 备用方案:在新窗口中打开图像
try {
window.open(imageUrl, '_blank');
} catch (e) {
// 静默处理错误
}
}
},
// 显示图像预览
displayImagePreview: function(imageData) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = `
<div class="card h-100">
<img src="${imageData.data}" class="card-img-top image-preview h-75" alt="${imageData.name}" loading="lazy" style="object-fit: contain;">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">${utils.formatFileSize(imageData.size)}</small>
<button class="btn btn-sm btn-outline-danger" onclick="app.removeImage(${imageData.id})" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
},
// 保存聊天历史 - 新增消息时调用
saveChatHistory: async function(role, content) {
if (!indexedDBStorage.isSupported) {
console.warn('IndexedDB 不支持,无法保存聊天历史');
return;
}
try {
const saved = await indexedDBStorage.addChatMessage(role, content);
if (!saved) {
console.error('IndexedDB 保存聊天历史失败');
}
} catch (error) {
console.error('保存聊天历史失败:', error);
}
},
// 保存生成的图像 - 新增图像时调用
saveGeneratedImages: async function(imageId, imageUrl) {
if (!indexedDBStorage.isSupported) {
console.warn('IndexedDB 不支持,无法保存图像记录');
return;
}
try {
const saved = await indexedDBStorage.addGeneratedImage(imageId, imageUrl);
if (!saved) {
console.error('IndexedDB 保存图像记录失败');
}
} catch (error) {
console.error('保存生成的图像失败:', error);
}
},
// 加载设置
loadSettings: async function() {
if (!indexedDBStorage.isSupported) {
console.warn('IndexedDB 不支持,无法加载设置');
return;
}
try {
// 从IndexedDB加载设置
const indexedSettings = await indexedDBStorage.getSettings();
if (indexedSettings) {
currentSettings = { ...currentSettings, ...indexedSettings };
}
// 更新UI
document.getElementById('apiKey').value = currentSettings.apiKey;
document.getElementById('baseUrl').value = currentSettings.baseUrl;
document.getElementById('model').value = currentSettings.model;
document.getElementById('timeout').value = currentSettings.timeout;
document.getElementById('proxy').value = currentSettings.proxy;
// 加载聊天历史
const indexedChatHistory = await indexedDBStorage.getChatHistory();
if (indexedChatHistory && indexedChatHistory.length > 0) {
chatHistory = indexedChatHistory;
}
// 重建聊天UI
const self = this;
chatHistory.forEach(msg => {
const chatHistoryDiv = document.getElementById('chatHistory');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${msg.role} fade-in`;
const timestamp = new Date(msg.timestamp || Date.now()).toLocaleTimeString();
messageDiv.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>${msg.role === 'user' ? '用户' : '助手'}</strong>
<small class="text-muted ms-2">${timestamp}</small>
</div>
</div>
<div class="mt-2">${self.escapeHtml(msg.content)}</div>
`;
chatHistoryDiv.appendChild(messageDiv);
});
// 加载生成的图像
const indexedImages = await indexedDBStorage.getGeneratedImages();
if (indexedImages && indexedImages.length > 0) {
generatedImages = indexedImages;
}
// 重建图像UI
generatedImages.forEach(img => {
const gallery = document.getElementById('imageGallery');
const imageDiv = document.createElement('div');
imageDiv.className = 'image-item fade-in';
imageDiv.innerHTML = `
<img src="${img.url}" alt="Generated Image" loading="lazy" class="generated-image">
<div class="image-overlay">
<div class="btn-grid">
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${img.url}" title="查看大图">
<i class="fas fa-search-plus"></i>
</button>
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${img.url}" title="下载图像">
<i class="fas fa-download"></i>
</button>
</div>
<div class="btn-grid-row">
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${img.url}" title="复制URL">
<i class="fas fa-copy"></i>
</button>
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${img.id}" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
gallery.appendChild(imageDiv);
// 添加事件监听器
const viewLargeBtn = imageDiv.querySelector('.view-large-btn');
const downloadBtn = imageDiv.querySelector('.download-btn');
const copyBtn = imageDiv.querySelector('.copy-url-btn');
const removeBtn = imageDiv.querySelector('.remove-btn');
if (viewLargeBtn) {
viewLargeBtn.addEventListener('click', (e) => {
e.stopPropagation();
uiController.viewLargeImage(img.url);
});
}
if (downloadBtn) {
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.downloadImage(img.url);
});
}
if (copyBtn) {
copyBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.copyImageUrl(img.url);
});
}
if (removeBtn) {
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
app.removeGeneratedImage(img.id);
});
}
});
} catch (error) {
console.error('加载设置失败:', error);
}
},
// 移除生成的图像
removeGeneratedImage: function(imageId) {
// 从内存中移除
generatedImages = generatedImages.filter(img => img.id !== imageId);
// 从DOM中移除
const gallery = document.getElementById('imageGallery');
const imageElements = gallery.querySelectorAll('.image-item');
imageElements.forEach(element => {
const removeBtn = element.querySelector('.remove-btn');
if (removeBtn && removeBtn.dataset.imageId == imageId) {
element.remove();
}
});
// 不需要再更新存储,因为 IndexedDB 会自动管理
},
// 保存设置
saveSettings: async function() {
if (!indexedDBStorage.isSupported) {
console.warn('IndexedDB 不支持,无法保存设置');
return;
}
try {
currentSettings.apiKey = document.getElementById('apiKey').value;
currentSettings.baseUrl = document.getElementById('baseUrl').value;
currentSettings.model = document.getElementById('model').value;
currentSettings.timeout = parseInt(document.getElementById('timeout').value);
currentSettings.proxy = document.getElementById('proxy').value;
const saved = await indexedDBStorage.saveSettings(currentSettings);
if (!saved) {
console.error('IndexedDB 保存设置失败');
}
} catch (error) {
console.error('保存设置失败:', error);
}
}
};
// 文件处理
const fileHandler = {
// 处理拖拽
handleDragOver: function(e) {
e.preventDefault();
e.currentTarget.classList.add('dragover');
},
handleDragLeave: function(e) {
e.currentTarget.classList.remove('dragover');
},
handleDrop: function(e) {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
const files = e.dataTransfer.files;
app.handleFiles(files);
},
// 处理文件选择
handleFileSelect: function(e) {
const files = e.target.files;
app.handleFiles(files);
},
// 处理文件
handleFiles: function(files) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = '';
// 限制文件数量
const filesToProcess = Array.from(files).slice(0, CONFIG.MAX_FILES_PER_UPLOAD);
filesToProcess.forEach(file => {
try {
utils.validateFile(file);
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const imageData = {
id: Date.now() + Math.random(),
data: e.target.result,
name: file.name,
type: file.type,
size: file.size
};
uploadedImages.push(imageData);
uiController.displayImagePreview(imageData);
};
reader.readAsDataURL(file);
}
} catch (error) {
utils.showNotification(`${file.name}: ${error.message}`, 'danger');
}
});
}
};
// 主应用对象
const app = {
// 初始化
init: async function() {
this.initializeEventListeners();
// 初始化 IndexedDB
try {
console.log('正在初始化 IndexedDB...');
const indexedDBSupported = await indexedDBStorage.init();
if (indexedDBSupported) {
console.log('IndexedDB 初始化成功,将优先使用 IndexedDB 存储');
// 尝试迁移数据
await indexedDBStorage.migrateFromLocalStorage();
} else {
console.warn('IndexedDB 不可用,将使用 localStorage 作为后备');
}
} catch (error) {
console.error('IndexedDB 初始化失败:', error);
}
// 启动时检查存储空间
await utils.checkAndCleanStorage();
await utils.checkStorageStatus();
await uiController.loadSettings();
console.log('OpenRouter Image Generator initialized');
},
// 初始化事件监听器
initializeEventListeners: function() {
// 文件拖拽
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', fileHandler.handleDragOver);
dropZone.addEventListener('dragleave', fileHandler.handleDragLeave);
dropZone.addEventListener('drop', fileHandler.handleDrop);
// 文件选择
document.getElementById('imageInput').addEventListener('change', fileHandler.handleFileSelect);
// 回车发送消息
const messageInput = document.getElementById('messageInput');
const self = this;
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
self.sendMessage();
}
});
// 设置变化时自动保存
const inputs = ['apiKey', 'baseUrl', 'model', 'timeout', 'proxy'];
inputs.forEach(id => {
document.getElementById(id).addEventListener('change', utils.debounce(() => {
uiController.saveSettings();
}, 1000));
});
// 页面卸载时保存设置
window.addEventListener('beforeunload', () => {
uiController.saveSettings();
});
},
// 测试连接
testConnection: async function() {
const apiKey = document.getElementById('apiKey').value;
const baseUrl = document.getElementById('baseUrl').value;
if (!apiKey) {
utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning');
return;
}
uiController.showLoading(true);
try {
const result = await apiService.testConnection(apiKey, baseUrl);
if (result.success) {
uiController.updateConnectionStatus(true);
utils.showNotification('连接成功!', 'success');
} else {
uiController.updateConnectionStatus(false);
utils.showNotification(`连接失败:${result.error}`, 'danger');
}
} catch (error) {
uiController.updateConnectionStatus(false);
utils.showNotification(`连接失败:${error.message}`, 'danger');
} finally {
uiController.showLoading(false);
}
},
// 发送消息 - 生成一张图像(使用批量生成方法)
sendMessage: async function() {
// 临时设置批量数量为1
const batchCountInput = document.getElementById('batchCount');
const originalBatchCount = batchCountInput.value;
batchCountInput.value = 1;
// 调用批量生成方法
await this.sendBatchMessage();
// 恢复原始批量数量
batchCountInput.value = originalBatchCount;
},
// 批量发送消息
sendBatchMessage: async function() {
const messageInput = document.getElementById('messageInput');
const batchCountInput = document.getElementById('batchCount');
const message = messageInput.value.trim();
const batchCount = parseInt(batchCountInput.value) || 6;
if (!message) {
utils.showNotification('请输入消息', 'warning');
return;
}
if (batchCount < 1 || batchCount > 10) {
utils.showNotification('请输入有效的生成数量1-10', 'warning');
return;
}
const apiKey = document.getElementById('apiKey').value;
if (!apiKey) {
utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning');
return;
}
// 隐藏传统加载状态,使用占位图像代替
uiController.hideTraditionalLoading();
// 添加用户消息到聊天历史
uiController.addChatMessage('user', message);
// 创建占位图像
const placeholders = [];
for (let i = 0; i < batchCount; i++) {
const placeholder = uiController.addImagePlaceholder(i, batchCount);
placeholders.push(placeholder);
}
// 显示批量生成状态
uiController.showBatchStatus(`正在批量生成 ${batchCount} 张图像...`);
let successCount = 0;
let failCount = 0;
let firstResponse = null;
let completedCount = 0;
// 同时发送所有请求,但每个请求完成后立即处理
const promises = [];
for (let i = 0; i < batchCount; i++) {
promises.push(
this.generateImageWithRetry(message, uploadedImages, currentSettings, i, batchCount)
.then(result => {
if (result.success) {
// 保存第一个响应用于添加聊天消息
if (!firstResponse) {
firstResponse = result.response;
uiController.addChatMessage('assistant', result.response.content);
}
// 显示生成的图像
if (result.response.images && result.response.images.length > 0) {
// 如果返回多个图像,为每个图像创建新的占位符
if (result.response.images.length > 1) {
// 移除原始占位符
const originalPlaceholder = document.getElementById(`placeholder-${i}`);
if (originalPlaceholder) {
originalPlaceholder.remove();
}
// 为每个图像添加新的图像项
result.response.images.forEach((img, imgIndex) => {
uiController.addGeneratedImage(img);
});
} else {
// 只有一个图像,直接替换占位符
uiController.replacePlaceholderWithImage(`placeholder-${i}`, result.response.images[0]);
}
successCount++;
} else {
// 没有图像数据但请求成功这种情况应该不会发生因为generateImageWithRetry已经检查了
failCount++;
}
} else {
// 生成失败,检查是否需要保留占位符
if (!result.keepPlaceholder) {
// 移除占位符(旧的行为)
const placeholder = document.getElementById(`placeholder-${i}`);
if (placeholder) {
placeholder.remove();
}
}
failCount++;
}
// 更新完成计数和状态
completedCount++;
uiController.showBatchStatus(`已完成 ${completedCount}/${batchCount} 张图像...`);
return { index: i, success: result.success };
})
);
}
// 等待所有请求完成
await Promise.all(promises);
// 检查存储空间
await utils.checkAndCleanStorage();
// 清空输入框
messageInput.value = '';
// 隐藏批量生成状态
setTimeout(() => {
uiController.showBatchStatus('', false);
}, 3000);
// 显示最终结果
if (failCount === 0) {
utils.showNotification(`批量生成完成!成功生成 ${successCount} 张图像`, 'success');
} else {
utils.showNotification(`批量生成完成!成功 ${successCount} 张,失败 ${failCount}`, 'warning');
}
},
// 带重试机制的图像生成方法
generateImageWithRetry: async function(message, images, settings, imageIndex, totalImages) {
const maxRetries = 5;
let retryCount = 0;
while (retryCount <= maxRetries) {
try {
const response = await apiService.generateImage(message, images, settings);
// 检查响应中是否包含图像
if (!response.images || response.images.length === 0) {
throw new Error('生成响应中没有图像数据');
}
return { success: true, response: response };
} catch (error) {
retryCount++;
console.error(`生成第 ${imageIndex + 1} 张图像失败,重试次数 ${retryCount}/${maxRetries}:`, error);
if (retryCount <= maxRetries) {
// 更新占位符状态,显示重试信息
const placeholder = document.getElementById(`placeholder-${imageIndex}`);
if (placeholder) {
const placeholderText = placeholder.querySelector('.image-placeholder-text');
if (placeholderText) {
placeholderText.textContent = `正在重试生成图像 ${imageIndex + 1}/${totalImages} (${retryCount}/${maxRetries})`;
}
}
// 等待一段时间后重试,使用指数退避策略
const delayTime = 1000 * Math.pow(2, retryCount - 1);
await new Promise(resolve => setTimeout(resolve, delayTime));
} else {
// 重试次数用完,返回失败,但不要移除占位符
// 更新占位符显示最终失败状态
const placeholder = document.getElementById(`placeholder-${imageIndex}`);
if (placeholder) {
const placeholderText = placeholder.querySelector('.image-placeholder-text');
if (placeholderText) {
placeholderText.textContent = `生成图像 ${imageIndex + 1}/${totalImages} 失败,已重试 ${maxRetries}`;
}
const placeholderIcon = placeholder.querySelector('.image-placeholder-icon');
if (placeholderIcon) {
placeholderIcon.className = 'fas fa-exclamation-triangle image-placeholder-icon';
placeholderIcon.style.color = '#dc3545';
}
}
return { success: false, error: error, keepPlaceholder: true };
}
}
}
// 重试次数用完,返回失败,但不要移除占位符
return { success: false, error: new Error('重试次数用完'), keepPlaceholder: true };
},
// 移除上传的图像
removeImage: function(imageId) {
uploadedImages = uploadedImages.filter(img => img.id !== imageId);
const preview = document.getElementById('imagePreview');
if (uploadedImages.length === 0) {
preview.innerHTML = '<p class="text-muted">未选择图像</p>';
} else {
preview.innerHTML = '';
uploadedImages.forEach(img => uiController.displayImagePreview(img));
}
},
// 移除生成的图像
removeGeneratedImage: function(imageId) {
uiController.removeGeneratedImage(imageId);
},
// 下载图像
downloadImage: function(imageUrl) {
const link = document.createElement('a');
link.href = imageUrl;
link.download = `generated-image-${Date.now()}.png`;
link.click();
},
// 复制图像URL
copyImageUrl: function(imageUrl) {
navigator.clipboard.writeText(imageUrl).then(() => {
utils.showNotification('图像URL已复制到剪贴板', 'success');
}).catch(() => {
utils.showNotification('复制失败,请手动复制', 'warning');
});
},
// 处理文件
handleFiles: function(files) {
fileHandler.handleFiles(files);
},
// 清除存储数据
clearStorage: async function() {
if (confirm('确定要清除所有存储的数据吗?这将删除所有聊天记录和生成的图像记录。')) {
try {
if (indexedDBStorage.isSupported) {
// 清除 IndexedDB 数据
const cleared = await indexedDBStorage.clearAllData();
if (cleared) {
console.log('IndexedDB 数据已清除');
} else {
console.error('IndexedDB 清除失败');
}
}
// 清空内存中的数据
chatHistory = [];
generatedImages = [];
uploadedImages = [];
// 更新UI
document.getElementById('chatHistory').innerHTML = '';
document.getElementById('imageGallery').innerHTML = '';
document.getElementById('imagePreview').innerHTML = '';
utils.showNotification('已成功清除所有存储数据', 'success');
} catch (error) {
console.error('清除存储数据失败:', error);
utils.showNotification('清除存储数据失败', 'danger');
}
}
}
};
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', async () => {
await app.init();
});
// 全局函数供HTML调用
window.testConnection = () => app.testConnection();
window.sendMessage = () => app.sendMessage();
window.sendBatchMessage = () => app.sendBatchMessage();
window.downloadImage = (url) => app.downloadImage(url);
window.copyImageUrl = (url) => app.copyImageUrl(url);
window.removeImage = (id) => app.removeImage(id);
window.removeGeneratedImage = (id) => app.removeGeneratedImage(id);
window.clearStorage = () => app.clearStorage();
// 调试函数
window.checkStorageStatus = () => utils.checkStorageStatus();
window.checkEventBindings = () => utils.checkEventBindings();
window.clearIndexedDB = async () => {
if (indexedDBStorage.isSupported) {
const cleared = await indexedDBStorage.clearAllData();
console.log('IndexedDB 已清空:', cleared);
}
};