import { Injectable } from '@angular/core'; import { FmodeParse } from 'fmode-ng/core'; import { FmodeChatCompletion, completionJSON } from 'fmode-ng/core/agent/chat/completion'; const Parse = FmodeParse.with('nova'); /** * 室内设计AI分析服务 * 使用豆包1.6模型进行设计分析 */ @Injectable({ providedIn: 'root' }) export class DesignAnalysisAIService { // AI模型配置(豆包1.6) private readonly AI_MODEL = 'fmode-1.6-cn'; constructor() {} /** * 分析参考图片,识别场景类型和设计维度 */ async analyzeReferenceImages(options: { images: string[]; textDescription?: string; spaceType?: string; conversationHistory?: Array<{ role: string; content: string }>; deepThinking?: boolean; onProgressChange?: (progress: string) => void; onContentStream?: (content: string) => void; // 新增:流式内容回调 loading?: any; }): Promise { return new Promise(async (resolve, reject) => { try { // 构建详细的分析提示词 const prompt = this.buildAnalysisPrompt( options.spaceType, options.textDescription, options.conversationHistory, options.deepThinking ); options.onProgressChange?.('正在识别场景和分析设计维度...'); // 🔥 直接使用图片URL,不转base64(参考ai-k12-daofa的实现) console.log('📸 准备传入图片URL到AI模型...'); console.log('📸 图片URL列表:', options.images); // 日志输出,帮助调试 console.log('🤖 调用豆包1.6模型进行vision分析...'); console.log('📸 图片数量:', options.images.length); console.log('📝 提示词长度:', prompt.length, '字符'); console.log('🏠 空间类型:', options.spaceType); console.log('💬 对话历史:', options.conversationHistory?.length || 0, '条'); // 检查提示词长度(建议不超过10000字符) if (prompt.length > 10000) { console.warn('⚠️ 提示词过长,可能导致API调用失败'); } // 🔥 使用completionJSON进行vision分析(严格参考ai-k12-daofa的成功实现) console.log('🚀 开始调用completionJSON进行vision分析...'); // 定义JSON schema(与提示词中的JSON格式完全一致) const outputSchema = `{ "spaceType": "空间类型(如:客餐厅一体化、主卧、玄关等)", "spacePositioning": "空间定位与场景属性的详细分析", "layout": "空间布局与动线的详细分析", "hardDecoration": "硬装系统细节的详细分析(顶面、墙面、地面、门窗)", "colorAnalysis": "色调精准分析(主色调、辅助色、色调关系)", "materials": "材质应用解析(自然材质、现代材质、材质对比)", "form": "形体与比例分析(空间形体、家具形体、造型细节)", "style": "风格与氛围营造(风格识别、氛围手法)", "suggestions": "专业优化建议(居住适配、细节优化、落地可行性)", "summary": "简洁摘要(格式: 空间类型 | 风格 | 色调 | 氛围)" }`; // 流式内容累积 let streamContent = ''; try { // 🔥 关键:使用completionJSON + vision: true + images (URL数组) console.log('📤 发送给AI的提示词:', prompt); console.log('📤 JSON Schema:', outputSchema); console.log('📤 图片URL:', options.images); const result = await completionJSON( prompt, outputSchema, // 🔥 关键:提供JSON schema (content) => { // 流式回调(模拟) console.log('📥 AI流式响应:', typeof content, content); if (content && options.onContentStream) { streamContent = content; // 将JSON转为易读文本 const displayText = typeof content === 'string' ? content : JSON.stringify(content, null, 2); options.onContentStream(displayText); options.onProgressChange?.(`已接收 ${displayText.length} 字符...`); } }, 2, // 重试次数 { model: this.AI_MODEL, vision: true, // 🔥 关键:启用vision images: options.images, // 🔥 关键:直接传URL数组 max_tokens: 8000 } ); console.log('📥 AI最终返回结果:', result); console.log('📥 返回结果类型:', typeof result); // 获取最终内容(result应该就是JSON对象) const analysisResult = result; console.log('✅ AI分析完成,返回JSON对象:', analysisResult); console.log('📝 AI返回JSON预览:', JSON.stringify(analysisResult).substring(0, 500)); // 验证返回的JSON结构 if (!analysisResult || typeof analysisResult !== 'object') { console.error('❌ AI返回格式错误,不是JSON对象'); reject(new Error('AI返回格式异常,请重试')); return; } // 检查必要字段 if (!analysisResult.spaceType || !analysisResult.spacePositioning) { console.error('❌ AI返回JSON缺少必要字段'); console.error('🔍 AI返回的完整对象:', analysisResult); reject(new Error('AI分析结果不完整,请重试')); return; } // 解析JSON结果 const analysisData = this.parseJSONAnalysis(analysisResult); console.log('📊 解析后的分析数据:', analysisData); resolve(analysisData); } catch (err: any) { console.error('❌ completionJSON失败,详细错误:', err); console.error('❌ 错误类型:', err?.constructor?.name); console.error('❌ 错误消息:', err?.message); // 🔥 关键:如果completionJSON失败,尝试使用FmodeChatCompletion作为备选方案 if (err?.message?.includes('JSON') || err?.message?.includes('格式')) { console.warn('⚠️ completionJSON解析失败,尝试使用FmodeChatCompletion备选方案...'); try { // 使用FmodeChatCompletion获取纯文本响应 const textPrompt = this.buildTextAnalysisPrompt(options.spaceType, options.textDescription); const messageList = [{ role: 'user', content: textPrompt, images: options.images }]; const completion = new FmodeChatCompletion(messageList, { model: this.AI_MODEL, max_tokens: 8000 }); let fullContent = ''; const subscription = completion.sendCompletion({ isDirect: true, }).subscribe({ next: (message: any) => { const content = message?.content || ''; if (content) { fullContent = content; options.onContentStream?.(content); } if (message?.complete && fullContent) { console.log('✅ FmodeChatCompletion备选方案成功,内容长度:', fullContent.length); const analysisData = this.parseAnalysisContent(fullContent); resolve(analysisData); subscription?.unsubscribe(); } }, error: (err2: any) => { console.error('❌ FmodeChatCompletion备选方案也失败:', err2); reject(new Error('AI分析失败,请稍后重试')); subscription?.unsubscribe(); } }); return; // 使用备选方案,不继续执行下面的reject } catch (fallbackErr: any) { console.error('❌ 备选方案失败:', fallbackErr); } } // 根据错误类型提供更具体的错误信息 let errorMessage = 'AI分析失败'; if (err?.message?.includes('500')) { errorMessage = 'AI服务暂时不可用(服务器错误),请稍后重试'; } else if (err?.message?.includes('timeout')) { errorMessage = 'AI分析超时,请减少图片数量或简化需求后重试'; } else if (err?.message?.includes('token')) { errorMessage = '提示词过长,请简化描述或减少对话历史'; } else if (err?.message) { errorMessage = `AI分析失败: ${err.message}`; } reject(new Error(errorMessage)); } } catch (error: any) { reject(new Error('分析失败: ' + error.message)); } }); } /** * 构建纯文本分析提示词(用于FmodeChatCompletion备选方案) */ private buildTextAnalysisPrompt(spaceType?: string, textDescription?: string): string { let prompt = `请对图片中的室内设计进行专业分析,从以下8个维度详细展开: 一、空间定位与场景属性 二、空间布局与动线 三、硬装系统细节 四、色调精准分析 五、材质应用解析 六、形体与比例 七、风格与氛围营造 八、专业优化建议 要求: 1. 基于图片实际视觉内容进行分析 2. 每个维度2-4个段落,每段3-5行 3. 使用专业的室内设计术语 4. 不使用Markdown符号,使用纯文本格式`; if (spaceType) { prompt += `\n5. 空间类型参考: ${spaceType}`; } if (textDescription) { prompt += `\n6. 客户需求参考: ${textDescription}`; } return prompt; } /** * 构建AI分析提示词(JSON格式输出,兼容completionJSON) * 参考ai-k12-daofa的简洁提示词风格 */ private buildAnalysisPrompt(spaceType?: string, textDescription?: string, conversationHistory?: Array<{ role: string; content: string }>, deepThinking?: boolean): string { // 🔥 关键:提示词要简洁明了,直接要求JSON格式(参考ai-k12-daofa) let prompt = `请分析图片中的室内设计,并按以下JSON格式输出: { "spaceType": "空间类型(如:客餐厅一体化、主卧、玄关等)", "spacePositioning": "空间定位与场景属性的详细分析", "layout": "空间布局与动线的详细分析", "hardDecoration": "硬装系统细节的详细分析(顶面、墙面、地面、门窗)", "colorAnalysis": "色调精准分析(主色调、辅助色、色调关系)", "materials": "材质应用解析(自然材质、现代材质、材质对比)", "form": "形体与比例分析(空间形体、家具形体、造型细节)", "style": "风格与氛围营造(风格识别、氛围手法)", "suggestions": "专业优化建议(居住适配、细节优化、落地可行性)", "summary": "简洁摘要(格式: 空间类型 | 风格 | 色调 | 氛围)" } 要求: 1. 基于图片实际视觉内容进行分析 2. 每个字段提供详细描述(200-400字) 3. 使用专业的室内设计术语 4. 不提及品牌名称,仅描述材质、色调、形态`; // 添加空间类型提示 if (spaceType) { prompt += `\n5. 空间类型参考: ${spaceType}`; } // 添加客户需求提示 if (textDescription) { prompt += `\n6. 客户需求参考: ${textDescription}`; } return prompt; } /** * 解析JSON格式的AI分析结果(新方法,处理completionJSON返回的JSON对象) */ private parseJSONAnalysis(jsonResult: any): any { console.log('📝 解析JSON分析结果...'); // 将JSON字段转换为易读的格式化文本 const formattedContent = this.formatJSONToText(jsonResult); return { rawContent: JSON.stringify(jsonResult, null, 2), // 原始JSON formattedContent: formattedContent, // 格式化文本 structuredData: { spacePositioning: jsonResult.spacePositioning || '', layout: jsonResult.layout || '', hardDecoration: jsonResult.hardDecoration || '', colorAnalysis: jsonResult.colorAnalysis || '', materials: jsonResult.materials || '', form: jsonResult.form || '', style: jsonResult.style || '', suggestions: jsonResult.suggestions || '' }, spaceType: jsonResult.spaceType || '', summary: jsonResult.summary || '', hasContent: true, timestamp: new Date().toISOString() }; } /** * 将JSON结果转换为易读的文本格式 */ private formatJSONToText(jsonResult: any): string { const sections = []; if (jsonResult.spacePositioning) { sections.push(`一、空间定位与场景属性\n\n${jsonResult.spacePositioning}\n`); } if (jsonResult.layout) { sections.push(`二、空间布局与动线\n\n${jsonResult.layout}\n`); } if (jsonResult.hardDecoration) { sections.push(`三、硬装系统细节\n\n${jsonResult.hardDecoration}\n`); } if (jsonResult.colorAnalysis) { sections.push(`四、色调精准分析\n\n${jsonResult.colorAnalysis}\n`); } if (jsonResult.materials) { sections.push(`五、材质应用解析\n\n${jsonResult.materials}\n`); } if (jsonResult.form) { sections.push(`六、形体与比例\n\n${jsonResult.form}\n`); } if (jsonResult.style) { sections.push(`七、风格与氛围营造\n\n${jsonResult.style}\n`); } if (jsonResult.suggestions) { sections.push(`八、专业优化建议\n\n${jsonResult.suggestions}\n`); } return sections.join('\n'); } /** * 解析AI分析内容(优化版:格式化处理,确保结构清晰) * @deprecated 使用parseJSONAnalysis代替 */ private parseAnalysisContent(content: string): any { console.log('📝 AI返回的原始内容长度:', content.length); console.log('📝 AI返回的内容预览:', content.substring(0, 500)); if (!content || content.length < 50) { console.warn('⚠️ AI返回内容过短或为空'); return { rawContent: content, formattedContent: content, hasContent: false, timestamp: new Date().toISOString() }; } // 格式化处理:优化段落、间距、结构 const formattedContent = this.formatAnalysisContent(content); // 提取结构化信息 const structuredData = this.extractStructuredInfo(content); return { rawContent: content, // 原始AI输出 formattedContent: formattedContent, // 格式化后的内容 structuredData: structuredData, // 结构化数据(维度分段) hasContent: content.length > 50, timestamp: new Date().toISOString() }; } /** * 格式化分析内容:优化排版、段落、间距 */ private formatAnalysisContent(content: string): string { let formatted = content; // 1. 统一维度标题格式(确保维度标题前后有空行) const dimensionPattern = /([一二三四五六七八九十]、[^\n]+)/g; formatted = formatted.replace(dimensionPattern, '\n\n$1\n'); // 2. 处理过长段落:如果段落超过300字,尝试在句号处换行 const paragraphs = formatted.split('\n'); const processedParagraphs = paragraphs.map(para => { if (para.trim().length > 300) { // 在句号、问号、感叹号后添加换行,但保持在段落内 return para.replace(/([。!?])(?=[^。!?\n]{50,})/g, '$1\n'); } return para; }); formatted = processedParagraphs.join('\n'); // 3. 清理多余空行(超过2个连续空行压缩为2个) formatted = formatted.replace(/\n{3,}/g, '\n\n'); // 4. 确保维度之间有明确的空行分隔 formatted = formatted.replace(/(一、|二、|三、|四、|五、|六、|七、|八、)/g, '\n\n$1'); // 5. 移除开头和结尾的多余空行 formatted = formatted.trim(); // 6. 确保每个维度内部段落之间有适当间距 formatted = formatted.replace(/([。!?])\s*\n(?=[^\n一二三四五六七八])/g, '$1\n\n'); // 7. 最后清理:确保格式整洁 formatted = formatted.replace(/\n{3,}/g, '\n\n'); return formatted; } /** * 提取结构化信息:将内容按维度分段 */ private extractStructuredInfo(content: string): any { const dimensions: any = { spacePositioning: '', // 空间定位与场景属性 layout: '', // 空间布局与动线 hardDecoration: '', // 硬装系统细节 colorAnalysis: '', // 色调精准分析 materials: '', // 材质应用解析 form: '', // 形体与比例 style: '', // 风格与氛围营造 suggestions: '' // 专业优化建议 }; // 按维度标题分割内容 const dimensionRegex = /([一二三四五六七八]、[^\n]+)\n+([\s\S]*?)(?=\n[一二三四五六七八]、|$)/g; let match; while ((match = dimensionRegex.exec(content)) !== null) { const title = match[1].trim(); const contentText = match[2].trim(); // 根据标题关键词匹配到对应维度 if (title.includes('空间定位') || title.includes('场景属性')) { dimensions.spacePositioning = contentText; } else if (title.includes('布局') || title.includes('动线')) { dimensions.layout = contentText; } else if (title.includes('硬装') || title.includes('系统细节')) { dimensions.hardDecoration = contentText; } else if (title.includes('色调') || title.includes('色彩')) { dimensions.colorAnalysis = contentText; } else if (title.includes('材质')) { dimensions.materials = contentText; } else if (title.includes('形体') || title.includes('比例')) { dimensions.form = contentText; } else if (title.includes('风格') || title.includes('氛围')) { dimensions.style = contentText; } else if (title.includes('建议') || title.includes('优化')) { dimensions.suggestions = contentText; } } return dimensions; } /** * 生成简洁摘要:提取关键信息,适合客服和设计师快速查看 */ generateBriefSummary(analysisData: any): string { if (!analysisData || !analysisData.rawContent) { return '暂无分析内容'; } const content = analysisData.rawContent; const summary: string[] = []; // 1. 提取空间类型 const spaceTypeMatch = content.match(/(?:这是|空间为|属于).*?([^\n]{2,20}?(?:空间|客厅|餐厅|卧室|厨房|卫生间|玄关|书房))/); if (spaceTypeMatch) { summary.push(spaceTypeMatch[1].trim()); } // 2. 提取风格关键词 const styleKeywords = ['现代', '法式', '简约', '极简', '轻奢', '新中式', '日式', '北欧', '工业风', '台式', '侘寂', '美式']; const foundStyles: string[] = []; styleKeywords.forEach(keyword => { if (content.includes(keyword) && !foundStyles.includes(keyword)) { foundStyles.push(keyword); } }); if (foundStyles.length > 0) { summary.push(foundStyles.join('+')); } // 3. 提取色调关键词 const colorKeywords = ['暖色系', '冷色系', '暖调', '冷调', '高级灰', '奶白', '米白', '暖灰', '木色', '原木色']; const foundColors: string[] = []; colorKeywords.forEach(keyword => { if (content.includes(keyword) && !foundColors.includes(keyword)) { foundColors.push(keyword); } }); if (foundColors.length > 0) { summary.push(foundColors.join('、')); } // 4. 提取氛围关键词 const moodKeywords = ['温馨', '舒适', '精致', '高级', '松弛', '静谧', '优雅', '时尚', '女性向', '男性向', '亲子']; const foundMoods: string[] = []; moodKeywords.forEach(keyword => { if (content.includes(keyword) && !foundMoods.includes(keyword)) { foundMoods.push(keyword); } }); if (foundMoods.length > 0) { summary.push(foundMoods.slice(0, 2).join('、')); } // 5. 提取关键材质 const materialKeywords = ['木材', '大理石', '瓷砖', '布艺', '皮革', '金属', '玻璃', '护墙板']; const foundMaterials: string[] = []; materialKeywords.forEach(keyword => { if (content.includes(keyword) && !foundMaterials.includes(keyword)) { foundMaterials.push(keyword); } }); if (foundMaterials.length > 0) { summary.push('主要材质:' + foundMaterials.slice(0, 3).join('、')); } return summary.length > 0 ? summary.join(' | ') : '整体设计基于图片实际内容分析'; } /** * 生成客服标注格式:提取客户要求的关键点 */ generateCustomerServiceNotes(analysisData: any, customerRequirements?: string): string { if (!analysisData) { return '暂无标注内容'; } const notes: string[] = []; // 优先使用JSON格式的structuredData,否则使用rawContent const structuredData = analysisData.structuredData; const rawContent = analysisData.rawContent || ''; // 1. 客户要求(如果有) if (customerRequirements) { notes.push(`【客户要求】\n${customerRequirements}`); } // 2. 空间类型识别 let spaceType = ''; if (structuredData?.spaceType) { spaceType = structuredData.spaceType; } else { // 从rawContent提取 const spaceMatch = rawContent.match(/(?:这是|空间为|属于).*?([^\n]{2,20}?(?:空间|客厅|餐厅|卧室|厨房|卫生间|玄关|书房|客餐厅一体化|三室两厅|两室一厅))/); if (spaceMatch) spaceType = spaceMatch[1].trim(); } if (spaceType) { notes.push(`【空间类型】\n${spaceType}`); } // 3. 风格定位(从结构化数据或rawContent提取) let styleInfo = ''; if (structuredData?.style) { // 提取风格关键词 const styleKeywords = ['现代', '法式', '简约', '极简', '轻奢', '新中式', '日式', '北欧', '工业风', '侘寂', '美式', '台式']; const foundStyles: string[] = []; styleKeywords.forEach(keyword => { if (structuredData.style.includes(keyword) && !foundStyles.includes(keyword)) { foundStyles.push(keyword); } }); if (foundStyles.length > 0) { styleInfo = foundStyles.join('+') + '风格'; } // 提取氛围描述 const moodMatch = structuredData.style.match(/(?:氛围|营造|呈现).*?([^\n。]{5,30}?(?:温馨|舒适|精致|高级|松弛|静谧|优雅|时尚))/); if (moodMatch) { styleInfo += `,${moodMatch[1].trim()}`; } } else if (rawContent) { // 从rawContent提取风格 const styleMatch = rawContent.match(/(?:风格|呈现|属于).*?([^\n。]{5,40}?(?:风格|法式|现代|简约|极简))/); if (styleMatch) styleInfo = styleMatch[1].trim(); } if (styleInfo) { notes.push(`【风格定位】\n${styleInfo}`); } // 4. 色调要求(从色彩分析提取) let colorInfo = ''; if (structuredData?.colorAnalysis) { // 提取主色调 const mainColorMatch = structuredData.colorAnalysis.match(/主色调[::]\s*([^\n。]{5,50})/); if (mainColorMatch) { colorInfo = `主色调:${mainColorMatch[1].trim()}`; } // 提取辅助色 const subColorMatch = structuredData.colorAnalysis.match(/辅助色[::]\s*([^\n。]{5,50})/); if (subColorMatch) { colorInfo += `\n辅助色:${subColorMatch[1].trim()}`; } } else if (rawContent) { // 从rawContent提取色调 const colorMatch = rawContent.match(/(?:色调|色彩|主色)[::]\s*([^\n。]{5,50})/); if (colorMatch) colorInfo = colorMatch[1].trim(); } if (colorInfo) { notes.push(`【色调要求】\n${colorInfo}`); } // 5. 材质要求(从硬装和材质维度提取) const materials: string[] = []; if (structuredData?.hardDecoration) { // 提取地面材质 const floorMatch = structuredData.hardDecoration.match(/地面[::]\s*([^\n。]{5,40})/); if (floorMatch) materials.push(`地面:${floorMatch[1].trim()}`); // 提取墙面材质 const wallMatch = structuredData.hardDecoration.match(/墙面[::]\s*([^\n。]{5,40})/); if (wallMatch) materials.push(`墙面:${wallMatch[1].trim()}`); // 提取顶面材质 const ceilingMatch = structuredData.hardDecoration.match(/顶面[::]\s*([^\n。]{5,40})/); if (ceilingMatch) materials.push(`顶面:${ceilingMatch[1].trim()}`); } // 从材质维度补充 if (structuredData?.materials && materials.length < 2) { const materialMatch = structuredData.materials.match(/(?:主要材质|材质应用)[::]\s*([^\n。]{10,60})/); if (materialMatch) materials.push(materialMatch[1].trim()); } if (materials.length > 0) { notes.push(`【材质要求】\n${materials.join('\n')}`); } // 6. 空间布局要点 if (structuredData?.layout) { const layoutMatch = structuredData.layout.match(/(?:布局特点|空间关系)[::]\s*([^\n。]{10,60})/); if (layoutMatch) { notes.push(`【布局要点】\n${layoutMatch[1].trim()}`); } } // 7. 施工注意事项(从优化建议提取) if (structuredData?.suggestions) { const attentionPoints: string[] = []; // 提取落地可行性 const feasibilityMatch = structuredData.suggestions.match(/落地可行性[::]\s*([^\n。]{10,80})/); if (feasibilityMatch) attentionPoints.push(feasibilityMatch[1].trim()); // 提取细节优化 const detailMatch = structuredData.suggestions.match(/细节优化[::]\s*([^\n。]{10,80})/); if (detailMatch) attentionPoints.push(detailMatch[1].trim()); if (attentionPoints.length > 0) { notes.push(`【施工注意】\n${attentionPoints.join('\n')}`); } } // 8. 品质要求(固定添加) notes.push(`【品质要求】\n新客户,需严格把控施工品质和材料质量`); return notes.length > 0 ? notes.join('\n\n') : '请根据分析内容补充具体要求'; } /** * 生成客户报告(此方法保留以便后续使用) */ async generateClientReport(options: { analysisData: any; spaceName: string; onContentChange?: (content: string) => void; loading?: any; }): Promise { return new Promise(async (resolve, reject) => { try { // 使用格式化后的内容 const content = options.analysisData?.formattedContent || options.analysisData?.rawContent || '暂无报告内容'; resolve(content); } catch (error: any) { reject(new Error('生成报告失败: ' + error.message)); } }); } }