Files
OpenRouter_Image/script.js
2025-08-29 11:15:44 +08:00

1354 lines
51 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';
},
// 添加聊天消息
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);
},
// 新的查看大图功能
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() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (!message) {
utils.showNotification('请输入消息', 'warning');
return;
}
const apiKey = document.getElementById('apiKey').value;
if (!apiKey) {
utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning');
return;
}
// 显示加载状态
uiController.showLoading(true);
// 添加用户消息到聊天历史
uiController.addChatMessage('user', message);
try {
const response = await apiService.generateImage(message, uploadedImages, currentSettings);
// 添加助手回复到聊天历史
uiController.addChatMessage('assistant', response.content);
// 显示生成的图像
if (response.images && response.images.length > 0) {
response.images.forEach(img => {
uiController.addGeneratedImage(img);
});
// 检查存储空间
await utils.checkAndCleanStorage();
// 提示用户存储空间有限
utils.showNotification('已生成图像,但请注意本地存储空间有限,建议及时下载重要图像', 'info');
}
// 清空输入框
messageInput.value = '';
// 显示使用统计
if (response.usage) {
console.log('API Usage:', response.usage);
}
} catch (error) {
console.error('生成图像失败:', error);
uiController.addChatMessage('assistant', `生成图像失败: ${error.message}`);
utils.showNotification(`生成图像失败: ${error.message}`, 'danger');
} finally {
uiController.showLoading(false);
}
},
// 移除上传的图像
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.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);
}
};