diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..4798424
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "liveServer.settings.port": 5501
+}
\ No newline at end of file
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..57cf23a
--- /dev/null
+++ b/config.json
@@ -0,0 +1,8 @@
+{
+ "enableVision": true,
+ "imageConfig": {
+ "maxCount": 4,
+ "maxSizeMB": 4,
+ "allowedTypes": ["image/jpeg", "image/png", "image/webp", "image/gif"]
+ }
+}
diff --git a/css/style.css b/css/style.css
index 3425a74..7f3eab5 100644
--- a/css/style.css
+++ b/css/style.css
@@ -489,3 +489,213 @@ iconify-icon {
}
#dmermaidSvg{ height: 0px;}
+
+/* ========== 图片上传相关样式 ========== */
+
+/* 图片上传按钮 */
+.image-upload-btn {
+ flex-shrink: 0;
+ border-radius: 8px;
+ transition: all 0.2s ease;
+}
+
+.image-upload-btn:hover {
+ background: rgba(6, 182, 212, 0.1);
+ transform: scale(1.05);
+}
+
+.image-upload-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.image-upload-btn:disabled:hover {
+ background: transparent;
+ transform: none;
+}
+
+/* 图片缩略图容器 */
+#image-preview-container {
+ padding: 8px;
+ background: rgba(255, 255, 255, 0.8);
+ border: 2px dashed #d1d5db;
+ border-radius: 8px;
+}
+
+#image-preview-container:not(.hidden) {
+ display: flex;
+}
+
+/* 单个图片缩略图项 */
+.image-thumbnail-item {
+ position: relative;
+ width: 72px;
+ height: 72px;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 2px solid #e5e7eb;
+ background: #f9fafb;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.image-thumbnail-item:hover {
+ border-color: #06b6d4;
+ transform: scale(1.05);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.image-thumbnail-item img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+/* 图片删除按钮 */
+.image-thumbnail-delete {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: rgba(220, 38, 38, 0.9);
+ color: white;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ z-index: 10;
+}
+
+.image-thumbnail-item:hover .image-thumbnail-delete {
+ opacity: 1;
+}
+
+.image-thumbnail-delete:hover {
+ background: #dc2626;
+ transform: scale(1.1);
+}
+
+.image-thumbnail-delete iconify-icon {
+ font-size: 12px;
+}
+
+/* 图片数量限制提示 */
+.image-count-badge {
+ position: absolute;
+ bottom: 2px;
+ left: 2px;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ font-size: 10px;
+ padding: 1px 4px;
+ border-radius: 4px;
+}
+
+/* 图片预览模态窗 */
+.image-preview-modal-content {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 90vw;
+ height: 90vh;
+ background: rgba(0, 0, 0, 0.95);
+ border-radius: 8px;
+}
+
+.image-preview-modal-content img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ border-radius: 4px;
+}
+
+/* 聊天气泡中的图片显示 */
+.chat-bubble-images {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 8px;
+}
+
+.chat-bubble-image {
+ width: 80px;
+ height: 80px;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.chat-bubble-image:hover {
+ transform: scale(1.05);
+ border-color: rgba(255, 255, 255, 0.6);
+}
+
+.chat-bubble-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+/* 图片加载中状态 */
+.image-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f3f4f6;
+}
+
+.image-loading::after {
+ content: '';
+ width: 24px;
+ height: 24px;
+ border: 2px solid #d1d5db;
+ border-top-color: #06b6d4;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* 图片上传拖拽区域高亮 */
+.image-drop-active {
+ background: rgba(6, 182, 212, 0.1) !important;
+ border-color: #06b6d4 !important;
+ border-style: solid !important;
+}
+
+/* 图片大小/格式错误提示 */
+.image-error-toast {
+ position: fixed;
+ bottom: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #dc2626;
+ color: white;
+ padding: 12px 20px;
+ border-radius: 8px;
+ font-weight: 500;
+ z-index: 9999;
+ animation: toast-in 0.3s ease;
+}
+
+@keyframes toast-in {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 5390db0..8d1f54d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,4 +6,10 @@ services:
ports:
- "3000:3000"
container_name: product-canvas-app
- restart: unless-stopped
\ No newline at end of file
+ restart: unless-stopped
+ environment:
+ # 是否启用图片解析功能(Vision API),默认启用
+ - ENABLE_VISION=true
+ volumes:
+ # 可选:挂载自定义配置文件覆盖默认配置
+ # - ./config.json:/usr/share/nginx/html/config.json:ro
\ No newline at end of file
diff --git a/index.html b/index.html
index d320da1..cd87dc7 100644
--- a/index.html
+++ b/index.html
@@ -65,10 +65,19 @@
+
+
+
+
+
+
+
@@ -76,6 +85,11 @@
+
+
+
+ 图片解析功能已禁用
+
@@ -172,15 +186,30 @@
模型 (Model)
-
-
+
+
+
+
+
+
+
+ 允许上传图片并由AI进行视觉理解
+
+
+
+
+
@@ -251,6 +280,16 @@
+
+
+
+
+
![图片预览]()
+
+
+
diff --git a/js/apiclient.js b/js/apiclient.js
index b29ab68..cbe45dc 100644
--- a/js/apiclient.js
+++ b/js/apiclient.js
@@ -2,13 +2,23 @@
* API客户端 - 处理与AI服务的交互
*/
+// 图片上传配置常量
+const IMAGE_CONFIG = {
+ maxCount: 4, // 最大图片数量
+ maxSizeBytes: 4 * 1024 * 1024, // 单张最大4MB
+ allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
+ allowedExtensions: ['.jpg', '.jpeg', '.png', '.webp', '.gif']
+};
+
class APIClient {
constructor() {
this.config = {
url: '',
key: '',
- model: ''
+ model: '',
+ enableVision: true // 默认启用图片解析
};
+ this.runtimeConfig = null; // 运行时配置(从config.json加载)
this.promptMap = {};
this.promptFiles = {
canvas: 'prompts/canvas-prompt.txt',
@@ -32,15 +42,72 @@ class APIClient {
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
};
this.loadConfig();
+ this.loadRuntimeConfig(); // 加载运行时配置
this.preloadPrompts(Object.keys(this.promptFiles));
}
+ // 加载运行时配置(从config.json,支持Docker环境变量注入)
+ async loadRuntimeConfig() {
+ try {
+ const response = await fetch('config.json');
+ if (response.ok) {
+ this.runtimeConfig = await response.json();
+ // 运行时配置优先级高于本地存储
+ if (this.runtimeConfig.enableVision !== undefined) {
+ this.config.enableVision = this.runtimeConfig.enableVision;
+ }
+ console.log('运行时配置已加载:', this.runtimeConfig);
+
+ // 触发配置更新事件,通知UI同步
+ this.notifyConfigUpdated();
+ }
+ } catch (error) {
+ // config.json不存在时静默失败,使用默认配置
+ console.log('未找到运行时配置文件,使用默认配置');
+ }
+ }
+
+ // 触发配置更新事件
+ notifyConfigUpdated() {
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new CustomEvent('vision-config-updated', {
+ detail: {
+ enableVision: this.isVisionEnabled(),
+ isRuntimeLocked: this.runtimeConfig && this.runtimeConfig.enableVision !== undefined
+ }
+ }));
+ }
+ }
+
+ // 检查Vision配置是否被运行时锁定
+ isVisionConfigLocked() {
+ return this.runtimeConfig && this.runtimeConfig.enableVision !== undefined;
+ }
+
// 加载API配置
loadConfig() {
const savedConfig = Utils.storage.get('apiConfig');
if (savedConfig) {
this.config = { ...this.config, ...savedConfig };
}
+ // 确保enableVision有默认值
+ if (this.config.enableVision === undefined) {
+ this.config.enableVision = true;
+ }
+ }
+
+ // 检查图片解析是否启用
+ isVisionEnabled() {
+ // 运行时配置优先级最高
+ if (this.runtimeConfig && this.runtimeConfig.enableVision !== undefined) {
+ return this.runtimeConfig.enableVision;
+ }
+ return this.config.enableVision !== false;
+ }
+
+ // 获取图片配置
+ getImageConfig() {
+ return { ...IMAGE_CONFIG };
}
preloadPrompts(keys = []) {
@@ -118,29 +185,71 @@ class APIClient {
}
}
- async buildMessagesForModule(manifest, userMessage, contextMessages = []) {
+ async buildMessagesForModule(manifest, userMessage, contextMessages = [], images = []) {
const prompt =
(manifest && manifest.promptKey
? await this.ensurePrompt(manifest.promptKey)
: null) || this.promptFallbacks.default;
+ // 构建用户消息内容(支持图片)
+ const userContent = this.buildUserContent(userMessage, images);
+
return [
{ role: 'system', content: prompt },
...contextMessages,
- { role: 'user', content: userMessage }
+ { role: 'user', content: userContent }
];
}
+ /**
+ * 构建用户消息内容(支持图片的OpenAI Vision API格式)
+ * @param {string} text - 文本内容
+ * @param {Array} images - 图片数组,每项包含 { base64, mimeType }
+ * @returns {string|Array} - 纯文本或多模态内容数组
+ */
+ buildUserContent(text, images = []) {
+ // 如果没有图片或未启用Vision,返回纯文本
+ if (!images || images.length === 0 || !this.isVisionEnabled()) {
+ return text;
+ }
+
+ // 构建多模态内容数组(OpenAI Vision API格式)
+ const content = [];
+
+ // 添加图片
+ for (const img of images) {
+ content.push({
+ type: 'image_url',
+ image_url: {
+ url: `data:${img.mimeType};base64,${img.base64}`,
+ detail: 'auto' // 可选: 'low', 'high', 'auto'
+ }
+ });
+ }
+
+ // 添加文本(放在图片后面)
+ if (text && text.trim()) {
+ content.push({
+ type: 'text',
+ text: text
+ });
+ }
+
+ return content;
+ }
+
async generateModuleCompletion(
manifest,
userMessage,
contextMessages = [],
- options = {}
+ options = {},
+ images = []
) {
const messages = await this.buildMessagesForModule(
manifest,
userMessage,
- contextMessages
+ contextMessages,
+ images
);
return this.sendChatMessage(messages, options);
}
@@ -151,12 +260,14 @@ class APIClient {
contextMessages = [],
onChunk,
onComplete,
- options = {}
+ options = {},
+ images = []
) {
const messages = await this.buildMessagesForModule(
manifest,
userMessage,
- contextMessages
+ contextMessages,
+ images
);
return this.sendChatMessageStream(messages, options, onChunk, onComplete);
}
diff --git a/js/core/app-shell.js b/js/core/app-shell.js
index a18361c..eea98f6 100644
--- a/js/core/app-shell.js
+++ b/js/core/app-shell.js
@@ -34,6 +34,9 @@
this.mermaidPanZoom = null;
this.mermaidInitialized = false;
+ // 图片上传状态管理
+ this.pendingImages = []; // 待发送的图片列表 { id, file, blobUrl, base64?, mimeType }
+
this.globalStore = moduleRuntime.storageService.global();
this.activeModuleId = null;
@@ -57,6 +60,16 @@
this.el.clearHistoryBtn = document.getElementById('clear-history-btn');
this.el.chatHistory = document.getElementById('chat-history');
+ // 图片上传相关
+ this.el.imageUploadBtn = document.getElementById('image-upload-btn');
+ this.el.imageFileInput = document.getElementById('image-file-input');
+ this.el.imagePreviewContainer = document.getElementById('image-preview-container');
+ this.el.imagePreviewModal = document.getElementById('image-preview-modal');
+ this.el.imagePreviewFull = document.getElementById('image-preview-full');
+ this.el.closeImagePreviewBtn = document.getElementById('close-image-preview-btn');
+ this.el.imageVisionDisabledTip = document.getElementById('image-vision-disabled-tip');
+ this.el.configEnableVision = document.getElementById('config-enable-vision');
+
// 视图区域
this.el.viewer = document.getElementById('svg-viewer');
this.el.placeholderText =
@@ -287,6 +300,101 @@
}
});
}
+
+ // 图片上传相关事件绑定
+ this.bindImageUploadEvents();
+ }
+
+ /**
+ * 绑定图片上传相关事件
+ */
+ bindImageUploadEvents() {
+ // 图片上传按钮点击
+ if (this.el.imageUploadBtn && this.el.imageFileInput) {
+ this.el.imageUploadBtn.addEventListener('click', () => {
+ if (!this.apiClient.isVisionEnabled()) {
+ this.showImageError('图片解析功能已禁用');
+ return;
+ }
+ this.el.imageFileInput.click();
+ });
+ }
+
+ // 文件选择变化
+ if (this.el.imageFileInput) {
+ this.el.imageFileInput.addEventListener('change', (event) => {
+ const files = Array.from(event.target.files || []);
+ this.handleImageFiles(files);
+ event.target.value = ''; // 清空以允许重复选择同一文件
+ });
+ }
+
+ // 粘贴事件(支持图片粘贴)
+ if (this.el.chatInput) {
+ this.el.chatInput.addEventListener('paste', (event) => {
+ if (!this.apiClient.isVisionEnabled()) return;
+
+ const items = event.clipboardData?.items;
+ if (!items) return;
+
+ const imageFiles = [];
+ for (const item of items) {
+ if (item.type.startsWith('image/')) {
+ const file = item.getAsFile();
+ if (file) imageFiles.push(file);
+ }
+ }
+
+ if (imageFiles.length > 0) {
+ // 不阻止默认行为,允许同时粘贴文本
+ this.handleImageFiles(imageFiles);
+ }
+ });
+ }
+
+ // 图片预览容器点击事件(删除和预览)
+ if (this.el.imagePreviewContainer) {
+ this.el.imagePreviewContainer.addEventListener('click', (event) => {
+ const deleteBtn = event.target.closest('.image-thumbnail-delete');
+ if (deleteBtn) {
+ event.stopPropagation();
+ const imageId = deleteBtn.dataset.imageId;
+ this.removeImage(imageId);
+ return;
+ }
+
+ const thumbnailItem = event.target.closest('.image-thumbnail-item');
+ if (thumbnailItem) {
+ const imageId = thumbnailItem.dataset.imageId;
+ this.openImagePreview(imageId);
+ }
+ });
+ }
+
+ // 图片预览模态窗关闭
+ if (this.el.closeImagePreviewBtn) {
+ this.el.closeImagePreviewBtn.addEventListener('click', () => {
+ this.closeImagePreviewModal();
+ });
+ }
+ if (this.el.imagePreviewModal) {
+ this.el.imagePreviewModal.addEventListener('click', (event) => {
+ if (event.target === this.el.imagePreviewModal) {
+ this.closeImagePreviewModal();
+ }
+ });
+ }
+
+ // 初始化图片上传UI状态
+ this.updateImageUploadUI();
+
+ // 监听运行时配置更新事件
+ if (typeof window !== 'undefined') {
+ window.addEventListener('vision-config-updated', (event) => {
+ this.updateImageUploadUI();
+ this.updateVisionConfigUI(event.detail);
+ });
+ }
}
setupModuleSwitcher() {
@@ -461,14 +569,21 @@
const wrapper = document.createElement('div');
const manifest = this.getActiveManifest();
const actionsHtml = this.buildMessageActions(message, options);
+
+ // 构建图片HTML(用于用户消息)
+ const imagesHtml = this.buildMessageImagesHtml(message.images);
+
if (message.type === 'user') {
wrapper.className = 'flex justify-end';
wrapper.innerHTML = `
-
${Utils.escapeHtml(message.content)}
+ ${imagesHtml}
+
${Utils.escapeHtml(message.content || '')}
${actionsHtml}
`;
+ // 绑定图片点击预览事件
+ this.bindBubbleImagePreview(wrapper, message.images);
} else if (message.type === 'error') {
wrapper.className = 'flex justify-start';
wrapper.innerHTML = `
@@ -509,6 +624,47 @@
return wrapper;
}
+ /**
+ * 构建消息中图片的HTML
+ * @param {Array} images - 图片数组
+ * @returns {string} - HTML字符串
+ */
+ buildMessageImagesHtml(images) {
+ if (!images || images.length === 0) return '';
+
+ const imageItems = images.map((img, index) => {
+ const src = `data:${img.mimeType};base64,${img.base64}`;
+ return `
+
+

+
+ `;
+ }).join('');
+
+ return `${imageItems}
`;
+ }
+
+ /**
+ * 绑定气泡中图片的点击预览事件
+ * @param {HTMLElement} wrapper - 消息包装元素
+ * @param {Array} images - 图片数组
+ */
+ bindBubbleImagePreview(wrapper, images) {
+ if (!images || images.length === 0) return;
+
+ const imageElements = wrapper.querySelectorAll('.chat-bubble-image');
+ imageElements.forEach((el, index) => {
+ el.addEventListener('click', (event) => {
+ event.stopPropagation();
+ const img = images[index];
+ if (img && this.el.imagePreviewModal && this.el.imagePreviewFull) {
+ this.el.imagePreviewFull.src = `data:${img.mimeType};base64,${img.base64}`;
+ this.el.imagePreviewModal.classList.add('active');
+ }
+ });
+ });
+ }
+
buildMessageActions(message, options = {}) {
const {
allowRollback = false,
@@ -692,7 +848,8 @@
.map((msg) => ({
role: msg.type === 'user' ? 'user' : 'assistant',
// 使用原始内容作为 LLM 上下文,兼容无 rawContent 的旧记录
- content: msg.rawContent || msg.content || ''
+ content: msg.rawContent || msg.content || '',
+ images: msg.type === 'user' ? msg.images || [] : undefined
}));
this.isProcessing = true;
@@ -702,7 +859,8 @@
this.beginStreaming(manifest, {
userMessage,
- contextMessages
+ contextMessages,
+ images: userMessage.images || [] // 传递原始用户消息的图片
});
}
@@ -745,10 +903,14 @@
return nextId;
}
- sendMessage() {
+ async sendMessage() {
if (!this.el.chatInput) return;
const message = this.el.chatInput.value.trim();
- if (!message || this.isProcessing) return;
+ const hasImages = this.pendingImages.length > 0;
+
+ // 必须有文本或图片才能发送
+ if (!message && !hasImages) return;
+ if (this.isProcessing) return;
if (!this.apiClient.isConfigValid()) {
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
@@ -756,12 +918,25 @@
return;
}
+ // 准备图片数据(转换为base64)
+ let images = [];
+ if (hasImages && this.apiClient.isVisionEnabled()) {
+ try {
+ images = await this.prepareImagesForSend();
+ } catch (error) {
+ console.error('图片处理失败:', error);
+ this.showImageError('图片处理失败,请重试');
+ return;
+ }
+ }
+
const manifest = this.getActiveManifest();
const userMessage = {
id: Utils.generateId('msg'),
type: 'user',
content: message,
- timestamp: new Date().toISOString()
+ timestamp: new Date().toISOString(),
+ images: images.length > 0 ? images : undefined // 保存图片数据到消息
};
this.conversationService.appendMessage(manifest, userMessage);
@@ -769,6 +944,9 @@
this.el.chatInput.value = '';
Utils.autoResizeTextarea(this.el.chatInput);
+ // 清空待发送图片
+ this.clearPendingImages();
+
const context = this.conversationService.buildContext(manifest);
if (!context) {
console.warn('无法构建上下文,终止发送');
@@ -782,7 +960,8 @@
this.beginStreaming(manifest, {
userMessage: context.userMessage,
- contextMessages: context.contextMessages
+ contextMessages: context.contextMessages,
+ images: images // 传递图片数据
});
}
@@ -858,7 +1037,8 @@
payload.contextMessages,
handleChunk,
handleComplete,
- STREAM_DEFAULT_OPTIONS
+ STREAM_DEFAULT_OPTIONS,
+ payload.images || [] // 传递图片数据
)
.then((streamHandle) => {
this.activeStreamHandle = streamHandle;
@@ -2705,6 +2885,11 @@
if (this.el.apiUrlInput) this.el.apiUrlInput.value = config.url || '';
if (this.el.apiKeyInput) this.el.apiKeyInput.value = config.key || '';
if (this.el.apiModelInput) this.el.apiModelInput.value = config.model || '';
+ if (this.el.configEnableVision) {
+ this.el.configEnableVision.checked = config.enableVision !== false;
+ }
+ // 更新图片上传UI状态
+ this.updateImageUploadUI();
}
async testAPI() {
@@ -2721,7 +2906,8 @@
const config = {
url: this.el.apiUrlInput?.value.trim(),
key: this.el.apiKeyInput?.value.trim(),
- model: this.el.apiModelInput?.value.trim()
+ model: this.el.apiModelInput?.value.trim(),
+ enableVision: this.el.configEnableVision?.checked !== false
};
if (!config.url || !config.key || !config.model) {
this.setConfigStatus('error', '请填写完整的配置');
@@ -2729,6 +2915,8 @@
}
this.apiClient.saveConfig(config);
this.setConfigStatus('success', '配置已保存');
+ // 更新图片上传UI状态
+ this.updateImageUploadUI();
//setTimeout(() => this.closeConfigModal(), 600);
}
@@ -2776,6 +2964,255 @@
);
}
}
+
+ // ==================== 图片上传相关方法 ====================
+
+ /**
+ * 处理图片文件(上传或粘贴)
+ * @param {File[]} files - 文件列表
+ */
+ handleImageFiles(files) {
+ const config = this.apiClient.getImageConfig();
+ const currentCount = this.pendingImages.length;
+ const availableSlots = config.maxCount - currentCount;
+
+ if (availableSlots <= 0) {
+ this.showImageError(`最多只能上传 ${config.maxCount} 张图片`);
+ return;
+ }
+
+ const filesToAdd = files.slice(0, availableSlots);
+
+ for (const file of filesToAdd) {
+ // 验证文件类型
+ if (!config.allowedTypes.includes(file.type)) {
+ this.showImageError(`不支持的图片格式: ${file.type}`);
+ continue;
+ }
+
+ // 验证文件大小
+ if (file.size > config.maxSizeBytes) {
+ const maxSizeMB = config.maxSizeBytes / (1024 * 1024);
+ this.showImageError(`图片大小超过限制 (最大 ${maxSizeMB}MB)`);
+ continue;
+ }
+
+ // 创建图片对象
+ const imageObj = {
+ id: Utils.generateId('img'),
+ file: file,
+ blobUrl: URL.createObjectURL(file),
+ mimeType: file.type,
+ base64: null // 发送时再转换
+ };
+
+ this.pendingImages.push(imageObj);
+ }
+
+ this.renderImageThumbnails();
+
+ if (files.length > availableSlots) {
+ this.showImageError(`已达到最大图片数量限制 (${config.maxCount} 张)`);
+ }
+ }
+
+ /**
+ * 渲染图片缩略图
+ */
+ renderImageThumbnails() {
+ if (!this.el.imagePreviewContainer) return;
+
+ if (this.pendingImages.length === 0) {
+ this.el.imagePreviewContainer.classList.add('hidden');
+ this.el.imagePreviewContainer.innerHTML = '';
+ return;
+ }
+
+ this.el.imagePreviewContainer.classList.remove('hidden');
+ this.el.imagePreviewContainer.innerHTML = this.pendingImages.map((img, index) => `
+
+

+
+
+ `).join('');
+ }
+
+ /**
+ * 删除指定图片
+ * @param {string} imageId - 图片ID
+ */
+ removeImage(imageId) {
+ const index = this.pendingImages.findIndex(img => img.id === imageId);
+ if (index === -1) return;
+
+ const img = this.pendingImages[index];
+ // 释放Blob URL
+ if (img.blobUrl) {
+ URL.revokeObjectURL(img.blobUrl);
+ }
+
+ this.pendingImages.splice(index, 1);
+ this.renderImageThumbnails();
+ }
+
+ /**
+ * 清空所有待发送图片
+ */
+ clearPendingImages() {
+ for (const img of this.pendingImages) {
+ if (img.blobUrl) {
+ URL.revokeObjectURL(img.blobUrl);
+ }
+ }
+ this.pendingImages = [];
+ this.renderImageThumbnails();
+ }
+
+ /**
+ * 打开图片预览模态窗
+ * @param {string} imageId - 图片ID
+ */
+ openImagePreview(imageId) {
+ const img = this.pendingImages.find(i => i.id === imageId);
+ if (!img || !this.el.imagePreviewModal || !this.el.imagePreviewFull) return;
+
+ this.el.imagePreviewFull.src = img.blobUrl;
+ this.el.imagePreviewModal.classList.add('active');
+ }
+
+ /**
+ * 关闭图片预览模态窗
+ */
+ closeImagePreviewModal() {
+ if (!this.el.imagePreviewModal) return;
+ this.el.imagePreviewModal.classList.remove('active');
+ if (this.el.imagePreviewFull) {
+ this.el.imagePreviewFull.src = '';
+ }
+ }
+
+ /**
+ * 显示图片错误提示
+ * @param {string} message - 错误信息
+ */
+ showImageError(message) {
+ // 创建toast提示
+ const toast = document.createElement('div');
+ toast.className = 'image-error-toast';
+ toast.textContent = message;
+ document.body.appendChild(toast);
+
+ // 3秒后自动移除
+ setTimeout(() => {
+ toast.remove();
+ }, 3000);
+ }
+
+ /**
+ * 更新图片上传UI状态(根据enableVision配置)
+ */
+ updateImageUploadUI() {
+ const visionEnabled = this.apiClient.isVisionEnabled();
+
+ // 更新上传按钮状态
+ if (this.el.imageUploadBtn) {
+ this.el.imageUploadBtn.disabled = !visionEnabled;
+ this.el.imageUploadBtn.title = visionEnabled
+ ? '上传图片 (支持粘贴)'
+ : '图片解析功能已禁用';
+ }
+
+ // 更新禁用提示
+ if (this.el.imageVisionDisabledTip) {
+ this.el.imageVisionDisabledTip.classList.toggle('hidden', visionEnabled);
+ }
+
+ // 如果禁用了Vision,清空待发送图片
+ if (!visionEnabled && this.pendingImages.length > 0) {
+ this.clearPendingImages();
+ }
+ }
+
+ /**
+ * 更新Vision配置UI(处理运行时锁定状态)
+ * @param {Object} detail - 配置详情 { enableVision, isRuntimeLocked }
+ */
+ updateVisionConfigUI(detail) {
+ if (!this.el.configEnableVision) return;
+
+ const { enableVision, isRuntimeLocked } = detail;
+
+ // 更新复选框状态
+ this.el.configEnableVision.checked = enableVision;
+
+ // 如果被运行时配置锁定,禁用复选框并添加提示
+ if (isRuntimeLocked) {
+ this.el.configEnableVision.disabled = true;
+
+ // 添加锁定提示
+ const parent = this.el.configEnableVision.closest('.flex');
+ if (parent) {
+ let lockHint = parent.querySelector('.vision-lock-hint');
+ if (!lockHint) {
+ lockHint = document.createElement('span');
+ lockHint.className = 'vision-lock-hint text-xs text-orange-600 ml-2';
+ lockHint.innerHTML = ' 由部署配置锁定';
+ parent.appendChild(lockHint);
+ }
+ }
+ } else {
+ this.el.configEnableVision.disabled = false;
+
+ // 移除锁定提示
+ const parent = this.el.configEnableVision.closest('.flex');
+ if (parent) {
+ const lockHint = parent.querySelector('.vision-lock-hint');
+ if (lockHint) {
+ lockHint.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * 将File转换为Base64
+ * @param {File} file - 文件对象
+ * @returns {Promise} - Base64字符串(不含data:前缀)
+ */
+ fileToBase64(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ // 移除 "data:image/xxx;base64," 前缀
+ const base64 = reader.result.split(',')[1];
+ resolve(base64);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+ }
+
+ /**
+ * 准备图片数据用于API发送(并行转换优化)
+ * @returns {Promise} - 图片数据数组 [{ base64, mimeType }]
+ */
+ async prepareImagesForSend() {
+ // 并行转换所有图片为base64
+ const conversionPromises = this.pendingImages.map(async (img) => {
+ // 如果还没有转换为base64,现在转换
+ if (!img.base64) {
+ img.base64 = await this.fileToBase64(img.file);
+ }
+ return {
+ base64: img.base64,
+ mimeType: img.mimeType
+ };
+ });
+
+ return Promise.all(conversionPromises);
+ }
}
global.AppShell = AppShell;