design-analysis-ai.service.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  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. constructor() {}
  16. /**
  17. * 分析参考图片,识别场景类型和设计维度
  18. */
  19. async analyzeReferenceImages(options: {
  20. images: string[];
  21. textDescription?: string;
  22. spaceType?: string;
  23. conversationHistory?: Array<{ role: string; content: string }>;
  24. deepThinking?: boolean;
  25. onProgressChange?: (progress: string) => void;
  26. onContentStream?: (content: string) => void; // 新增:流式内容回调
  27. loading?: any;
  28. }): Promise<any> {
  29. return new Promise(async (resolve, reject) => {
  30. try {
  31. // 构建详细的分析提示词
  32. const prompt = this.buildAnalysisPrompt(
  33. options.spaceType,
  34. options.textDescription,
  35. options.conversationHistory,
  36. options.deepThinking
  37. );
  38. options.onProgressChange?.('正在识别场景和分析设计维度...');
  39. // 🔥 直接使用图片URL,不转base64(参考ai-k12-daofa的实现)
  40. console.log('📸 准备传入图片URL到AI模型...');
  41. console.log('📸 图片URL列表:', options.images);
  42. // 日志输出,帮助调试
  43. console.log('🤖 调用豆包1.6模型进行vision分析...');
  44. console.log('📸 图片数量:', options.images.length);
  45. console.log('📝 提示词长度:', prompt.length, '字符');
  46. console.log('🏠 空间类型:', options.spaceType);
  47. console.log('💬 对话历史:', options.conversationHistory?.length || 0, '条');
  48. // 检查提示词长度(建议不超过10000字符)
  49. if (prompt.length > 10000) {
  50. console.warn('⚠️ 提示词过长,可能导致API调用失败');
  51. }
  52. // 🔥 使用completionJSON进行vision分析(严格参考ai-k12-daofa的成功实现)
  53. console.log('🚀 开始调用completionJSON进行vision分析...');
  54. // 定义JSON schema(与提示词中的JSON格式完全一致)
  55. const outputSchema = `{
  56. "spaceType": "空间类型(如:客餐厅一体化、主卧、玄关等)",
  57. "spacePositioning": "空间定位与场景属性的详细分析",
  58. "layout": "空间布局与动线的详细分析",
  59. "hardDecoration": "硬装系统细节的详细分析(顶面、墙面、地面、门窗)",
  60. "colorAnalysis": "色调精准分析(主色调、辅助色、色调关系)",
  61. "materials": "材质应用解析(自然材质、现代材质、材质对比)",
  62. "form": "形体与比例分析(空间形体、家具形体、造型细节)",
  63. "style": "风格与氛围营造(风格识别、氛围手法)",
  64. "suggestions": "专业优化建议(居住适配、细节优化、落地可行性)",
  65. "summary": "简洁摘要(格式: 空间类型 | 风格 | 色调 | 氛围)"
  66. }`;
  67. // 流式内容累积
  68. let streamContent = '';
  69. try {
  70. // 🔥 关键:使用completionJSON + vision: true + images (URL数组)
  71. console.log('📤 发送给AI的提示词:', prompt);
  72. console.log('📤 JSON Schema:', outputSchema);
  73. console.log('📤 图片URL:', options.images);
  74. const result = await completionJSON(
  75. prompt,
  76. outputSchema, // 🔥 关键:提供JSON schema
  77. (content) => {
  78. // 流式回调(模拟)
  79. console.log('📥 AI流式响应:', typeof content, content);
  80. if (content && options.onContentStream) {
  81. streamContent = content;
  82. // 将JSON转为易读文本
  83. const displayText = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
  84. options.onContentStream(displayText);
  85. options.onProgressChange?.(`已接收 ${displayText.length} 字符...`);
  86. }
  87. },
  88. 2, // 重试次数
  89. {
  90. model: this.AI_MODEL,
  91. vision: true, // 🔥 关键:启用vision
  92. images: options.images, // 🔥 关键:直接传URL数组
  93. max_tokens: 8000
  94. }
  95. );
  96. console.log('📥 AI最终返回结果:', result);
  97. console.log('📥 返回结果类型:', typeof result);
  98. // 获取最终内容(result应该就是JSON对象)
  99. const analysisResult = result;
  100. console.log('✅ AI分析完成,返回JSON对象:', analysisResult);
  101. console.log('📝 AI返回JSON预览:', JSON.stringify(analysisResult).substring(0, 500));
  102. // 验证返回的JSON结构
  103. if (!analysisResult || typeof analysisResult !== 'object') {
  104. console.error('❌ AI返回格式错误,不是JSON对象');
  105. reject(new Error('AI返回格式异常,请重试'));
  106. return;
  107. }
  108. // 检查必要字段
  109. if (!analysisResult.spaceType || !analysisResult.spacePositioning) {
  110. console.error('❌ AI返回JSON缺少必要字段');
  111. console.error('🔍 AI返回的完整对象:', analysisResult);
  112. reject(new Error('AI分析结果不完整,请重试'));
  113. return;
  114. }
  115. // 解析JSON结果
  116. const analysisData = this.parseJSONAnalysis(analysisResult);
  117. console.log('📊 解析后的分析数据:', analysisData);
  118. resolve(analysisData);
  119. } catch (err: any) {
  120. console.error('❌ completionJSON失败,详细错误:', err);
  121. console.error('❌ 错误类型:', err?.constructor?.name);
  122. console.error('❌ 错误消息:', err?.message);
  123. // 🔥 关键:如果completionJSON失败,尝试使用FmodeChatCompletion作为备选方案
  124. if (err?.message?.includes('JSON') || err?.message?.includes('格式')) {
  125. console.warn('⚠️ completionJSON解析失败,尝试使用FmodeChatCompletion备选方案...');
  126. try {
  127. // 使用FmodeChatCompletion获取纯文本响应
  128. const textPrompt = this.buildTextAnalysisPrompt(options.spaceType, options.textDescription);
  129. const messageList = [{
  130. role: 'user',
  131. content: textPrompt,
  132. images: options.images
  133. }];
  134. const completion = new FmodeChatCompletion(messageList, {
  135. model: this.AI_MODEL,
  136. max_tokens: 8000
  137. });
  138. let fullContent = '';
  139. const subscription = completion.sendCompletion({
  140. isDirect: true,
  141. }).subscribe({
  142. next: (message: any) => {
  143. const content = message?.content || '';
  144. if (content) {
  145. fullContent = content;
  146. options.onContentStream?.(content);
  147. }
  148. if (message?.complete && fullContent) {
  149. console.log('✅ FmodeChatCompletion备选方案成功,内容长度:', fullContent.length);
  150. const analysisData = this.parseAnalysisContent(fullContent);
  151. resolve(analysisData);
  152. subscription?.unsubscribe();
  153. }
  154. },
  155. error: (err2: any) => {
  156. console.error('❌ FmodeChatCompletion备选方案也失败:', err2);
  157. reject(new Error('AI分析失败,请稍后重试'));
  158. subscription?.unsubscribe();
  159. }
  160. });
  161. return; // 使用备选方案,不继续执行下面的reject
  162. } catch (fallbackErr: any) {
  163. console.error('❌ 备选方案失败:', fallbackErr);
  164. }
  165. }
  166. // 根据错误类型提供更具体的错误信息
  167. let errorMessage = 'AI分析失败';
  168. if (err?.message?.includes('500')) {
  169. errorMessage = 'AI服务暂时不可用(服务器错误),请稍后重试';
  170. } else if (err?.message?.includes('timeout')) {
  171. errorMessage = 'AI分析超时,请减少图片数量或简化需求后重试';
  172. } else if (err?.message?.includes('token')) {
  173. errorMessage = '提示词过长,请简化描述或减少对话历史';
  174. } else if (err?.message) {
  175. errorMessage = `AI分析失败: ${err.message}`;
  176. }
  177. reject(new Error(errorMessage));
  178. }
  179. } catch (error: any) {
  180. reject(new Error('分析失败: ' + error.message));
  181. }
  182. });
  183. }
  184. /**
  185. * 构建纯文本分析提示词(用于FmodeChatCompletion备选方案)
  186. */
  187. private buildTextAnalysisPrompt(spaceType?: string, textDescription?: string): string {
  188. let prompt = `请对图片中的室内设计进行专业分析,从以下8个维度详细展开:
  189. 一、空间定位与场景属性
  190. 二、空间布局与动线
  191. 三、硬装系统细节
  192. 四、色调精准分析
  193. 五、材质应用解析
  194. 六、形体与比例
  195. 七、风格与氛围营造
  196. 八、专业优化建议
  197. 要求:
  198. 1. 基于图片实际视觉内容进行分析
  199. 2. 每个维度2-4个段落,每段3-5行
  200. 3. 使用专业的室内设计术语
  201. 4. 不使用Markdown符号,使用纯文本格式`;
  202. if (spaceType) {
  203. prompt += `\n5. 空间类型参考: ${spaceType}`;
  204. }
  205. if (textDescription) {
  206. prompt += `\n6. 客户需求参考: ${textDescription}`;
  207. }
  208. return prompt;
  209. }
  210. /**
  211. * 构建AI分析提示词(JSON格式输出,兼容completionJSON)
  212. * 参考ai-k12-daofa的简洁提示词风格
  213. */
  214. private buildAnalysisPrompt(spaceType?: string, textDescription?: string, conversationHistory?: Array<{ role: string; content: string }>, deepThinking?: boolean): string {
  215. // 🔥 关键:提示词要简洁明了,直接要求JSON格式(参考ai-k12-daofa)
  216. let prompt = `请分析图片中的室内设计,并按以下JSON格式输出:
  217. {
  218. "spaceType": "空间类型(如:客餐厅一体化、主卧、玄关等)",
  219. "spacePositioning": "空间定位与场景属性的详细分析",
  220. "layout": "空间布局与动线的详细分析",
  221. "hardDecoration": "硬装系统细节的详细分析(顶面、墙面、地面、门窗)",
  222. "colorAnalysis": "色调精准分析(主色调、辅助色、色调关系)",
  223. "materials": "材质应用解析(自然材质、现代材质、材质对比)",
  224. "form": "形体与比例分析(空间形体、家具形体、造型细节)",
  225. "style": "风格与氛围营造(风格识别、氛围手法)",
  226. "suggestions": "专业优化建议(居住适配、细节优化、落地可行性)",
  227. "summary": "简洁摘要(格式: 空间类型 | 风格 | 色调 | 氛围)"
  228. }
  229. 要求:
  230. 1. 基于图片实际视觉内容进行分析
  231. 2. 每个字段提供详细描述(200-400字)
  232. 3. 使用专业的室内设计术语
  233. 4. 不提及品牌名称,仅描述材质、色调、形态`;
  234. // 添加空间类型提示
  235. if (spaceType) {
  236. prompt += `\n5. 空间类型参考: ${spaceType}`;
  237. }
  238. // 添加客户需求提示
  239. if (textDescription) {
  240. prompt += `\n6. 客户需求参考: ${textDescription}`;
  241. }
  242. return prompt;
  243. }
  244. /**
  245. * 解析JSON格式的AI分析结果(新方法,处理completionJSON返回的JSON对象)
  246. */
  247. private parseJSONAnalysis(jsonResult: any): any {
  248. console.log('📝 解析JSON分析结果...');
  249. // 将JSON字段转换为易读的格式化文本
  250. const formattedContent = this.formatJSONToText(jsonResult);
  251. return {
  252. rawContent: JSON.stringify(jsonResult, null, 2), // 原始JSON
  253. formattedContent: formattedContent, // 格式化文本
  254. structuredData: {
  255. spacePositioning: jsonResult.spacePositioning || '',
  256. layout: jsonResult.layout || '',
  257. hardDecoration: jsonResult.hardDecoration || '',
  258. colorAnalysis: jsonResult.colorAnalysis || '',
  259. materials: jsonResult.materials || '',
  260. form: jsonResult.form || '',
  261. style: jsonResult.style || '',
  262. suggestions: jsonResult.suggestions || ''
  263. },
  264. spaceType: jsonResult.spaceType || '',
  265. summary: jsonResult.summary || '',
  266. hasContent: true,
  267. timestamp: new Date().toISOString()
  268. };
  269. }
  270. /**
  271. * 将JSON结果转换为易读的文本格式
  272. */
  273. private formatJSONToText(jsonResult: any): string {
  274. const sections = [];
  275. if (jsonResult.spacePositioning) {
  276. sections.push(`一、空间定位与场景属性\n\n${jsonResult.spacePositioning}\n`);
  277. }
  278. if (jsonResult.layout) {
  279. sections.push(`二、空间布局与动线\n\n${jsonResult.layout}\n`);
  280. }
  281. if (jsonResult.hardDecoration) {
  282. sections.push(`三、硬装系统细节\n\n${jsonResult.hardDecoration}\n`);
  283. }
  284. if (jsonResult.colorAnalysis) {
  285. sections.push(`四、色调精准分析\n\n${jsonResult.colorAnalysis}\n`);
  286. }
  287. if (jsonResult.materials) {
  288. sections.push(`五、材质应用解析\n\n${jsonResult.materials}\n`);
  289. }
  290. if (jsonResult.form) {
  291. sections.push(`六、形体与比例\n\n${jsonResult.form}\n`);
  292. }
  293. if (jsonResult.style) {
  294. sections.push(`七、风格与氛围营造\n\n${jsonResult.style}\n`);
  295. }
  296. if (jsonResult.suggestions) {
  297. sections.push(`八、专业优化建议\n\n${jsonResult.suggestions}\n`);
  298. }
  299. return sections.join('\n');
  300. }
  301. /**
  302. * 解析AI分析内容(优化版:格式化处理,确保结构清晰)
  303. * @deprecated 使用parseJSONAnalysis代替
  304. */
  305. private parseAnalysisContent(content: string): any {
  306. console.log('📝 AI返回的原始内容长度:', content.length);
  307. console.log('📝 AI返回的内容预览:', content.substring(0, 500));
  308. if (!content || content.length < 50) {
  309. console.warn('⚠️ AI返回内容过短或为空');
  310. return {
  311. rawContent: content,
  312. formattedContent: content,
  313. hasContent: false,
  314. timestamp: new Date().toISOString()
  315. };
  316. }
  317. // 格式化处理:优化段落、间距、结构
  318. const formattedContent = this.formatAnalysisContent(content);
  319. // 提取结构化信息
  320. const structuredData = this.extractStructuredInfo(content);
  321. return {
  322. rawContent: content, // 原始AI输出
  323. formattedContent: formattedContent, // 格式化后的内容
  324. structuredData: structuredData, // 结构化数据(维度分段)
  325. hasContent: content.length > 50,
  326. timestamp: new Date().toISOString()
  327. };
  328. }
  329. /**
  330. * 格式化分析内容:优化排版、段落、间距
  331. */
  332. private formatAnalysisContent(content: string): string {
  333. let formatted = content;
  334. // 1. 统一维度标题格式(确保维度标题前后有空行)
  335. const dimensionPattern = /([一二三四五六七八九十]、[^\n]+)/g;
  336. formatted = formatted.replace(dimensionPattern, '\n\n$1\n');
  337. // 2. 处理过长段落:如果段落超过300字,尝试在句号处换行
  338. const paragraphs = formatted.split('\n');
  339. const processedParagraphs = paragraphs.map(para => {
  340. if (para.trim().length > 300) {
  341. // 在句号、问号、感叹号后添加换行,但保持在段落内
  342. return para.replace(/([。!?])(?=[^。!?\n]{50,})/g, '$1\n');
  343. }
  344. return para;
  345. });
  346. formatted = processedParagraphs.join('\n');
  347. // 3. 清理多余空行(超过2个连续空行压缩为2个)
  348. formatted = formatted.replace(/\n{3,}/g, '\n\n');
  349. // 4. 确保维度之间有明确的空行分隔
  350. formatted = formatted.replace(/(一、|二、|三、|四、|五、|六、|七、|八、)/g, '\n\n$1');
  351. // 5. 移除开头和结尾的多余空行
  352. formatted = formatted.trim();
  353. // 6. 确保每个维度内部段落之间有适当间距
  354. formatted = formatted.replace(/([。!?])\s*\n(?=[^\n一二三四五六七八])/g, '$1\n\n');
  355. // 7. 最后清理:确保格式整洁
  356. formatted = formatted.replace(/\n{3,}/g, '\n\n');
  357. return formatted;
  358. }
  359. /**
  360. * 提取结构化信息:将内容按维度分段
  361. */
  362. private extractStructuredInfo(content: string): any {
  363. const dimensions: any = {
  364. spacePositioning: '', // 空间定位与场景属性
  365. layout: '', // 空间布局与动线
  366. hardDecoration: '', // 硬装系统细节
  367. colorAnalysis: '', // 色调精准分析
  368. materials: '', // 材质应用解析
  369. form: '', // 形体与比例
  370. style: '', // 风格与氛围营造
  371. suggestions: '' // 专业优化建议
  372. };
  373. // 按维度标题分割内容
  374. const dimensionRegex = /([一二三四五六七八]、[^\n]+)\n+([\s\S]*?)(?=\n[一二三四五六七八]、|$)/g;
  375. let match;
  376. while ((match = dimensionRegex.exec(content)) !== null) {
  377. const title = match[1].trim();
  378. const contentText = match[2].trim();
  379. // 根据标题关键词匹配到对应维度
  380. if (title.includes('空间定位') || title.includes('场景属性')) {
  381. dimensions.spacePositioning = contentText;
  382. } else if (title.includes('布局') || title.includes('动线')) {
  383. dimensions.layout = contentText;
  384. } else if (title.includes('硬装') || title.includes('系统细节')) {
  385. dimensions.hardDecoration = contentText;
  386. } else if (title.includes('色调') || title.includes('色彩')) {
  387. dimensions.colorAnalysis = contentText;
  388. } else if (title.includes('材质')) {
  389. dimensions.materials = contentText;
  390. } else if (title.includes('形体') || title.includes('比例')) {
  391. dimensions.form = contentText;
  392. } else if (title.includes('风格') || title.includes('氛围')) {
  393. dimensions.style = contentText;
  394. } else if (title.includes('建议') || title.includes('优化')) {
  395. dimensions.suggestions = contentText;
  396. }
  397. }
  398. return dimensions;
  399. }
  400. /**
  401. * 生成简洁摘要:提取关键信息,适合客服和设计师快速查看
  402. */
  403. generateBriefSummary(analysisData: any): string {
  404. if (!analysisData || !analysisData.rawContent) {
  405. return '暂无分析内容';
  406. }
  407. const content = analysisData.rawContent;
  408. const summary: string[] = [];
  409. // 1. 提取空间类型
  410. const spaceTypeMatch = content.match(/(?:这是|空间为|属于).*?([^\n]{2,20}?(?:空间|客厅|餐厅|卧室|厨房|卫生间|玄关|书房))/);
  411. if (spaceTypeMatch) {
  412. summary.push(spaceTypeMatch[1].trim());
  413. }
  414. // 2. 提取风格关键词
  415. const styleKeywords = ['现代', '法式', '简约', '极简', '轻奢', '新中式', '日式', '北欧', '工业风', '台式', '侘寂', '美式'];
  416. const foundStyles: string[] = [];
  417. styleKeywords.forEach(keyword => {
  418. if (content.includes(keyword) && !foundStyles.includes(keyword)) {
  419. foundStyles.push(keyword);
  420. }
  421. });
  422. if (foundStyles.length > 0) {
  423. summary.push(foundStyles.join('+'));
  424. }
  425. // 3. 提取色调关键词
  426. const colorKeywords = ['暖色系', '冷色系', '暖调', '冷调', '高级灰', '奶白', '米白', '暖灰', '木色', '原木色'];
  427. const foundColors: string[] = [];
  428. colorKeywords.forEach(keyword => {
  429. if (content.includes(keyword) && !foundColors.includes(keyword)) {
  430. foundColors.push(keyword);
  431. }
  432. });
  433. if (foundColors.length > 0) {
  434. summary.push(foundColors.join('、'));
  435. }
  436. // 4. 提取氛围关键词
  437. const moodKeywords = ['温馨', '舒适', '精致', '高级', '松弛', '静谧', '优雅', '时尚', '女性向', '男性向', '亲子'];
  438. const foundMoods: string[] = [];
  439. moodKeywords.forEach(keyword => {
  440. if (content.includes(keyword) && !foundMoods.includes(keyword)) {
  441. foundMoods.push(keyword);
  442. }
  443. });
  444. if (foundMoods.length > 0) {
  445. summary.push(foundMoods.slice(0, 2).join('、'));
  446. }
  447. // 5. 提取关键材质
  448. const materialKeywords = ['木材', '大理石', '瓷砖', '布艺', '皮革', '金属', '玻璃', '护墙板'];
  449. const foundMaterials: string[] = [];
  450. materialKeywords.forEach(keyword => {
  451. if (content.includes(keyword) && !foundMaterials.includes(keyword)) {
  452. foundMaterials.push(keyword);
  453. }
  454. });
  455. if (foundMaterials.length > 0) {
  456. summary.push('主要材质:' + foundMaterials.slice(0, 3).join('、'));
  457. }
  458. return summary.length > 0 ? summary.join(' | ') : '整体设计基于图片实际内容分析';
  459. }
  460. /**
  461. * 生成客服标注格式:提取客户要求的关键点
  462. */
  463. generateCustomerServiceNotes(analysisData: any, customerRequirements?: string): string {
  464. if (!analysisData) {
  465. return '暂无标注内容';
  466. }
  467. const notes: string[] = [];
  468. // 优先使用JSON格式的structuredData,否则使用rawContent
  469. const structuredData = analysisData.structuredData;
  470. const rawContent = analysisData.rawContent || '';
  471. // 1. 客户要求(如果有)
  472. if (customerRequirements) {
  473. notes.push(`【客户要求】\n${customerRequirements}`);
  474. }
  475. // 2. 空间类型识别
  476. let spaceType = '';
  477. if (structuredData?.spaceType) {
  478. spaceType = structuredData.spaceType;
  479. } else {
  480. // 从rawContent提取
  481. const spaceMatch = rawContent.match(/(?:这是|空间为|属于).*?([^\n]{2,20}?(?:空间|客厅|餐厅|卧室|厨房|卫生间|玄关|书房|客餐厅一体化|三室两厅|两室一厅))/);
  482. if (spaceMatch) spaceType = spaceMatch[1].trim();
  483. }
  484. if (spaceType) {
  485. notes.push(`【空间类型】\n${spaceType}`);
  486. }
  487. // 3. 风格定位(从结构化数据或rawContent提取)
  488. let styleInfo = '';
  489. if (structuredData?.style) {
  490. // 提取风格关键词
  491. const styleKeywords = ['现代', '法式', '简约', '极简', '轻奢', '新中式', '日式', '北欧', '工业风', '侘寂', '美式', '台式'];
  492. const foundStyles: string[] = [];
  493. styleKeywords.forEach(keyword => {
  494. if (structuredData.style.includes(keyword) && !foundStyles.includes(keyword)) {
  495. foundStyles.push(keyword);
  496. }
  497. });
  498. if (foundStyles.length > 0) {
  499. styleInfo = foundStyles.join('+') + '风格';
  500. }
  501. // 提取氛围描述
  502. const moodMatch = structuredData.style.match(/(?:氛围|营造|呈现).*?([^\n。]{5,30}?(?:温馨|舒适|精致|高级|松弛|静谧|优雅|时尚))/);
  503. if (moodMatch) {
  504. styleInfo += `,${moodMatch[1].trim()}`;
  505. }
  506. } else if (rawContent) {
  507. // 从rawContent提取风格
  508. const styleMatch = rawContent.match(/(?:风格|呈现|属于).*?([^\n。]{5,40}?(?:风格|法式|现代|简约|极简))/);
  509. if (styleMatch) styleInfo = styleMatch[1].trim();
  510. }
  511. if (styleInfo) {
  512. notes.push(`【风格定位】\n${styleInfo}`);
  513. }
  514. // 4. 色调要求(从色彩分析提取)
  515. let colorInfo = '';
  516. if (structuredData?.colorAnalysis) {
  517. // 提取主色调
  518. const mainColorMatch = structuredData.colorAnalysis.match(/主色调[::]\s*([^\n。]{5,50})/);
  519. if (mainColorMatch) {
  520. colorInfo = `主色调:${mainColorMatch[1].trim()}`;
  521. }
  522. // 提取辅助色
  523. const subColorMatch = structuredData.colorAnalysis.match(/辅助色[::]\s*([^\n。]{5,50})/);
  524. if (subColorMatch) {
  525. colorInfo += `\n辅助色:${subColorMatch[1].trim()}`;
  526. }
  527. } else if (rawContent) {
  528. // 从rawContent提取色调
  529. const colorMatch = rawContent.match(/(?:色调|色彩|主色)[::]\s*([^\n。]{5,50})/);
  530. if (colorMatch) colorInfo = colorMatch[1].trim();
  531. }
  532. if (colorInfo) {
  533. notes.push(`【色调要求】\n${colorInfo}`);
  534. }
  535. // 5. 材质要求(从硬装和材质维度提取)
  536. const materials: string[] = [];
  537. if (structuredData?.hardDecoration) {
  538. // 提取地面材质
  539. const floorMatch = structuredData.hardDecoration.match(/地面[::]\s*([^\n。]{5,40})/);
  540. if (floorMatch) materials.push(`地面:${floorMatch[1].trim()}`);
  541. // 提取墙面材质
  542. const wallMatch = structuredData.hardDecoration.match(/墙面[::]\s*([^\n。]{5,40})/);
  543. if (wallMatch) materials.push(`墙面:${wallMatch[1].trim()}`);
  544. // 提取顶面材质
  545. const ceilingMatch = structuredData.hardDecoration.match(/顶面[::]\s*([^\n。]{5,40})/);
  546. if (ceilingMatch) materials.push(`顶面:${ceilingMatch[1].trim()}`);
  547. }
  548. // 从材质维度补充
  549. if (structuredData?.materials && materials.length < 2) {
  550. const materialMatch = structuredData.materials.match(/(?:主要材质|材质应用)[::]\s*([^\n。]{10,60})/);
  551. if (materialMatch) materials.push(materialMatch[1].trim());
  552. }
  553. if (materials.length > 0) {
  554. notes.push(`【材质要求】\n${materials.join('\n')}`);
  555. }
  556. // 6. 空间布局要点
  557. if (structuredData?.layout) {
  558. const layoutMatch = structuredData.layout.match(/(?:布局特点|空间关系)[::]\s*([^\n。]{10,60})/);
  559. if (layoutMatch) {
  560. notes.push(`【布局要点】\n${layoutMatch[1].trim()}`);
  561. }
  562. }
  563. // 7. 施工注意事项(从优化建议提取)
  564. if (structuredData?.suggestions) {
  565. const attentionPoints: string[] = [];
  566. // 提取落地可行性
  567. const feasibilityMatch = structuredData.suggestions.match(/落地可行性[::]\s*([^\n。]{10,80})/);
  568. if (feasibilityMatch) attentionPoints.push(feasibilityMatch[1].trim());
  569. // 提取细节优化
  570. const detailMatch = structuredData.suggestions.match(/细节优化[::]\s*([^\n。]{10,80})/);
  571. if (detailMatch) attentionPoints.push(detailMatch[1].trim());
  572. if (attentionPoints.length > 0) {
  573. notes.push(`【施工注意】\n${attentionPoints.join('\n')}`);
  574. }
  575. }
  576. // 8. 品质要求(固定添加)
  577. notes.push(`【品质要求】\n新客户,需严格把控施工品质和材料质量`);
  578. return notes.length > 0 ? notes.join('\n\n') : '请根据分析内容补充具体要求';
  579. }
  580. /**
  581. * 生成客户报告(此方法保留以便后续使用)
  582. */
  583. async generateClientReport(options: {
  584. analysisData: any;
  585. spaceName: string;
  586. onContentChange?: (content: string) => void;
  587. loading?: any;
  588. }): Promise<string> {
  589. return new Promise(async (resolve, reject) => {
  590. try {
  591. // 使用格式化后的内容
  592. const content = options.analysisData?.formattedContent || options.analysisData?.rawContent || '暂无报告内容';
  593. resolve(content);
  594. } catch (error: any) {
  595. reject(new Error('生成报告失败: ' + error.message));
  596. }
  597. });
  598. }
  599. }