design-analysis-ai.service.ts 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453
  1. import { Injectable } from '@angular/core';
  2. import { FmodeParse } from 'fmode-ng/core';
  3. import { FmodeChatCompletion, completionJSON } from 'fmode-ng/core/agent/chat/completion';
  4. const Parse = FmodeParse.with('nova');
  5. /**
  6. * 室内设计AI分析服务
  7. * 使用豆包1.6模型进行设计分析
  8. */
  9. @Injectable({
  10. providedIn: 'root'
  11. })
  12. export class DesignAnalysisAIService {
  13. // AI模型配置(豆包1.6)
  14. private readonly AI_MODEL = 'fmode-1.6-cn';
  15. // 防止重复分析的标记
  16. private isAnalyzing: boolean = false;
  17. constructor() {}
  18. /**
  19. * 分析参考图片,识别场景类型和设计维度
  20. */
  21. async analyzeReferenceImages(options: {
  22. images: string[];
  23. textDescription?: string;
  24. spaceType?: string;
  25. conversationHistory?: Array<{ role: string; content: string }>;
  26. deepThinking?: boolean;
  27. onProgressChange?: (progress: string) => void;
  28. onContentStream?: (content: string) => void; // 新增:流式内容回调
  29. loading?: any;
  30. }): Promise<any> {
  31. // 🔥 防止重复分析:如果正在分析中,直接拒绝
  32. if (this.isAnalyzing) {
  33. console.log('⚠️ [analyzeReferenceImages] 正在分析中,拒绝重复调用');
  34. return Promise.reject(new Error('正在分析中,请稍候'));
  35. }
  36. this.isAnalyzing = true;
  37. console.log('🔒 [analyzeReferenceImages] 设置分析锁');
  38. return new Promise(async (resolve, reject) => {
  39. try {
  40. // 构建详细的分析提示词
  41. const prompt = this.buildAnalysisPrompt(
  42. options.spaceType,
  43. options.textDescription,
  44. options.conversationHistory,
  45. options.deepThinking
  46. );
  47. options.onProgressChange?.('正在识别场景和分析设计维度...');
  48. // 🔥 直接使用图片URL,不转base64(参考ai-k12-daofa的实现)
  49. console.log('📸 准备传入图片URL到AI模型...');
  50. console.log('📸 图片URL列表:', options.images);
  51. // 日志输出,帮助调试
  52. console.log('🤖 调用豆包1.6模型进行vision分析...');
  53. console.log('📸 图片数量:', options.images.length);
  54. console.log('📝 提示词长度:', prompt.length, '字符');
  55. console.log('🏠 空间类型:', options.spaceType);
  56. console.log('💬 对话历史:', options.conversationHistory?.length || 0, '条');
  57. // 检查提示词长度(建议不超过10000字符)
  58. if (prompt.length > 10000) {
  59. console.warn('⚠️ 提示词过长,可能导致API调用失败');
  60. }
  61. // 🔥 使用completionJSON进行vision分析(严格参考ai-k12-daofa的成功实现)
  62. console.log('🚀 开始调用completionJSON进行vision分析...');
  63. // 定义JSON schema(与提示词中的JSON格式完全一致)
  64. const outputSchema = `{
  65. "quickSummary": {
  66. "colorTone": "色彩基调(如: 暖色调、木色和暖灰色结合)",
  67. "mainMaterials": "主要材质(如: 软装以木作为主、黑色皮革沙发)",
  68. "atmosphere": "整体氛围(如: 温暖、舒适、生活气息浓厚)"
  69. },
  70. "spaceType": "空间类型(如:客餐厅一体化、主卧、玄关等)",
  71. "spacePositioning": "空间定位与场景属性的详细分析",
  72. "layout": "空间布局与动线的详细分析",
  73. "hardDecoration": "硬装系统细节的详细分析(顶面、墙面、地面、门窗)",
  74. "colorAnalysis": "色调精准分析(主色调、辅助色、色调关系)",
  75. "materials": "材质应用解析(自然材质、现代材质、材质对比)",
  76. "form": "形体与比例分析(空间形体、家具形体、造型细节)",
  77. "style": "风格与氛围营造(风格识别、氛围手法)",
  78. "suggestions": "专业优化建议(居住适配、细节优化、落地可行性)",
  79. "summary": "简洁摘要(格式: 空间类型 | 风格 | 色调 | 氛围)"
  80. }`;
  81. // 流式内容累积
  82. let streamContent = '';
  83. try {
  84. // 🔥 关键:使用completionJSON + vision: true + images (URL数组)
  85. console.log('📤 发送给AI的提示词:', prompt);
  86. console.log('📤 JSON Schema:', outputSchema);
  87. console.log('📤 图片URL:', options.images);
  88. const result = await completionJSON(
  89. prompt,
  90. outputSchema, // 🔥 关键:提供JSON schema
  91. (content) => {
  92. // 流式回调(模拟)
  93. console.log('📥 AI流式响应:', typeof content, content);
  94. if (content && options.onContentStream) {
  95. streamContent = content;
  96. // 🔥 关键修复:将JSON转为易读的中文格式化文本
  97. let displayText: string;
  98. let jsonObject: any = null;
  99. // 1. 尝试获取JSON对象
  100. if (typeof content === 'string') {
  101. // 检查是否是JSON字符串
  102. const trimmed = content.trim();
  103. if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
  104. console.log('🔍 检测到JSON格式字符串,尝试解析...');
  105. try {
  106. jsonObject = JSON.parse(trimmed);
  107. console.log('✅ JSON解析成功,完整对象');
  108. } catch (e) {
  109. // 🔥 关键修复:JSON不完整时,提取已完整的字段并格式化显示
  110. console.log('⚠️ JSON解析失败,尝试提取部分字段...');
  111. // 尝试从不完整的JSON中提取已完成的字段
  112. jsonObject = this.extractPartialJSON(trimmed);
  113. if (jsonObject && Object.keys(jsonObject).length > 0) {
  114. console.log('✅ 成功提取部分字段:', Object.keys(jsonObject).join(', '));
  115. } else {
  116. // 完全无法提取,显示提示
  117. displayText = '🔄 正在生成分析结果...';
  118. console.log('⚠️ 无法提取有效字段,等待更多数据');
  119. }
  120. }
  121. } else {
  122. // 普通文本,直接显示
  123. displayText = content;
  124. }
  125. } else if (typeof content === 'object') {
  126. jsonObject = content;
  127. console.log('📦 收到JSON对象');
  128. }
  129. // 2. 如果是JSON对象,进行格式化
  130. if (jsonObject) {
  131. console.log('🎨 开始格式化JSON对象...');
  132. displayText = this.formatJSONToText(jsonObject);
  133. // 如果格式化失败或内容过短,使用后备方案
  134. if (!displayText || displayText.trim().length < 50) {
  135. console.log('⚠️ formatJSONToText结果过短,使用fallback...');
  136. displayText = this.fallbackFormatJSON(jsonObject);
  137. }
  138. // 最后兜底:美化JSON
  139. if (!displayText || displayText.trim().length < 20) {
  140. console.log('⚠️ fallback也失败,使用beautifyJSON...');
  141. displayText = this.beautifyJSON(jsonObject);
  142. }
  143. console.log('✅ 格式化完成,长度:', displayText.length);
  144. }
  145. // 3. 如果还没有displayText,使用默认值
  146. if (!displayText) {
  147. displayText = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
  148. }
  149. options.onContentStream(displayText);
  150. options.onProgressChange?.(`正在分析,已接收 ${displayText.length} 字符...`);
  151. }
  152. },
  153. 2, // 重试次数
  154. {
  155. model: this.AI_MODEL,
  156. vision: true, // 🔥 关键:启用vision
  157. images: options.images, // 🔥 关键:直接传URL数组
  158. max_tokens: 8000,
  159. temperature: 0.3 // 🔥 降低随机性,提高一致性(0.0-1.0,越低越确定)
  160. }
  161. );
  162. console.log('📥 AI最终返回结果:', result);
  163. console.log('📥 返回结果类型:', typeof result);
  164. // 获取最终内容(result应该就是JSON对象)
  165. const analysisResult = result;
  166. console.log('✅ AI分析完成,返回JSON对象:', analysisResult);
  167. console.log('📝 AI返回JSON预览:', JSON.stringify(analysisResult).substring(0, 500));
  168. // 验证返回的JSON结构
  169. if (!analysisResult || typeof analysisResult !== 'object') {
  170. console.error('❌ AI返回格式错误,不是JSON对象');
  171. reject(new Error('AI返回格式异常,请重试'));
  172. return;
  173. }
  174. // 检查必要字段
  175. if (!analysisResult.spaceType || !analysisResult.spacePositioning) {
  176. console.error('❌ AI返回JSON缺少必要字段');
  177. console.error('🔍 AI返回的完整对象:', analysisResult);
  178. reject(new Error('AI分析结果不完整,请重试'));
  179. return;
  180. }
  181. // 解析JSON结果
  182. const analysisData = this.parseJSONAnalysis(analysisResult);
  183. console.log('📊 解析后的分析数据:', analysisData);
  184. // 🔥 关键:在最后发送完整的格式化内容
  185. if (options.onContentStream && analysisData.formattedContent) {
  186. console.log('📤 发送最终格式化内容到UI...');
  187. options.onContentStream(analysisData.formattedContent);
  188. }
  189. // 🔥 释放分析锁
  190. this.isAnalyzing = false;
  191. console.log('🔓 [analyzeReferenceImages] 释放分析锁(成功)');
  192. resolve(analysisData);
  193. } catch (err: any) {
  194. // 🔥 释放分析锁
  195. this.isAnalyzing = false;
  196. console.log('🔓 [analyzeReferenceImages] 释放分析锁(失败)');
  197. console.error('❌ completionJSON失败,详细错误:', err);
  198. console.error('❌ 错误类型:', err?.constructor?.name);
  199. console.error('❌ 错误消息:', err?.message);
  200. // 🔥 关键:如果completionJSON失败,尝试使用FmodeChatCompletion作为备选方案
  201. if (err?.message?.includes('JSON') || err?.message?.includes('格式')) {
  202. console.warn('⚠️ completionJSON解析失败,尝试使用FmodeChatCompletion备选方案...');
  203. try {
  204. // 使用FmodeChatCompletion获取纯文本响应
  205. const textPrompt = this.buildTextAnalysisPrompt(options.spaceType, options.textDescription);
  206. const messageList = [{
  207. role: 'user',
  208. content: textPrompt,
  209. images: options.images
  210. }];
  211. const completion = new FmodeChatCompletion(messageList, {
  212. model: this.AI_MODEL,
  213. max_tokens: 8000
  214. });
  215. let fullContent = '';
  216. const subscription = completion.sendCompletion({
  217. isDirect: true,
  218. }).subscribe({
  219. next: (message: any) => {
  220. const content = message?.content || '';
  221. if (content) {
  222. fullContent = content;
  223. options.onContentStream?.(content);
  224. }
  225. if (message?.complete && fullContent) {
  226. console.log('✅ FmodeChatCompletion备选方案成功,内容长度:', fullContent.length);
  227. const analysisData = this.parseAnalysisContent(fullContent);
  228. resolve(analysisData);
  229. subscription?.unsubscribe();
  230. }
  231. },
  232. error: (err2: any) => {
  233. console.error('❌ FmodeChatCompletion备选方案也失败:', err2);
  234. reject(new Error('AI分析失败,请稍后重试'));
  235. subscription?.unsubscribe();
  236. }
  237. });
  238. return; // 使用备选方案,不继续执行下面的reject
  239. } catch (fallbackErr: any) {
  240. console.error('❌ 备选方案失败:', fallbackErr);
  241. }
  242. }
  243. // 根据错误类型提供更具体的错误信息
  244. let errorMessage = 'AI分析失败';
  245. if (err?.message?.includes('500')) {
  246. errorMessage = 'AI服务暂时不可用(服务器错误),请稍后重试';
  247. } else if (err?.message?.includes('timeout')) {
  248. errorMessage = 'AI分析超时,请减少图片数量或简化需求后重试';
  249. } else if (err?.message?.includes('token')) {
  250. errorMessage = '提示词过长,请简化描述或减少对话历史';
  251. } else if (err?.message) {
  252. errorMessage = `AI分析失败: ${err.message}`;
  253. }
  254. reject(new Error(errorMessage));
  255. }
  256. } catch (error: any) {
  257. reject(new Error('分析失败: ' + error.message));
  258. }
  259. });
  260. }
  261. /**
  262. * 构建纯文本分析提示词(用于FmodeChatCompletion备选方案)
  263. */
  264. private buildTextAnalysisPrompt(spaceType?: string, textDescription?: string): string {
  265. let prompt = `请对图片中的室内设计进行专业分析,从以下8个维度详细展开:
  266. 一、空间定位与场景属性
  267. 二、空间布局与动线
  268. 三、硬装系统细节
  269. 四、色调精准分析
  270. 五、材质应用解析
  271. 六、形体与比例
  272. 七、风格与氛围营造
  273. 八、专业优化建议
  274. 要求:
  275. 1. 基于图片实际视觉内容进行分析
  276. 2. 每个维度2-4个段落,每段3-5行
  277. 3. 使用专业的室内设计术语
  278. 4. 不使用Markdown符号,使用纯文本格式`;
  279. if (spaceType) {
  280. prompt += `\n5. 空间类型参考: ${spaceType}`;
  281. }
  282. if (textDescription) {
  283. prompt += `\n6. 客户需求参考: ${textDescription}`;
  284. }
  285. return prompt;
  286. }
  287. /**
  288. * 检测用户是否在进行单维度询问(而非完整分析)
  289. */
  290. private detectSingleDimensionQuery(userInput: string): boolean {
  291. const lowerInput = userInput.toLowerCase();
  292. // 🔥 单维度询问关键词
  293. const singleDimensionKeywords = [
  294. // 询问词
  295. '什么', '如何', '怎么', '哪些', '是否', '有没有',
  296. // 具体维度
  297. '色彩', '色调', '颜色', '配色',
  298. '材质', '材料', '用料',
  299. '布局', '动线', '空间',
  300. '灯光', '照明',
  301. '风格', '氛围',
  302. '尺寸', '大小', '面积',
  303. '建议', '优化', '改进',
  304. // 疑问形式
  305. '?', '?'
  306. ];
  307. // 🔥 完整分析关键词(如果包含这些,说明要完整分析)
  308. const fullAnalysisKeywords = [
  309. '完整', '全面', '详细分析', '整体分析',
  310. '重新分析', '再分析一次', '重新生成',
  311. '全部', '所有维度', '各个方面'
  312. ];
  313. // 如果包含完整分析关键词,返回false(不是单维度)
  314. for (const keyword of fullAnalysisKeywords) {
  315. if (lowerInput.includes(keyword)) {
  316. return false;
  317. }
  318. }
  319. // 如果包含单维度关键词,返回true
  320. for (const keyword of singleDimensionKeywords) {
  321. if (lowerInput.includes(keyword)) {
  322. return true;
  323. }
  324. }
  325. // 🔥 如果用户输入很短(<20字),可能是简单询问
  326. if (userInput.length < 20) {
  327. return true;
  328. }
  329. // 默认:如果用户输入很长(>50字),可能是要求完整分析
  330. return userInput.length < 50;
  331. }
  332. /**
  333. * 构建AI分析提示词(JSON格式输出,兼容completionJSON)
  334. * 参考ai-k12-daofa的简洁提示词风格
  335. */
  336. private buildAnalysisPrompt(spaceType?: string, textDescription?: string, conversationHistory?: Array<{ role: string; content: string }>, deepThinking?: boolean): string {
  337. // 🔥 全面优化的提示词:支持多种风格,精准识别材质和色调,支持重新分析和单维度问答
  338. const hasPreviousAnalysis = conversationHistory && conversationHistory.length > 0;
  339. // 🔥 检测用户是否在进行单维度询问
  340. const isSingleDimensionQuery = hasPreviousAnalysis && textDescription && this.detectSingleDimensionQuery(textDescription);
  341. let prompt = `你是一位专业的室内设计分析师,请仔细观察图片中的室内设计细节`;
  342. // 🔥 如果是单维度询问,使用对话模式
  343. if (isSingleDimensionQuery) {
  344. prompt += `,并针对用户的具体问题进行专业回答。
  345. 【重要说明 - 单维度问答模式】
  346. • 用户正在进行特定维度的询问(如色彩、材质、布局等)
  347. • 请直接针对用户的问题进行详细、专业的文字回答
  348. • 不需要输出完整的JSON结构分析报告
  349. • 使用自然流畅的语言,就像设计师之间的专业交流
  350. • 回答应该详细、准确,包含具体的色号、材质名称等专业术语
  351. 【用户当前问题】:${textDescription}
  352. 请针对这个问题,给出专业、详细的回答:`;
  353. return prompt;
  354. }
  355. // 🔥 否则,使用完整分析模式
  356. prompt += `,并按以下JSON格式输出专业分析:`;
  357. // 🔥 如果有对话历史,说明这是用户提出修正意见后的重新分析
  358. if (hasPreviousAnalysis) {
  359. prompt += `
  360. 【重要说明 - 完整重新分析】
  361. • 用户已经看过之前的分析,并提出了修正意见或新的要求
  362. • 请基于用户的反馈,重新生成一份完整的、修正后的分析报告
  363. • 不要继续之前的分析内容,而是输出一份全新的、完整的JSON分析结果
  364. • 特别关注用户提到的色调、材质、风格等修正意见,确保新的分析符合用户期望
  365. 【分析要求】`;
  366. }
  367. prompt += `
  368. {
  369. "quickSummary": {
  370. "colorTone": "色彩基调(如: 暖白法式偏女性向,象牙白护墙板+浅灰地面+米色木地板,软装点缀豆沙紫/湖蓝色)",
  371. "mainMaterials": "主要材质(如: 象牙白护墙板+线条装饰、大理石纹理台面、黑色雕刻家具、水晶灯、香槟金灯具、豆沙紫/湖蓝色软装)",
  372. "atmosphere": "整体氛围(如: 柔暖、精致、优雅、女性向、明亮通透)"
  373. },
  374. "spaceType": "空间类型(如:客餐厅一体化、主卧、玄关等)",
  375. "spacePositioning": "空间定位与场景属性的详细分析",
  376. "layout": "空间布局与动线的详细分析",
  377. "hardDecoration": "硬装系统细节的详细分析(顶面、墙面、地面、门窗)",
  378. "colorAnalysis": "色调精准分析",
  379. "materials": "材质应用解析",
  380. "form": "形体与比例分析(空间形体、家具形体、造型细节)",
  381. "style": "风格与氛围营造",
  382. "suggestions": "专业优化建议(居住适配、细节优化、落地可行性)",
  383. "summary": "简洁摘要(格式: 空间类型 | 风格 | 色调 | 氛围)"
  384. }
  385. 【核心分析原则 - 保证一致性】
  386. • 🔥 **客观描述优先**:基于图片中可见的元素进行描述,避免过度解读
  387. • 🔥 **细节精准识别**:准确识别护墙板、线条、大理石纹理、金属材质等细节
  388. • 🔥 **色调精准定位**:使用精确的色彩描述(淡奶灰、暖白、米色、木色等)
  389. • 🔥 **风格准确判断**:根据硬装+软装+色调综合判断(现代法式、侘寂、轻奢等)
  390. • 🔥 **保持分析一致性**:相同的视觉元素应该得出相同的结论
  391. 【关键分析要求】
  392. 0. **快速总结 (quickSummary)** - 🔥 优先级最高:
  393. • 色彩基调(colorTone):精准描述主色调组合
  394. 【现代法式女性向示例】
  395. - "暖白法式偏女性向,象牙白护墙板+浅灰地面+米色木地板,软装点缀豆沙紫/湖蓝色"
  396. - "浅色调为主,暖白色/米色为基底,黑色家具+香槟金灯具点缀"
  397. - "暖白象牙色主导,大理石灰白褐纹呼应,彩色软装增添柔美"
  398. 【侘寂风格示例】
  399. - "暖色调,木色和暖灰色结合"
  400. - "自然暖棕+柔和灰,低饱和度舒适系"
  401. 【轻奢风格示例】
  402. - "高级灰+香槟金,大理石质感"
  403. - "冷灰主导+金属光泽,精致轻奢"
  404. ⚠️ 必须明确:
  405. - 冷暖倾向(暖白/冷白/中性)
  406. - 主色调名称(象牙白/暖白/淡奶灰)
  407. - 关键配色(软装彩色/黑色对比/金属点缀)
  408. • 主要材质(mainMaterials):按重要性列举关键材质
  409. 【现代法式女性向示例】
  410. - "象牙白护墙板+线条装饰、大理石纹理台面、黑色雕刻家具、水晶灯、香槟金灯具"
  411. - "白色岩板、浅灰地砖、米色木地板、豆沙紫/湖蓝色软装、绿植装饰"
  412. 【侘寂风格示例】
  413. - "木质软装为主、黑色皮革沙发、藤编家具、混凝土墙面"
  414. ⚠️ 优先识别:
  415. - 硬装:护墙板、线条、大理石/岩板、木地板、瓷砖
  416. - 软装:家具材质(木质/皮革/布艺)、灯具(水晶/金属)
  417. - 装饰:花瓶、烛台、绿植、艺术品
  418. • 整体氛围(atmosphere):3-5个关键词
  419. 【现代法式女性向示例】
  420. - "柔暖、精致、优雅、女性向、浪漫"
  421. - "明亮通透、轻盈柔美、精致细腻"
  422. 【侘寂风格示例】
  423. - "温暖、舒适、质朴、生活气息"
  424. ⚠️ 氛围词必须与色调、材质相呼应:
  425. - 暖白+浅色 = 明亮、通透、柔和
  426. - 彩色软装 = 女性向、浪漫、柔美
  427. - 黑色对比+金属 = 精致、优雅、轻奢感
  428. 1. **色调精准分析 (colorAnalysis)** - 🔥 核心重点:
  429. • 🎨 **硬装基础色精准识别**:
  430. - **暖白色/象牙白**:偏米色的白色,带微黄调(NCS S 0502-Y50R)
  431. * 应用:护墙板、墙面涂料、顶面
  432. * 特征:柔和、温润、不刺眼
  433. - **浅灰色系**:
  434. * 淡奶灰色:带微黄调/微粉调的浅灰色(NCS S 0502-Y、S 0502-R)
  435. * 灰蓝色:带微蓝调的浅灰色(如背景墙)
  436. * 浅灰地面:大理石/瓷砖的自然灰色
  437. - **米色/奶咖色**:
  438. * 木地板:浅橡木色、枫木色
  439. * 暖米色墙面:带黄调的浅米色
  440. - **大理石纹理色**:
  441. * 白色基底+褐色/金色纹理(桌面、台面)
  442. * 白色基底+蓝绿色纹理(装饰性大理石)
  443. * 灰白色带细褐纹(地面)
  444. • 🎀 **软装点缀色精准识别** - 女性向关键:
  445. - **豆沙紫/紫罗兰色**:柔和的紫色调沙发/椅子
  446. - **湖蓝色/Tiffany蓝**:清新的蓝绿色调椅子/装饰
  447. - **香槟金/玫瑰金**:金属灯具、装饰件
  448. - **黑色**:雕刻家具、烛台、电器(形成优雅对比)
  449. - **绿植色**:自然绿色,点缀生机
  450. • 🔢 **色彩比例量化分析**:
  451. 【现代法式女性向示例】
  452. - 主色调(70%):暖白色/象牙白护墙板+墙面
  453. - 辅助色(20%):浅灰色地面+米色木地板
  454. - 点缀色(10%):豆沙紫+湖蓝色软装 + 黑色家具 + 香槟金灯具
  455. 【侘寂风格示例】
  456. - 主色调(60%):暖灰色墙面
  457. - 辅助色(30%):木色家具
  458. - 点缀色(10%):黑色皮革
  459. • 🌡️ **冷暖定位精准判断**:
  460. - **暖色系**:木色、米色、暖灰、奶咖、淡粉、豆沙紫
  461. - **中性偏暖**:暖白/象牙白、淡奶灰(带微黄调)
  462. - **清新偏冷**:湖蓝色、Tiffany蓝、灰蓝色
  463. - **冷色系**:纯灰、冷白、深蓝灰
  464. - **中性色**:黑色(优雅对比)、金色(精致点缀)
  465. • 🎯 **色调协调性分析** - 重要:
  466. - 大面积浅色(暖白+浅灰)营造明亮通透感
  467. - 软装彩色(豆沙紫+湖蓝)增加女性柔美感
  468. - 黑色家具形成优雅对比,提升精致度
  469. - 金色饰品点缀轻奢感
  470. - 自然绿植平衡色彩,增加生机
  471. • ⚠️ **避免模糊词汇,使用精准描述**:
  472. ❌ "中性灰棕色系" → ✅ "暖白法式,浅色调为主"
  473. ❌ "浅色系" → ✅ "象牙白护墙板+浅灰地面+米色木地板"
  474. ❌ "彩色点缀" → ✅ "豆沙紫沙发+湖蓝色椅子+香槟金灯具"
  475. 2. **材质应用解析 (materials)** - 全面细致:
  476. • 🏗️ **硬装材质全面识别**:
  477. 【墙面系统】
  478. * **护墙板**(法式关键特征):
  479. - 颜色:象牙白/暖白/淡奶灰
  480. - 工艺:凸起线条装饰、方框造型、哑光质感
  481. - 细节:线条宽度、阴影层次、接缝工艺
  482. * **线条装饰**(法式精髓):
  483. - 顶角线/腰线/门框线
  484. - 造型:简约直线/曲线/雕花
  485. - 材质:石膏/PU/实木
  486. * **涂料墙面**:
  487. - 暖白色/米色/浅灰色乳胶漆
  488. - 质感:哑光/丝光
  489. * **大理石/岩板背景墙**:
  490. - 白色岩板(电视墙/装饰墙)
  491. - 纹理大理石(背景装饰)
  492. 【地面系统】
  493. * **大理石地砖**:
  494. - 浅灰色柔哑面地砖(主要区域)
  495. - 纹理:细密灰白纹理/大理石自然纹
  496. - 拼接:直铺/对角铺/拼花
  497. * **木地板**:
  498. - 浅色木地板(米色/浅橡木色)
  499. - 工艺:直拼/人字拼/鱼骨拼
  500. - 质感:哑光/半哑光
  501. 【顶面系统】
  502. * 石膏线装饰/隐藏式灯带
  503. * 平顶+局部造型
  504. * 无主灯设计(筒灯+吊灯组合)
  505. 【门窗系统】
  506. * 拱门造型(法式经典元素)
  507. * 门框线条装饰
  508. • 🛋️ **软装材质细节描述** - 女性向重点:
  509. 【家具材质】
  510. * **桌子**:
  511. - 大理石圆桌(灰白褐纹/蓝绿纹理)
  512. - 黑色雕刻底座(手工雕花纹理)
  513. * **沙发/椅子**:
  514. - 豆沙紫布艺沙发(丝绒/天鹅绒质感)
  515. - 湖蓝色椅子(皮革/布艺)
  516. - 曲线造型、包裹感强
  517. * **柜子**:
  518. - 白色浮雕柜(立体雕花装饰)
  519. - 储物功能+装饰性
  520. 【灯具材质】
  521. * **水晶灯**:
  522. - 透明水晶台灯(切面反光)
  523. - 白色灯罩(布艺/丝绸质感)
  524. * **金属灯**:
  525. - 香槟金/玫瑰金树枝造型灯
  526. - 艺术造型、雕塑感
  527. 【装饰品材质】
  528. * **花瓶**:
  529. - 白色陶瓷花瓶(哑光质感)
  530. - 造型:圆润/曲线/传统
  531. * **烛台**:
  532. - 黑色烛台(亮面烤漆/陶瓷)
  533. - 组合摆放,形成节奏感
  534. * **绿植**:
  535. - 自然枝条(枯枝/绿叶)
  536. - 点缀生机、柔化空间
  537. * **墙面装饰**:
  538. - 蝴蝶/鸟类装饰(白色/金色)
  539. - 艺术挂画
  540. • 🔍 **材质质感精准描述** - 触觉与视觉:
  541. - **护墙板**:哑光漆面,肌理感,立体阴影
  542. - **大理石**:
  543. * 柔哑面(不反光,温润触感)
  544. * 天然纹理(褐色/金色/蓝绿色不规则纹路)
  545. * 质感层次(深浅交错,自然过渡)
  546. - **木质**:自然木纹,温润触感,哑光/半哑光
  547. - **金属**:
  548. * 黑色雕刻(手工痕迹,哑光质感)
  549. * 香槟金(微光泽,细腻拉丝)
  550. - **水晶/玻璃**:透明清澈,切面反光,精致感
  551. - **布艺**:丝绒柔软,天鹅绒光泽,包裹感
  552. - **陶瓷**:哑光白瓷,温润细腻,手工感
  553. 3. **风格与氛围营造 (style)** - 综合判断:
  554. • 🎭 **风格准确识别** - 基于硬装+软装+色调综合判断:
  555. 【现代法式】关键特征:
  556. - 硬装:淡奶灰/暖白/象牙白护墙板 + 线条装饰 + 大理石地面 + 拱门造型
  557. - 软装:水晶灯/金属灯 + 精致家具 + 花艺绿植装饰
  558. - 色调:暖白/象牙白为主 + 米色/浅灰辅助 + 黑色家具对比
  559. - 氛围:柔暖、精致、优雅、明亮通透
  560. 【现代法式·女性向】附加特征:
  561. - 软装彩色:豆沙紫/湖蓝色/淡粉色沙发/椅子
  562. - 装饰细节:蝴蝶/鸟类墙饰、曲线造型、浮雕柜
  563. - 灯具选择:水晶台灯、香槟金/玫瑰金灯具
  564. - 色彩搭配:浅色基底+柔和彩色点缀
  565. - 氛围升级:女性向、浪漫、轻盈柔美、精致细腻
  566. 【温润侘寂】关键特征:
  567. - 硬装:暖灰色墙面 + 木地板 + 简约造型
  568. - 软装:木质家具为主 + 藤编/皮革 + 自然装饰
  569. - 色调:木色 + 暖灰色 + 低饱和度
  570. - 氛围:温暖、舒适、质朴、生活气息
  571. 【现代轻奢】关键特征:
  572. - 硬装:大理石 + 金属线条 + 高级灰
  573. - 软装:轻奢家具 + 金属饰品 + 艺术挂画
  574. - 色调:高级灰 + 香槟金/玫瑰金 + 白色
  575. - 氛围:精致、时尚、轻奢、品质感
  576. • 💫 **氛围判断依据**:
  577. - 柔暖精致:淡奶灰+暖白+水晶灯+护墙板
  578. - 温暖舒适:木色+暖灰+木质软装+自然光
  579. - 清冷克制:纯灰+冷白+极简家具+留白
  580. - 女性向/浪漫:淡粉色点缀+曲线造型+精致细节
  581. • ⚠️ **避免混淆**:
  582. - 护墙板+水晶灯+大理石 = 法式,而非侘寂
  583. - 木质+混凝土+暖灰 = 侘寂,而非法式
  584. 4. **专业优化建议 (suggestions)**:
  585. • 🏠 **居住适配** - 基于风格特征提建议:
  586. - 法式风格:补充淡粉色软装(女儿房)、台盆柜+梳妆台一体化设计
  587. - 侘寂风格:木质模块化收纳、暖灰色地毯、生活化装饰
  588. • 🔧 **细节优化**:
  589. - 材质统一性(护墙板色调协调、大理石纹理呼应)
  590. - 色彩过渡(淡奶灰→米色→木色的渐变)
  591. - 隐形工程(筒射灯预埋无边框、空调风口预埋无边框)
  592. • ✅ **落地可行性**:
  593. - 材料选择(护墙板材质、大理石品类、木地板工艺)
  594. - 施工注意事项(拼花对缝、线条安装、灯光预埋)
  595. 5. **基础要求 - 保证分析质量**:
  596. • ✅ 基于图片**实际可见元素**进行分析,严禁臆测或模板化
  597. • ✅ 每个字段提供**详细描述**(200-400字)
  598. • ✅ 使用**专业室内设计术语**(护墙板、线条、大理石纹理、柔哑面等)
  599. • ✅ **不提及品牌**,仅描述材质、色调、形态、氛围
  600. • ✅ **保持客观中立**,避免过度解读或情感化描述
  601. • ✅ **同一视觉元素=同一结论**,确保分析一致性`;
  602. // 添加空间类型提示
  603. if (spaceType) {
  604. prompt += `\n\n【空间类型参考】: ${spaceType}`;
  605. }
  606. // 添加客户需求提示
  607. if (textDescription) {
  608. prompt += `\n\n【客户核心需求】: ${textDescription}\n请特别关注客户需求中提到的色调、材质、氛围要求,确保分析结果与需求高度契合`;
  609. }
  610. // 🔥 强化分析质量要求
  611. if (hasPreviousAnalysis) {
  612. // 如果是重新分析,强调要结合用户反馈
  613. prompt += `\n\n【重要提醒 - 重新分析要点】
  614. • 仔细阅读用户的修正意见和反馈,理解用户的真实需求
  615. • 重新审视图片,基于用户指出的方向进行调整
  616. • 输出一份完整的、修正后的JSON分析报告
  617. • 使用精准的专业术语(如"淡奶灰色护墙板"而非"灰色墙面")
  618. • 如果用户指出了色调、材质、风格的具体要求,必须在新报告中体现
  619. • 保持分析的专业性和完整性,不要只修改某一部分`;
  620. } else {
  621. // 如果是首次分析,强调一致性
  622. prompt += `\n\n【重要提醒 - 保证分析质量】
  623. • 基于图片中客观可见的元素进行分析,避免主观臆测
  624. • 使用精准的专业术语(如"淡奶灰色护墙板"而非"灰色墙面")
  625. • 材质、色调、风格的判断应该保持逻辑一致性
  626. • 避免使用模糊或可变的描述词汇
  627. • 确保分析的完整性和专业性`;
  628. }
  629. return prompt;
  630. }
  631. /**
  632. * 解析JSON格式的AI分析结果(新方法,处理completionJSON返回的JSON对象)
  633. */
  634. private parseJSONAnalysis(jsonResult: any): any {
  635. console.log('📝 [parseJSONAnalysis] 开始解析JSON分析结果...');
  636. console.log('🔍 [parseJSONAnalysis] JSON对象:', JSON.stringify(jsonResult).substring(0, 300));
  637. // 将JSON字段转换为易读的格式化文本
  638. let formattedContent = this.formatJSONToText(jsonResult);
  639. // 🔥 关键:如果formattedContent为空或过短,说明JSON可能没有标准字段
  640. if (!formattedContent || formattedContent.trim().length < 50) {
  641. console.warn('⚠️ [parseJSONAnalysis] 格式化内容过短,尝试后备方案...');
  642. formattedContent = this.fallbackFormatJSON(jsonResult);
  643. }
  644. // 🔥 最终校验:如果还是为空,使用原始JSON的美化版本
  645. if (!formattedContent || formattedContent.trim().length < 20) {
  646. console.warn('⚠️ [parseJSONAnalysis] 后备方案也失败,使用JSON美化版本...');
  647. formattedContent = this.beautifyJSON(jsonResult);
  648. }
  649. console.log('✅ [parseJSONAnalysis] 最终格式化内容长度:', formattedContent.length);
  650. console.log('📝 [parseJSONAnalysis] 内容预览:', formattedContent.substring(0, 200));
  651. return {
  652. rawContent: JSON.stringify(jsonResult, null, 2), // 原始JSON
  653. formattedContent: formattedContent, // 格式化文本(确保有内容)
  654. structuredData: {
  655. quickSummary: jsonResult.quickSummary || null, // 🔥 快速总结
  656. spacePositioning: jsonResult.spacePositioning || '',
  657. layout: jsonResult.layout || '',
  658. hardDecoration: jsonResult.hardDecoration || '',
  659. colorAnalysis: jsonResult.colorAnalysis || '',
  660. materials: jsonResult.materials || '',
  661. form: jsonResult.form || '',
  662. style: jsonResult.style || '',
  663. suggestions: jsonResult.suggestions || ''
  664. },
  665. spaceType: jsonResult.spaceType || '',
  666. summary: jsonResult.summary || '',
  667. hasContent: true,
  668. timestamp: new Date().toISOString()
  669. };
  670. }
  671. /**
  672. * 美化JSON显示(当所有格式化方法都失败时的最后手段)
  673. */
  674. private beautifyJSON(jsonResult: any): string {
  675. const lines: string[] = [];
  676. for (const [key, value] of Object.entries(jsonResult)) {
  677. if (value && typeof value === 'string' && value.trim().length > 0) {
  678. // 将驼峰命名转换为中文标题
  679. const chineseTitle = this.getChineseTitleForKey(key);
  680. lines.push(`【${chineseTitle}】\n${value}\n`);
  681. }
  682. }
  683. return lines.join('\n') || '分析结果为空,请重新分析';
  684. }
  685. /**
  686. * 将JSON字段名转换为中文标题
  687. */
  688. private getChineseTitleForKey(key: string): string {
  689. const titleMap: { [key: string]: string } = {
  690. 'spaceType': '空间类型',
  691. 'spacePositioning': '空间定位与场景属性',
  692. 'layout': '空间布局与动线',
  693. 'hardDecoration': '硬装系统细节',
  694. 'colorAnalysis': '色调精准分析',
  695. 'materials': '材质应用解析',
  696. 'form': '形体与比例',
  697. 'style': '风格与氛围营造',
  698. 'suggestions': '专业优化建议',
  699. 'summary': '设计概要'
  700. };
  701. return titleMap[key] || key;
  702. }
  703. /**
  704. * 将JSON结果转换为易读的文本格式
  705. */
  706. private formatJSONToText(jsonResult: any): string {
  707. console.log('🔄 [formatJSONToText] 开始格式化JSON结果...');
  708. console.log('🔍 [formatJSONToText] JSON字段数量:', Object.keys(jsonResult).length);
  709. const sections = [];
  710. // 🔥 关键:确保每个字段都被处理,即使内容为空也显示标题
  711. if (jsonResult.spacePositioning) {
  712. sections.push(`一、空间定位与场景属性\n\n${jsonResult.spacePositioning}\n`);
  713. }
  714. if (jsonResult.layout) {
  715. sections.push(`二、空间布局与动线\n\n${jsonResult.layout}\n`);
  716. }
  717. if (jsonResult.hardDecoration) {
  718. sections.push(`三、硬装系统细节\n\n${jsonResult.hardDecoration}\n`);
  719. }
  720. if (jsonResult.colorAnalysis) {
  721. sections.push(`四、色调精准分析\n\n${jsonResult.colorAnalysis}\n`);
  722. }
  723. if (jsonResult.materials) {
  724. sections.push(`五、材质应用解析\n\n${jsonResult.materials}\n`);
  725. }
  726. if (jsonResult.form) {
  727. sections.push(`六、形体与比例\n\n${jsonResult.form}\n`);
  728. }
  729. if (jsonResult.style) {
  730. sections.push(`七、风格与氛围营造\n\n${jsonResult.style}\n`);
  731. }
  732. if (jsonResult.suggestions) {
  733. sections.push(`八、专业优化建议\n\n${jsonResult.suggestions}\n`);
  734. }
  735. const formattedText = sections.join('\n');
  736. console.log('✅ [formatJSONToText] 格式化完成,长度:', formattedText.length);
  737. console.log('📝 [formatJSONToText] 内容预览:', formattedText.substring(0, 200));
  738. // 🔥 后备机制:如果格式化结果为空,尝试从JSON直接生成文本
  739. if (!formattedText || formattedText.trim().length === 0) {
  740. console.warn('⚠️ [formatJSONToText] 格式化结果为空,使用后备方案...');
  741. return this.fallbackFormatJSON(jsonResult);
  742. }
  743. return formattedText;
  744. }
  745. /**
  746. * 从不完整的JSON字符串中提取已完成的字段
  747. * 🔥 流式传输专用:实时提取部分字段
  748. */
  749. private extractPartialJSON(jsonString: string): any {
  750. console.log('🔧 [extractPartialJSON] 开始提取部分JSON字段...');
  751. const result: any = {};
  752. // 定义所有可能的字段
  753. const fields = [
  754. 'spaceType', 'spacePositioning', 'layout', 'hardDecoration',
  755. 'colorAnalysis', 'materials', 'form', 'style', 'suggestions', 'summary'
  756. ];
  757. // 使用正则表达式提取每个字段的完整值
  758. for (const field of fields) {
  759. // 匹配 "fieldName": "value" 或 "fieldName": "value...(可能不完整)
  760. const regex = new RegExp(`"${field}"\\s*:\\s*"([^"]*(?:"[^"]*)*)"`, 'g');
  761. const match = regex.exec(jsonString);
  762. if (match && match[1]) {
  763. // 提取到完整的字段值
  764. result[field] = match[1];
  765. console.log(`✅ 提取字段 ${field}:`, match[1].substring(0, 50) + '...');
  766. } else {
  767. // 尝试提取不完整的值(到字符串末尾)
  768. const partialRegex = new RegExp(`"${field}"\\s*:\\s*"([^"]*?)(?:"|$)`, 's');
  769. const partialMatch = partialRegex.exec(jsonString);
  770. if (partialMatch && partialMatch[1] && partialMatch[1].length > 20) {
  771. // 只有当值足够长时才提取(避免只有几个字符的情况)
  772. result[field] = partialMatch[1] + '...';
  773. console.log(`⚠️ 提取不完整字段 ${field}:`, partialMatch[1].substring(0, 50) + '...');
  774. }
  775. }
  776. }
  777. const extractedCount = Object.keys(result).length;
  778. console.log(`✅ [extractPartialJSON] 提取了 ${extractedCount} 个字段`);
  779. return extractedCount > 0 ? result : null;
  780. }
  781. /**
  782. * 后备格式化方法:当主要方法失败时使用
  783. */
  784. private fallbackFormatJSON(jsonResult: any): string {
  785. const lines: string[] = [];
  786. // 遍历JSON对象的所有字段
  787. const fieldMap: { [key: string]: string } = {
  788. 'spaceType': '空间类型',
  789. 'spacePositioning': '一、空间定位与场景属性',
  790. 'layout': '二、空间布局与动线',
  791. 'hardDecoration': '三、硬装系统细节',
  792. 'colorAnalysis': '四、色调精准分析',
  793. 'materials': '五、材质应用解析',
  794. 'form': '六、形体与比例',
  795. 'style': '七、风格与氛围营造',
  796. 'suggestions': '八、专业优化建议',
  797. 'summary': '设计概要'
  798. };
  799. for (const [key, title] of Object.entries(fieldMap)) {
  800. if (jsonResult[key] && typeof jsonResult[key] === 'string' && jsonResult[key].trim().length > 0) {
  801. if (key === 'spaceType' || key === 'summary') {
  802. lines.push(`${title}:${jsonResult[key]}\n`);
  803. } else {
  804. lines.push(`${title}\n\n${jsonResult[key]}\n`);
  805. }
  806. }
  807. }
  808. const result = lines.join('\n');
  809. console.log('✅ [fallbackFormatJSON] 后备格式化完成,长度:', result.length);
  810. return result || '暂无分析内容';
  811. }
  812. /**
  813. * 解析AI分析内容(优化版:格式化处理,确保结构清晰)
  814. * @deprecated 使用parseJSONAnalysis代替
  815. */
  816. private parseAnalysisContent(content: string): any {
  817. console.log('📝 AI返回的原始内容长度:', content.length);
  818. console.log('📝 AI返回的内容预览:', content.substring(0, 500));
  819. if (!content || content.length < 50) {
  820. console.warn('⚠️ AI返回内容过短或为空');
  821. return {
  822. rawContent: content,
  823. formattedContent: content,
  824. hasContent: false,
  825. timestamp: new Date().toISOString()
  826. };
  827. }
  828. // 格式化处理:优化段落、间距、结构
  829. const formattedContent = this.formatAnalysisContent(content);
  830. // 提取结构化信息
  831. const structuredData = this.extractStructuredInfo(content);
  832. return {
  833. rawContent: content, // 原始AI输出
  834. formattedContent: formattedContent, // 格式化后的内容
  835. structuredData: structuredData, // 结构化数据(维度分段)
  836. hasContent: content.length > 50,
  837. timestamp: new Date().toISOString()
  838. };
  839. }
  840. /**
  841. * 格式化分析内容:优化排版、段落、间距
  842. */
  843. private formatAnalysisContent(content: string): string {
  844. let formatted = content;
  845. // 1. 统一维度标题格式(确保维度标题前后有空行)
  846. const dimensionPattern = /([一二三四五六七八九十]、[^\n]+)/g;
  847. formatted = formatted.replace(dimensionPattern, '\n\n$1\n');
  848. // 2. 处理过长段落:如果段落超过300字,尝试在句号处换行
  849. const paragraphs = formatted.split('\n');
  850. const processedParagraphs = paragraphs.map(para => {
  851. if (para.trim().length > 300) {
  852. // 在句号、问号、感叹号后添加换行,但保持在段落内
  853. return para.replace(/([。!?])(?=[^。!?\n]{50,})/g, '$1\n');
  854. }
  855. return para;
  856. });
  857. formatted = processedParagraphs.join('\n');
  858. // 3. 清理多余空行(超过2个连续空行压缩为2个)
  859. formatted = formatted.replace(/\n{3,}/g, '\n\n');
  860. // 4. 确保维度之间有明确的空行分隔
  861. formatted = formatted.replace(/(一、|二、|三、|四、|五、|六、|七、|八、)/g, '\n\n$1');
  862. // 5. 移除开头和结尾的多余空行
  863. formatted = formatted.trim();
  864. // 6. 确保每个维度内部段落之间有适当间距
  865. formatted = formatted.replace(/([。!?])\s*\n(?=[^\n一二三四五六七八])/g, '$1\n\n');
  866. // 7. 最后清理:确保格式整洁
  867. formatted = formatted.replace(/\n{3,}/g, '\n\n');
  868. return formatted;
  869. }
  870. /**
  871. * 提取结构化信息:将内容按维度分段
  872. */
  873. private extractStructuredInfo(content: string): any {
  874. const dimensions: any = {
  875. spacePositioning: '', // 空间定位与场景属性
  876. layout: '', // 空间布局与动线
  877. hardDecoration: '', // 硬装系统细节
  878. colorAnalysis: '', // 色调精准分析
  879. materials: '', // 材质应用解析
  880. form: '', // 形体与比例
  881. style: '', // 风格与氛围营造
  882. suggestions: '' // 专业优化建议
  883. };
  884. // 按维度标题分割内容
  885. const dimensionRegex = /([一二三四五六七八]、[^\n]+)\n+([\s\S]*?)(?=\n[一二三四五六七八]、|$)/g;
  886. let match;
  887. while ((match = dimensionRegex.exec(content)) !== null) {
  888. const title = match[1].trim();
  889. const contentText = match[2].trim();
  890. // 根据标题关键词匹配到对应维度
  891. if (title.includes('空间定位') || title.includes('场景属性')) {
  892. dimensions.spacePositioning = contentText;
  893. } else if (title.includes('布局') || title.includes('动线')) {
  894. dimensions.layout = contentText;
  895. } else if (title.includes('硬装') || title.includes('系统细节')) {
  896. dimensions.hardDecoration = contentText;
  897. } else if (title.includes('色调') || title.includes('色彩')) {
  898. dimensions.colorAnalysis = contentText;
  899. } else if (title.includes('材质')) {
  900. dimensions.materials = contentText;
  901. } else if (title.includes('形体') || title.includes('比例')) {
  902. dimensions.form = contentText;
  903. } else if (title.includes('风格') || title.includes('氛围')) {
  904. dimensions.style = contentText;
  905. } else if (title.includes('建议') || title.includes('优化')) {
  906. dimensions.suggestions = contentText;
  907. }
  908. }
  909. return dimensions;
  910. }
  911. /**
  912. * 生成简洁摘要:提取关键信息,适合客服和设计师快速查看
  913. */
  914. generateBriefSummary(analysisData: any): string {
  915. if (!analysisData || !analysisData.rawContent) {
  916. return '暂无分析内容';
  917. }
  918. const content = analysisData.rawContent;
  919. const summary: string[] = [];
  920. // 1. 提取空间类型
  921. const spaceTypeMatch = content.match(/(?:这是|空间为|属于).*?([^\n]{2,20}?(?:空间|客厅|餐厅|卧室|厨房|卫生间|玄关|书房))/);
  922. if (spaceTypeMatch) {
  923. summary.push(spaceTypeMatch[1].trim());
  924. }
  925. // 2. 提取风格关键词(优化:区分温润vs清冷侘寂)
  926. const styleKeywords = ['现代', '法式', '简约', '极简', '轻奢', '新中式', '日式', '北欧', '工业风', '台式', '温润侘寂', '侘寂', '美式', '混搭'];
  927. const foundStyles: string[] = [];
  928. styleKeywords.forEach(keyword => {
  929. if (content.includes(keyword) && !foundStyles.includes(keyword)) {
  930. foundStyles.push(keyword);
  931. }
  932. });
  933. if (foundStyles.length > 0) {
  934. summary.push(foundStyles.slice(0, 2).join('+'));
  935. }
  936. // 3. 提取色调关键词(优化:优先识别暖灰色、木色)
  937. const colorKeywords = [
  938. '暖灰色', '暖灰', '木色', '木棕', '原木色', '暖棕', '胡桃木色', // 暖色调优先
  939. '暖色系', '暖调', '米白', '奶白', '米色',
  940. '冷色系', '冷调', '高级灰', '纯灰' // 冷色调在后
  941. ];
  942. const foundColors: string[] = [];
  943. colorKeywords.forEach(keyword => {
  944. if (content.includes(keyword) && !foundColors.includes(keyword)) {
  945. foundColors.push(keyword);
  946. }
  947. });
  948. if (foundColors.length > 0) {
  949. summary.push(foundColors.slice(0, 3).join('、'));
  950. }
  951. // 4. 提取氛围关键词(优化:优先识别温暖、舒适、生活气息)
  952. const moodKeywords = [
  953. '温暖', '舒适', '生活气息', '温馨', '质朴', // 温暖氛围优先
  954. '精致', '高级', '优雅', '松弛', '静谧', '时尚',
  955. '女性向', '男性向', '亲子', '清冷' // 清冷在后
  956. ];
  957. const foundMoods: string[] = [];
  958. moodKeywords.forEach(keyword => {
  959. if (content.includes(keyword) && !foundMoods.includes(keyword)) {
  960. foundMoods.push(keyword);
  961. }
  962. });
  963. if (foundMoods.length > 0) {
  964. summary.push(foundMoods.slice(0, 3).join('、'));
  965. }
  966. // 5. 提取关键材质(优化:优先识别木材、皮革)
  967. const materialKeywords = [
  968. '木材', '木质', '实木', '胡桃木', '橡木', '柚木', // 木材优先
  969. '皮革', '黑色皮革', // 皮革
  970. '大理石', '瓷砖', '混凝土', '护墙板', '布艺', '金属', '玻璃', '藤编'
  971. ];
  972. const foundMaterials: string[] = [];
  973. materialKeywords.forEach(keyword => {
  974. if (content.includes(keyword) && !foundMaterials.includes(keyword)) {
  975. foundMaterials.push(keyword);
  976. }
  977. });
  978. if (foundMaterials.length > 0) {
  979. summary.push('主要材质:' + foundMaterials.slice(0, 4).join('、'));
  980. }
  981. return summary.length > 0 ? summary.join(' | ') : '暂无摘要';
  982. }
  983. /**
  984. * 生成客服标注(从AI分析结果提取关键信息)
  985. */
  986. generateCustomerServiceNotes(analysisData: any, customerRequirements?: string): string {
  987. const notes: string[] = [];
  988. // 优先使用JSON格式的structuredData,否则使用rawContent
  989. const structuredData = analysisData.structuredData;
  990. const rawContent = analysisData.rawContent || '';
  991. // 1. 客户要求(如果有)
  992. if (customerRequirements) {
  993. notes.push(`【客户要求】\n${customerRequirements}`);
  994. }
  995. // 2. 空间类型识别
  996. let spaceType = '';
  997. if (structuredData?.spaceType) {
  998. spaceType = structuredData.spaceType;
  999. } else {
  1000. // 从rawContent提取
  1001. const spaceMatch = rawContent.match(/(?:这是|空间为|属于).*?([^\n]{2,20}?(?:空间|客厅|餐厅|卧室|厨房|卫生间|玄关|书房|客餐厅一体化|三室两厅|两室一厅))/);
  1002. if (spaceMatch) spaceType = spaceMatch[1].trim();
  1003. }
  1004. if (spaceType) {
  1005. notes.push(`【空间类型】\n${spaceType}`);
  1006. }
  1007. // 3. 风格定位(从结构化数据或rawContent提取)
  1008. let styleInfo = '';
  1009. if (structuredData?.style) {
  1010. // 提取风格关键词
  1011. const styleKeywords = ['现代', '法式', '简约', '极简', '轻奢', '新中式', '日式', '北欧', '工业风', '侘寂', '美式', '台式'];
  1012. const foundStyles: string[] = [];
  1013. styleKeywords.forEach(keyword => {
  1014. if (structuredData.style.includes(keyword) && !foundStyles.includes(keyword)) {
  1015. foundStyles.push(keyword);
  1016. }
  1017. });
  1018. if (foundStyles.length > 0) {
  1019. styleInfo = foundStyles.join('+') + '风格';
  1020. }
  1021. // 提取氛围描述
  1022. const moodMatch = structuredData.style.match(/(?:氛围|营造|呈现).*?([^\n。]{5,30}?(?:温馨|舒适|精致|高级|松弛|静谧|优雅|时尚))/);
  1023. if (moodMatch) {
  1024. styleInfo += `,${moodMatch[1].trim()}`;
  1025. }
  1026. } else if (rawContent) {
  1027. // 从rawContent提取风格
  1028. const styleMatch = rawContent.match(/(?:风格|呈现|属于).*?([^\n。]{5,40}?(?:风格|法式|现代|简约|极简))/);
  1029. if (styleMatch) styleInfo = styleMatch[1].trim();
  1030. }
  1031. if (styleInfo) {
  1032. notes.push(`【风格定位】\n${styleInfo}`);
  1033. }
  1034. // 4. 色调要求(从色彩分析提取)
  1035. let colorInfo = '';
  1036. if (structuredData?.colorAnalysis) {
  1037. // 提取主色调
  1038. const mainColorMatch = structuredData.colorAnalysis.match(/主色调[::]\s*([^\n。]{5,50})/);
  1039. if (mainColorMatch) {
  1040. colorInfo = `主色调:${mainColorMatch[1].trim()}`;
  1041. }
  1042. // 提取辅助色
  1043. const subColorMatch = structuredData.colorAnalysis.match(/辅助色[::]\s*([^\n。]{5,50})/);
  1044. if (subColorMatch) {
  1045. colorInfo += `\n辅助色:${subColorMatch[1].trim()}`;
  1046. }
  1047. } else if (rawContent) {
  1048. // 从rawContent提取色调
  1049. const colorMatch = rawContent.match(/(?:色调|色彩|主色)[::]\s*([^\n。]{5,50})/);
  1050. if (colorMatch) colorInfo = colorMatch[1].trim();
  1051. }
  1052. if (colorInfo) {
  1053. notes.push(`【色调要求】\n${colorInfo}`);
  1054. }
  1055. // 5. 材质要求(从硬装和材质维度提取)
  1056. const materials: string[] = [];
  1057. if (structuredData?.hardDecoration) {
  1058. // 提取地面材质
  1059. const floorMatch = structuredData.hardDecoration.match(/地面[::]\s*([^\n。]{5,40})/);
  1060. if (floorMatch) materials.push(`地面:${floorMatch[1].trim()}`);
  1061. // 提取墙面材质
  1062. const wallMatch = structuredData.hardDecoration.match(/墙面[::]\s*([^\n。]{5,40})/);
  1063. if (wallMatch) materials.push(`墙面:${wallMatch[1].trim()}`);
  1064. // 提取顶面材质
  1065. const ceilingMatch = structuredData.hardDecoration.match(/顶面[::]\s*([^\n。]{5,40})/);
  1066. if (ceilingMatch) materials.push(`顶面:${ceilingMatch[1].trim()}`);
  1067. }
  1068. // 从材质维度补充
  1069. if (structuredData?.materials && materials.length < 2) {
  1070. const materialMatch = structuredData.materials.match(/(?:主要材质|材质应用)[::]\s*([^\n。]{10,60})/);
  1071. if (materialMatch) materials.push(materialMatch[1].trim());
  1072. }
  1073. if (materials.length > 0) {
  1074. notes.push(`【材质要求】\n${materials.join('\n')}`);
  1075. }
  1076. // 6. 空间布局要点
  1077. if (structuredData?.layout) {
  1078. const layoutMatch = structuredData.layout.match(/(?:布局特点|空间关系)[::]\s*([^\n。]{10,60})/);
  1079. if (layoutMatch) {
  1080. notes.push(`【布局要点】\n${layoutMatch[1].trim()}`);
  1081. }
  1082. }
  1083. // 7. 施工注意事项(从优化建议提取)
  1084. if (structuredData?.suggestions) {
  1085. const attentionPoints: string[] = [];
  1086. // 提取落地可行性
  1087. const feasibilityMatch = structuredData.suggestions.match(/落地可行性[::]\s*([^\n。]{10,80})/);
  1088. if (feasibilityMatch) attentionPoints.push(feasibilityMatch[1].trim());
  1089. // 提取细节优化
  1090. const detailMatch = structuredData.suggestions.match(/细节优化[::]\s*([^\n。]{10,80})/);
  1091. if (detailMatch) attentionPoints.push(detailMatch[1].trim());
  1092. if (attentionPoints.length > 0) {
  1093. notes.push(`【施工注意】\n${attentionPoints.join('\n')}`);
  1094. }
  1095. }
  1096. // 8. 品质要求(固定添加)
  1097. notes.push(`【品质要求】\n新客户,需严格把控施工品质和材料质量`);
  1098. return notes.length > 0 ? notes.join('\n\n') : '请根据分析内容补充具体要求';
  1099. }
  1100. /**
  1101. * 🔥 新增:真正的AI对话功能(流式响应)
  1102. * 参考ai.service.ts的实现方式,使用FmodeChatCompletion
  1103. */
  1104. async chatWithAI(options: {
  1105. userMessage: string;
  1106. conversationHistory: Array<{ role: string; content: string; images?: string[] }>;
  1107. images?: string[];
  1108. context?: any;
  1109. onContentStream?: (content: string) => void;
  1110. }): Promise<string> {
  1111. console.log('💬 [chatWithAI] 开始AI对话');
  1112. console.log('📝 [chatWithAI] 用户消息:', options.userMessage);
  1113. console.log('📜 [chatWithAI] 对话历史数量:', options.conversationHistory.length);
  1114. console.log('📸 [chatWithAI] 图片数量:', options.images?.length || 0);
  1115. return new Promise((resolve, reject) => {
  1116. try {
  1117. // 构建消息列表(包含历史对话和当前消息)
  1118. const messageList: any[] = [];
  1119. // 🔥 如果有上下文(结构化分析结果),添加为系统提示
  1120. if (options.context) {
  1121. console.log('🧠 [chatWithAI] 添加结构化上下文');
  1122. let contextContent = '';
  1123. // 如果是完整的分析结果对象
  1124. if (options.context.structuredData) {
  1125. contextContent = JSON.stringify(options.context.structuredData);
  1126. } else {
  1127. contextContent = JSON.stringify(options.context);
  1128. }
  1129. // 限制上下文长度,避免超出token限制
  1130. if (contextContent.length > 5000) {
  1131. contextContent = contextContent.substring(0, 5000) + '...';
  1132. }
  1133. messageList.push({
  1134. role: 'system',
  1135. content: `这是当前设计的详细分析数据,请基于此回答用户问题:\n${contextContent}`
  1136. });
  1137. }
  1138. // 添加历史对话(但不包含图片,只在首次分析时使用图片)
  1139. options.conversationHistory.forEach(msg => {
  1140. messageList.push({
  1141. role: msg.role,
  1142. content: msg.content
  1143. });
  1144. });
  1145. // 添加当前用户消息
  1146. const currentMessage: any = {
  1147. role: 'user',
  1148. content: options.userMessage
  1149. };
  1150. // 🔥 如果提供了图片,添加到当前消息
  1151. if (options.images && options.images.length > 0) {
  1152. currentMessage.images = options.images;
  1153. console.log('📸 [chatWithAI] 当前消息包含图片:', options.images.length);
  1154. }
  1155. messageList.push(currentMessage);
  1156. console.log('📤 [chatWithAI] 发送消息列表到AI,总计:', messageList.length, '条');
  1157. // 使用FmodeChatCompletion进行流式对话
  1158. const completion = new FmodeChatCompletion(messageList, {
  1159. model: this.AI_MODEL,
  1160. max_tokens: 8000
  1161. });
  1162. let fullContent = '';
  1163. const subscription = completion.sendCompletion({
  1164. isDirect: true
  1165. }).subscribe({
  1166. next: (message: any) => {
  1167. const content = message?.content || '';
  1168. if (content) {
  1169. fullContent = content;
  1170. // 🔥 实时回调,更新UI
  1171. options.onContentStream?.(content);
  1172. console.log('📨 [chatWithAI] 流式内容更新,当前长度:', content.length);
  1173. }
  1174. // 🔥 对话完成
  1175. if (message?.complete && fullContent) {
  1176. console.log('✅ [chatWithAI] 对话完成,总长度:', fullContent.length);
  1177. subscription.unsubscribe();
  1178. resolve(fullContent);
  1179. }
  1180. },
  1181. error: (error: any) => {
  1182. console.error('❌ [chatWithAI] 对话失败:', error);
  1183. subscription.unsubscribe();
  1184. reject(new Error(`AI对话失败: ${error?.message || '未知错误'}`));
  1185. }
  1186. });
  1187. // 🔥 超时保护(60秒)
  1188. setTimeout(() => {
  1189. if (fullContent) {
  1190. console.log('⏰ [chatWithAI] 超时但已有内容,返回部分结果');
  1191. subscription.unsubscribe();
  1192. resolve(fullContent);
  1193. } else {
  1194. console.error('❌ [chatWithAI] 超时且无内容');
  1195. subscription.unsubscribe();
  1196. reject(new Error('AI响应超时'));
  1197. }
  1198. }, 60000);
  1199. } catch (error: any) {
  1200. console.error('❌ [chatWithAI] 初始化失败:', error);
  1201. reject(new Error(`AI对话初始化失败: ${error?.message || '未知错误'}`));
  1202. }
  1203. });
  1204. }
  1205. /**
  1206. * 生成客户报告(此方法保留以便后续使用)
  1207. */
  1208. async generateClientReport(options: {
  1209. analysisData: any;
  1210. spaceName: string;
  1211. onContentChange?: (content: string) => void;
  1212. loading?: any;
  1213. }): Promise<string> {
  1214. return new Promise(async (resolve, reject) => {
  1215. try {
  1216. // 使用格式化后的内容
  1217. const content = options.analysisData?.formattedContent || options.analysisData?.rawContent || '暂无报告内容';
  1218. resolve(content);
  1219. } catch (error: any) {
  1220. reject(new Error('生成报告失败: ' + error.message));
  1221. }
  1222. });
  1223. }
  1224. }