|
|
@@ -1,6 +1,6 @@
|
|
|
import { Injectable } from '@angular/core';
|
|
|
import { FmodeParse } from 'fmode-ng/core';
|
|
|
-import { FmodeChatCompletion } from 'fmode-ng/core/agent/chat/completion';
|
|
|
+import { FmodeChatCompletion, completionJSON } from 'fmode-ng/core/agent/chat/completion';
|
|
|
|
|
|
const Parse = FmodeParse.with('nova');
|
|
|
|
|
|
@@ -43,17 +43,12 @@ export class DesignAnalysisAIService {
|
|
|
|
|
|
options.onProgressChange?.('正在识别场景和分析设计维度...');
|
|
|
|
|
|
- // 使用FmodeChatCompletion进行流式输出
|
|
|
- const messageList = [
|
|
|
- {
|
|
|
- role: 'user',
|
|
|
- content: prompt,
|
|
|
- images: options.images // 添加图片
|
|
|
- }
|
|
|
- ];
|
|
|
+ // 🔥 直接使用图片URL,不转base64(参考ai-k12-daofa的实现)
|
|
|
+ console.log('📸 准备传入图片URL到AI模型...');
|
|
|
+ console.log('📸 图片URL列表:', options.images);
|
|
|
|
|
|
// 日志输出,帮助调试
|
|
|
- console.log('🤖 调用豆包1.6模型...');
|
|
|
+ console.log('🤖 调用豆包1.6模型进行vision分析...');
|
|
|
console.log('📸 图片数量:', options.images.length);
|
|
|
console.log('📝 提示词长度:', prompt.length, '字符');
|
|
|
console.log('🏠 空间类型:', options.spaceType);
|
|
|
@@ -64,74 +59,154 @@ export class DesignAnalysisAIService {
|
|
|
console.warn('⚠️ 提示词过长,可能导致API调用失败');
|
|
|
}
|
|
|
|
|
|
- const completion = new FmodeChatCompletion(messageList, {
|
|
|
- model: this.AI_MODEL,
|
|
|
- max_tokens: 8000, // 支持长文本输出
|
|
|
- });
|
|
|
-
|
|
|
- let fullContent = '';
|
|
|
- const subscription = completion.sendCompletion({
|
|
|
- isDirect: true,
|
|
|
- }).subscribe({
|
|
|
- next: async (message: any) => {
|
|
|
- const content = message?.content || '';
|
|
|
- fullContent = content;
|
|
|
-
|
|
|
- // 🔥 流式输出:每次接收到新内容就立即回调
|
|
|
- if (content && content.length > 0) {
|
|
|
- options.onContentStream?.(content);
|
|
|
- }
|
|
|
-
|
|
|
- if (options.loading) {
|
|
|
- options.loading.message = '正在分析设计细节...' + content.length;
|
|
|
+ // 🔥 使用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
|
|
|
}
|
|
|
- options.onProgressChange?.('正在分析设计细节...');
|
|
|
+ );
|
|
|
+
|
|
|
+ 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 (message?.complete && content) {
|
|
|
- console.log('✅ AI分析完成,原始内容长度:', content.length);
|
|
|
- console.log('📝 AI返回内容预览:', content.substring(0, 500));
|
|
|
-
|
|
|
- // 检查内容长度
|
|
|
- if (content.length < 1000) {
|
|
|
- console.warn('⚠️ AI返回内容较短,可能不完整');
|
|
|
- }
|
|
|
+ // 检查必要字段
|
|
|
+ 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 analysisData = this.parseAnalysisContent(content);
|
|
|
+ const completion = new FmodeChatCompletion(messageList, {
|
|
|
+ model: this.AI_MODEL,
|
|
|
+ max_tokens: 8000
|
|
|
+ });
|
|
|
|
|
|
- console.log('📊 解析后的分析数据:', analysisData);
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- if (!analysisData || !analysisData.hasContent) {
|
|
|
- reject(new Error('AI分析结果解析失败或内容不足'));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- resolve(analysisData);
|
|
|
- subscription?.unsubscribe();
|
|
|
+ return; // 使用备选方案,不继续执行下面的reject
|
|
|
+ } catch (fallbackErr: any) {
|
|
|
+ console.error('❌ 备选方案失败:', fallbackErr);
|
|
|
}
|
|
|
- },
|
|
|
- error: (err) => {
|
|
|
- console.error('❌ AI分析失败,详细错误:', err);
|
|
|
- console.error('❌ 错误类型:', err?.constructor?.name);
|
|
|
- console.error('❌ 错误消息:', err?.message);
|
|
|
- console.error('❌ 错误详情:', JSON.stringify(err, null, 2));
|
|
|
-
|
|
|
- // 根据错误类型提供更具体的错误信息
|
|
|
- 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));
|
|
|
- subscription?.unsubscribe();
|
|
|
}
|
|
|
- });
|
|
|
+
|
|
|
+ // 根据错误类型提供更具体的错误信息
|
|
|
+ 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));
|
|
|
@@ -140,102 +215,464 @@ export class DesignAnalysisAIService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 构建AI分析提示词(专业设计分析版本)
|
|
|
+ * 构建纯文本分析提示词(用于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 {
|
|
|
- let prompt = `🚨 重要:你必须仔细观察我上传的图片,根据图片中的实际内容进行分析。每张图片的内容都不同,你的分析结果也必须不同。不要生成模板化内容。
|
|
|
+ // 🔥 关键:提示词要简洁明了,直接要求JSON格式(参考ai-k12-daofa)
|
|
|
+ let prompt = `请分析图片中的室内设计,并按以下JSON格式输出:
|
|
|
+
|
|
|
+{
|
|
|
+ "spaceType": "空间类型(如:客餐厅一体化、主卧、玄关等)",
|
|
|
+ "spacePositioning": "空间定位与场景属性的详细分析",
|
|
|
+ "layout": "空间布局与动线的详细分析",
|
|
|
+ "hardDecoration": "硬装系统细节的详细分析(顶面、墙面、地面、门窗)",
|
|
|
+ "colorAnalysis": "色调精准分析(主色调、辅助色、色调关系)",
|
|
|
+ "materials": "材质应用解析(自然材质、现代材质、材质对比)",
|
|
|
+ "form": "形体与比例分析(空间形体、家具形体、造型细节)",
|
|
|
+ "style": "风格与氛围营造(风格识别、氛围手法)",
|
|
|
+ "suggestions": "专业优化建议(居住适配、细节优化、落地可行性)",
|
|
|
+ "summary": "简洁摘要(格式: 空间类型 | 风格 | 色调 | 氛围)"
|
|
|
+}
|
|
|
|
|
|
-你是资深室内设计师,请基于图片实际内容进行专业分析。
|
|
|
+要求:
|
|
|
+1. 基于图片实际视觉内容进行分析
|
|
|
+2. 每个字段提供详细描述(200-400字)
|
|
|
+3. 使用专业的室内设计术语
|
|
|
+4. 不提及品牌名称,仅描述材质、色调、形态`;
|
|
|
|
|
|
-【项目信息】
|
|
|
-${spaceType ? `空间类型:${spaceType}` : ''}
|
|
|
-${textDescription ? `用户需求:${textDescription}` : ''}
|
|
|
+ // 添加空间类型提示
|
|
|
+ if (spaceType) {
|
|
|
+ prompt += `\n5. 空间类型参考: ${spaceType}`;
|
|
|
+ }
|
|
|
|
|
|
-【核心要求】请仔细观察图片,从以下维度分析:`;
|
|
|
+ // 添加客户需求提示
|
|
|
+ if (textDescription) {
|
|
|
+ prompt += `\n6. 客户需求参考: ${textDescription}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ return prompt;
|
|
|
+ }
|
|
|
|
|
|
- // 添加对话历史(限制2轮)
|
|
|
- if (conversationHistory && conversationHistory.length > 0) {
|
|
|
- const recentHistory = conversationHistory.slice(-4);
|
|
|
- prompt += `\n\n【对话历史】\n`;
|
|
|
- recentHistory.forEach((msg) => {
|
|
|
- const role = msg.role === 'user' ? '用户' : 'AI';
|
|
|
- const content = msg.content.length > 200 ? msg.content.substring(0, 200) + '...' : msg.content;
|
|
|
- prompt += `${role}: ${content}\n`;
|
|
|
- });
|
|
|
+ /**
|
|
|
+ * 解析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');
|
|
|
+ }
|
|
|
|
|
|
- // 深度思考模式
|
|
|
- if (deepThinking) {
|
|
|
- prompt += `\n💡 深度模式:更详细分析设计心理、材质光影、色彩情绪。\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()
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- prompt += `
|
|
|
+ // 格式化处理:优化段落、间距、结构
|
|
|
+ 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');
|
|
|
|
|
|
-【输出格式】
|
|
|
-- 纯文字段落,不使用Markdown符号
|
|
|
-- 每个维度之间空行分隔
|
|
|
-- 使用完整句子,不要简单列举
|
|
|
-- 总计800-2000字,根据图片内容详细程度调整
|
|
|
-- 不提及品牌名称
|
|
|
+ // 4. 确保维度之间有明确的空行分隔
|
|
|
+ formatted = formatted.replace(/(一、|二、|三、|四、|五、|六、|七、|八、)/g, '\n\n$1');
|
|
|
|
|
|
-【分析维度】请观察图片,从8个维度详细分析(必须基于图片实际内容):
|
|
|
+ // 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());
|
|
|
+ }
|
|
|
|
|
|
-请现在开始分析,用完整的句子和段落描述图片中的实际内容。`;
|
|
|
-
|
|
|
- return prompt;
|
|
|
+ // 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(' | ') : '整体设计基于图片实际内容分析';
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 解析AI分析内容(简化版:直接使用AI的纯文字输出)
|
|
|
+ * 生成客服标注格式:提取客户要求的关键点
|
|
|
*/
|
|
|
- private parseAnalysisContent(content: string): any {
|
|
|
- console.log('📝 AI返回的原始内容长度:', content.length);
|
|
|
- console.log('📝 AI返回的内容预览:', content.substring(0, 500));
|
|
|
+ generateCustomerServiceNotes(analysisData: any, customerRequirements?: string): string {
|
|
|
+ if (!analysisData) {
|
|
|
+ return '暂无标注内容';
|
|
|
+ }
|
|
|
|
|
|
- // 直接返回完整的AI分析内容(纯文字格式)
|
|
|
- return {
|
|
|
- rawContent: content, // 完整的AI分析内容(纯文字格式)
|
|
|
- hasContent: content && content.length > 50, // 检查是否有内容(至少50字)
|
|
|
- timestamp: new Date().toISOString()
|
|
|
- };
|
|
|
+ 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') : '请根据分析内容补充具体要求';
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -249,8 +686,8 @@ ${textDescription ? `用户需求:${textDescription}` : ''}
|
|
|
}): Promise<string> {
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
try {
|
|
|
- // 此方法暂时返回分析数据的原始内容
|
|
|
- const content = options.analysisData?.rawContent || '暂无报告内容';
|
|
|
+ // 使用格式化后的内容
|
|
|
+ const content = options.analysisData?.formattedContent || options.analysisData?.rawContent || '暂无报告内容';
|
|
|
resolve(content);
|
|
|
} catch (error: any) {
|
|
|
reject(new Error('生成报告失败: ' + error.message));
|