convert_v8_to_graph_v3.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. /**
  2. * 将 v6.1.2.8 的 run_context.json 转换成按 Round > 步骤 > 数据 组织的图结构
  3. * v3: 增加 [Q] 和 [SUG] 标识前缀
  4. */
  5. // 得分提升阈值:与Python代码保持一致
  6. const REQUIRED_SCORE_GAIN = 0.02;
  7. function convertV8ToGraphV2(runContext, searchResults, extractionData) {
  8. const nodes = {};
  9. const edges = [];
  10. const iterations = {};
  11. extractionData = extractionData || {}; // 默认为空对象
  12. const o = runContext.o || '原始问题';
  13. const rounds = runContext.rounds || [];
  14. // 添加原始问题根节点
  15. const rootId = 'root_o';
  16. nodes[rootId] = {
  17. type: 'root',
  18. query: o,
  19. level: 0,
  20. relevance_score: 1.0,
  21. strategy: '原始问题',
  22. iteration: 0,
  23. is_selected: true
  24. };
  25. iterations[0] = [rootId];
  26. // 处理每一轮
  27. rounds.forEach((round, roundIndex) => {
  28. if (round.type === 'initialization') {
  29. // Round 0: 初始化阶段(新架构)
  30. const roundNum = 0;
  31. const roundId = `round_${roundNum}`;
  32. // 创建 Round 节点
  33. nodes[roundId] = {
  34. type: 'round',
  35. query: `Round ${roundNum} (初始化)`,
  36. level: roundNum * 10,
  37. relevance_score: 0,
  38. strategy: '初始化',
  39. iteration: roundNum,
  40. is_selected: true
  41. };
  42. edges.push({
  43. from: rootId,
  44. to: roundId,
  45. edge_type: 'root_to_round',
  46. strategy: '初始化'
  47. });
  48. if (!iterations[roundNum * 10]) iterations[roundNum * 10] = [];
  49. iterations[roundNum * 10].push(roundId);
  50. // 步骤1: 分段
  51. if (round.segments && round.segments.length > 0) {
  52. const segStepId = `step_seg_r${roundNum}`;
  53. nodes[segStepId] = {
  54. type: 'step',
  55. query: `步骤1: 分段 (${round.segments.length}个segment)`,
  56. level: roundNum * 10 + 1,
  57. relevance_score: 0,
  58. strategy: '分段',
  59. iteration: roundNum,
  60. is_selected: true
  61. };
  62. edges.push({
  63. from: roundId,
  64. to: segStepId,
  65. edge_type: 'round_to_step',
  66. strategy: '分段'
  67. });
  68. iterations[roundNum * 10 + 1] = [segStepId];
  69. // 为每个 Segment 创建节点
  70. round.segments.forEach((seg, segIndex) => {
  71. const segId = `segment_${segIndex}_r${roundNum}`;
  72. nodes[segId] = {
  73. type: 'segment',
  74. query: `[${seg.type}] ${seg.text}`,
  75. level: roundNum * 10 + 2,
  76. relevance_score: seg.score || 0,
  77. evaluationReason: seg.reason || '',
  78. strategy: seg.type,
  79. iteration: roundNum,
  80. is_selected: true,
  81. segment_type: seg.type,
  82. domain_index: seg.domain_index,
  83. domain_type: seg.type // 新增:让可视化显示类型而不是D0
  84. };
  85. edges.push({
  86. from: segStepId,
  87. to: segId,
  88. edge_type: 'step_to_segment',
  89. strategy: seg.type
  90. });
  91. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  92. iterations[roundNum * 10 + 2].push(segId);
  93. // 为每个 Word 创建节点
  94. if (seg.words && seg.words.length > 0) {
  95. seg.words.forEach((word, wordIndex) => {
  96. const wordId = `word_${word.text}_seg${segIndex}_${wordIndex}`;
  97. nodes[wordId] = {
  98. type: 'word',
  99. query: word.text,
  100. level: roundNum * 10 + 3,
  101. relevance_score: word.score || 0,
  102. evaluationReason: word.reason || '',
  103. strategy: 'Word',
  104. iteration: roundNum,
  105. is_selected: true
  106. };
  107. edges.push({
  108. from: segId,
  109. to: wordId,
  110. edge_type: 'segment_to_word',
  111. strategy: 'Word'
  112. });
  113. if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
  114. iterations[roundNum * 10 + 3].push(wordId);
  115. });
  116. }
  117. });
  118. }
  119. // 步骤2: 域内组词
  120. if (round.step2_domain_combinations && round.step2_domain_combinations.length > 0) {
  121. const combStepId = `step_comb_r${roundNum}`;
  122. nodes[combStepId] = {
  123. type: 'step',
  124. query: `步骤2: 域内组词 (${round.step2_domain_combinations.length}个组合)`,
  125. level: roundNum * 10 + 1,
  126. relevance_score: 0,
  127. strategy: '域内组词',
  128. iteration: roundNum,
  129. is_selected: true
  130. };
  131. edges.push({
  132. from: roundId,
  133. to: combStepId,
  134. edge_type: 'round_to_step',
  135. strategy: '域内组词'
  136. });
  137. if (!iterations[roundNum * 10 + 1]) iterations[roundNum * 10 + 1] = [];
  138. iterations[roundNum * 10 + 1].push(combStepId);
  139. // 为每个域内组合创建节点
  140. round.step2_domain_combinations.forEach((comb, combIndex) => {
  141. const combId = `comb_${comb.text}_r${roundNum}_${combIndex}`;
  142. const sourceWordsStr = comb.source_words ? comb.source_words.map(words => words.join(',')).join(' + ') : '';
  143. nodes[combId] = {
  144. type: 'domain_combination',
  145. query: `${comb.text} ${comb.type_label}`,
  146. level: roundNum * 10 + 2,
  147. relevance_score: comb.score || 0,
  148. evaluationReason: comb.reason || '',
  149. strategy: '域内组合',
  150. iteration: roundNum,
  151. is_selected: true,
  152. type_label: comb.type_label,
  153. source_words: comb.source_words,
  154. source_segment: comb.source_segment,
  155. domain_index: comb.domain_index
  156. };
  157. edges.push({
  158. from: combStepId,
  159. to: combId,
  160. edge_type: 'step_to_comb',
  161. strategy: '域内组合'
  162. });
  163. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  164. iterations[roundNum * 10 + 2].push(combId);
  165. });
  166. }
  167. } else {
  168. // 普通轮次
  169. const roundNum = round.round_num;
  170. const roundId = `round_${roundNum}`;
  171. // 创建 Round 节点
  172. nodes[roundId] = {
  173. type: 'round',
  174. query: `Round ${roundNum}`,
  175. level: roundNum * 10, // 使用10的倍数作为层级
  176. relevance_score: 0,
  177. strategy: `第${roundNum}轮`,
  178. iteration: roundNum,
  179. is_selected: true
  180. };
  181. edges.push({
  182. from: rootId,
  183. to: roundId,
  184. edge_type: 'root_to_round',
  185. strategy: `第${roundNum}轮`
  186. });
  187. if (!iterations[roundNum * 10]) iterations[roundNum * 10] = [];
  188. iterations[roundNum * 10].push(roundId);
  189. // 步骤1: 请求&评估推荐词
  190. if (round.sug_details && Object.keys(round.sug_details).length > 0) {
  191. const sugStepId = `step_sug_r${roundNum}`;
  192. const totalSugs = Object.values(round.sug_details).reduce((sum, list) => sum + list.length, 0);
  193. nodes[sugStepId] = {
  194. type: 'step',
  195. query: `步骤1: 请求&评估推荐词 (${totalSugs}个)`,
  196. level: roundNum * 10 + 1,
  197. relevance_score: 0,
  198. strategy: '请求&评估推荐词',
  199. iteration: roundNum,
  200. is_selected: true
  201. };
  202. edges.push({
  203. from: roundId,
  204. to: sugStepId,
  205. edge_type: 'round_to_step',
  206. strategy: '推荐词'
  207. });
  208. iterations[roundNum * 10].push(sugStepId);
  209. // 为每个 Q 创建节点
  210. Object.keys(round.sug_details).forEach((qText, qIndex) => {
  211. // 从q_list_1中查找对应的q获取分数和理由
  212. // Round 0: 从q_list_1查找; Round 1+: 从input_queries查找
  213. let qData = {};
  214. if (roundNum === 0) {
  215. qData = round.q_list_1?.find(q => q.text === qText) || {};
  216. } else {
  217. // 从当前轮的input_queries中查找
  218. qData = round.input_queries?.find(q => q.text === qText) || {};
  219. }
  220. const qId = `q_${qText}_r${roundNum}_${qIndex}`;
  221. nodes[qId] = {
  222. type: 'q',
  223. query: '[Q] ' + qText,
  224. level: roundNum * 10 + 2,
  225. relevance_score: qData.score || 0,
  226. evaluationReason: qData.reason || '',
  227. strategy: 'Query',
  228. iteration: roundNum,
  229. is_selected: true,
  230. type_label: qData.type_label || qData.typeLabel || '',
  231. domain_index: qData.domain_index,
  232. domain_type: qData.domain_type || ''
  233. };
  234. edges.push({
  235. from: sugStepId,
  236. to: qId,
  237. edge_type: 'step_to_q',
  238. strategy: 'Query'
  239. });
  240. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  241. iterations[roundNum * 10 + 2].push(qId);
  242. // 为每个 Q 的 sug 创建节点
  243. const sugs = round.sug_details[qText] || [];
  244. const qScore = qData.score || 0; // 获取父Q的得分
  245. sugs.forEach((sug, sugIndex) => {
  246. const sugScore = sug.score || 0;
  247. // 比较得分决定颜色:SUG得分 >= Q得分 + 0.05 → 绿色,否则 → 红色
  248. const scoreColor = (sugScore >= qScore + REQUIRED_SCORE_GAIN) ? '#22c55e' : '#ef4444';
  249. const sugId = `sug_${sug.text}_r${roundNum}_q${qIndex}_${sugIndex}`;
  250. nodes[sugId] = {
  251. type: 'sug',
  252. query: '[SUG] ' + sug.text,
  253. level: roundNum * 10 + 3,
  254. relevance_score: sugScore,
  255. evaluationReason: sug.reason || '',
  256. strategy: '推荐词',
  257. iteration: roundNum,
  258. is_selected: true,
  259. scoreColor: scoreColor, // 新增:用于控制文字颜色
  260. parentQScore: qScore // 新增:保存父Q得分用于调试
  261. };
  262. edges.push({
  263. from: qId,
  264. to: sugId,
  265. edge_type: 'q_to_sug',
  266. strategy: '推荐词'
  267. });
  268. if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
  269. iterations[roundNum * 10 + 3].push(sugId);
  270. });
  271. });
  272. }
  273. // 步骤2: 域内组词(Round 1+)
  274. // 兼容旧字段名 domain_combinations_top10
  275. const domainCombinations = round.domain_combinations || round.domain_combinations_top10 || [];
  276. if (domainCombinations.length > 0) {
  277. const combStepId = `step_comb_r${roundNum}`;
  278. nodes[combStepId] = {
  279. type: 'step',
  280. query: roundNum === 1
  281. ? `步骤2: 域内组合 (${domainCombinations.length}个组合)`
  282. : `步骤2: 跨${roundNum}个域组合 (${domainCombinations.length}个组合)`,
  283. level: roundNum * 10 + 1,
  284. relevance_score: 0,
  285. strategy: '域内组词',
  286. iteration: roundNum,
  287. is_selected: true
  288. };
  289. edges.push({
  290. from: roundId,
  291. to: combStepId,
  292. edge_type: 'round_to_step',
  293. strategy: '域内组词'
  294. });
  295. iterations[roundNum * 10].push(combStepId);
  296. // 为每个域内组合创建节点
  297. domainCombinations.forEach((comb, combIndex) => {
  298. const combId = `comb_${comb.text}_r${roundNum}_${combIndex}`;
  299. const domainsStr = comb.domains ? comb.domains.map(d => `D${d}`).join(',') : '';
  300. const wordDetails = comb.source_word_details || [];
  301. const isAboveSources = comb.is_above_source_scores || false;
  302. const scoreColor = wordDetails.length > 0
  303. ? (isAboveSources ? '#22c55e' : '#ef4444')
  304. : null;
  305. nodes[combId] = {
  306. type: 'domain_combination',
  307. query: `${comb.text}`, // 移除 type_label,稍后在UI中单独显示
  308. level: roundNum * 10 + 2,
  309. relevance_score: comb.score || 0,
  310. evaluationReason: comb.reason || '',
  311. strategy: '域内组合',
  312. iteration: roundNum,
  313. is_selected: true,
  314. type_label: comb.type_label || '',
  315. source_words: comb.source_words || [],
  316. from_segments: comb.from_segments || [],
  317. domains: comb.domains || [],
  318. domains_str: domainsStr,
  319. source_word_details: wordDetails,
  320. source_scores: comb.source_scores || [],
  321. is_above_sources: isAboveSources,
  322. max_source_score: comb.max_source_score ?? null,
  323. scoreColor: scoreColor
  324. };
  325. edges.push({
  326. from: combStepId,
  327. to: combId,
  328. edge_type: 'step_to_comb',
  329. strategy: '域内组合'
  330. });
  331. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  332. iterations[roundNum * 10 + 2].push(combId);
  333. });
  334. }
  335. // 步骤3: 筛选并执行搜索
  336. const searchStepId = `step_search_r${roundNum}`;
  337. const searchCountText = round.search_count > 0
  338. ? `筛选${round.high_score_sug_count}个高分词,搜索${round.search_count}次,${round.total_posts}个帖子`
  339. : `无高分推荐词,未执行搜索`;
  340. nodes[searchStepId] = {
  341. type: 'step',
  342. query: `步骤3: 筛选并执行搜索 (${searchCountText})`,
  343. level: roundNum * 10 + 1,
  344. relevance_score: 0,
  345. strategy: '筛选并执行搜索',
  346. iteration: roundNum,
  347. is_selected: true
  348. };
  349. edges.push({
  350. from: roundId,
  351. to: searchStepId,
  352. edge_type: 'round_to_step',
  353. strategy: '搜索'
  354. });
  355. iterations[roundNum * 10].push(searchStepId);
  356. // 只有在有搜索结果时才添加搜索词和帖子
  357. // 优先使用 round.search_results(新格式),否则使用外部传入的 searchResults(兼容旧版本)
  358. const roundSearchResults = round.search_results || searchResults;
  359. if (round.search_count > 0 && roundSearchResults) {
  360. if (Array.isArray(roundSearchResults)) {
  361. roundSearchResults.forEach((search, searchIndex) => {
  362. const searchWordId = `search_${search.text}_r${roundNum}_${searchIndex}`;
  363. nodes[searchWordId] = {
  364. type: 'search_word',
  365. query: '[SEARCH] ' + search.text,
  366. level: roundNum * 10 + 2,
  367. relevance_score: search.score_with_o || 0,
  368. strategy: '搜索词',
  369. iteration: roundNum,
  370. is_selected: true
  371. };
  372. edges.push({
  373. from: searchStepId,
  374. to: searchWordId,
  375. edge_type: 'step_to_search_word',
  376. strategy: '搜索词'
  377. });
  378. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  379. iterations[roundNum * 10 + 2].push(searchWordId);
  380. // 添加帖子
  381. if (search.post_list && search.post_list.length > 0) {
  382. search.post_list.forEach((post, postIndex) => {
  383. const postId = `post_${post.note_id}_${searchIndex}_${postIndex}`;
  384. // 准备图片列表,将URL字符串转换为对象格式供轮播图使用
  385. const imageList = (post.images || []).map(url => ({
  386. image_url: url
  387. }));
  388. nodes[postId] = {
  389. type: 'post',
  390. query: '[R] ' + post.title,
  391. level: roundNum * 10 + 3,
  392. relevance_score: 0,
  393. strategy: '帖子',
  394. iteration: roundNum,
  395. is_selected: true,
  396. note_id: post.note_id,
  397. note_url: post.note_url,
  398. body_text: post.body_text || '',
  399. images: post.images || [],
  400. image_list: imageList,
  401. interact_info: post.interact_info || {},
  402. // 附加多模态提取数据
  403. extraction: extractionData && extractionData[post.note_id] ? extractionData[post.note_id] : null,
  404. // 评估数据 (V2)
  405. is_knowledge: post.is_knowledge !== undefined ? post.is_knowledge : null,
  406. knowledge_reason: post.knowledge_reason || '',
  407. knowledge_score: post.knowledge_score !== undefined ? post.knowledge_score : null,
  408. knowledge_level: post.knowledge_level !== undefined ? post.knowledge_level : null,
  409. knowledge_evaluation: post.knowledge_evaluation || null,
  410. post_relevance_score: post.relevance_score !== undefined ? post.relevance_score : null,
  411. relevance_level: post.relevance_level || '',
  412. relevance_reason: post.relevance_reason || '',
  413. relevance_conclusion: post.relevance_conclusion || '',
  414. relevance_evaluation: post.relevance_evaluation || null
  415. };
  416. edges.push({
  417. from: searchWordId,
  418. to: postId,
  419. edge_type: 'search_word_to_post',
  420. strategy: '搜索结果'
  421. });
  422. // 如果有提取数据,创建对应的分析节点
  423. if (extractionData && extractionData[post.note_id]) {
  424. const analysisId = `analysis_${post.note_id}_${searchIndex}_${postIndex}`;
  425. nodes[analysisId] = {
  426. type: 'analysis',
  427. query: '[AI分析] ' + post.title,
  428. level: roundNum * 10 + 4,
  429. relevance_score: 0,
  430. strategy: 'AI分析',
  431. iteration: roundNum,
  432. is_selected: true,
  433. note_id: post.note_id,
  434. note_url: post.note_url,
  435. title: post.title,
  436. body_text: post.body_text || '',
  437. interact_info: post.interact_info || {},
  438. extraction: extractionData[post.note_id],
  439. image_list: imageList
  440. };
  441. edges.push({
  442. from: postId,
  443. to: analysisId,
  444. edge_type: 'post_to_analysis',
  445. strategy: 'AI分析'
  446. });
  447. if (!iterations[roundNum * 10 + 4]) iterations[roundNum * 10 + 4] = [];
  448. iterations[roundNum * 10 + 4].push(analysisId);
  449. }
  450. if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
  451. iterations[roundNum * 10 + 3].push(postId);
  452. });
  453. }
  454. });
  455. }
  456. }
  457. // 步骤3: 加词生成新查询
  458. if (round.add_word_details && Object.keys(round.add_word_details).length > 0) {
  459. const addWordStepId = `step_add_r${roundNum}`;
  460. const totalAddWords = Object.values(round.add_word_details).reduce((sum, list) => sum + list.length, 0);
  461. nodes[addWordStepId] = {
  462. type: 'step',
  463. query: `步骤3: 加词生成新查询 (${totalAddWords}个)`,
  464. level: roundNum * 10 + 1,
  465. relevance_score: 0,
  466. strategy: '加词生成新查询',
  467. iteration: roundNum,
  468. is_selected: true
  469. };
  470. edges.push({
  471. from: roundId,
  472. to: addWordStepId,
  473. edge_type: 'round_to_step',
  474. strategy: '加词'
  475. });
  476. iterations[roundNum * 10].push(addWordStepId);
  477. // 为每个 Seed 创建节点
  478. Object.keys(round.add_word_details).forEach((seedText, seedIndex) => {
  479. const seedId = `seed_${seedText}_r${roundNum}_${seedIndex}`;
  480. // 查找seed的来源信息和分数 - 动态从正确的轮次查找
  481. let seedInfo = {};
  482. if (roundNum === 1) {
  483. // Round 1:种子来自 Round 0 的 seed_list
  484. const round0 = rounds.find(r => r.round_num === 0 || r.type === 'initialization');
  485. seedInfo = round0?.seed_list?.find(s => s.text === seedText) || {};
  486. } else {
  487. // Round 2+:种子来自前一轮的 seed_list_next
  488. const prevRound = rounds.find(r => r.round_num === roundNum - 1);
  489. seedInfo = prevRound?.seed_list_next?.find(s => s.text === seedText) || {};
  490. }
  491. const fromType = seedInfo.from_type || seedInfo.from || 'unknown';
  492. // 根据来源设置strategy
  493. let strategy;
  494. if (fromType === 'seg') {
  495. strategy = '初始分词';
  496. } else if (fromType === 'add') {
  497. strategy = '加词';
  498. } else if (fromType === 'sug') {
  499. strategy = '调用sug';
  500. } else {
  501. strategy = 'Seed'; // 默认灰色
  502. }
  503. nodes[seedId] = {
  504. type: 'seed',
  505. query: seedText,
  506. level: roundNum * 10 + 2,
  507. relevance_score: seedInfo.score || 0, // 从seedInfo读取种子的得分
  508. strategy: strategy,
  509. iteration: roundNum,
  510. is_selected: true
  511. };
  512. edges.push({
  513. from: addWordStepId,
  514. to: seedId,
  515. edge_type: 'step_to_seed',
  516. strategy: 'Seed'
  517. });
  518. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  519. iterations[roundNum * 10 + 2].push(seedId);
  520. // 为每个 Seed 的组合词创建节点
  521. const combinedWords = round.add_word_details[seedText] || [];
  522. combinedWords.forEach((word, wordIndex) => {
  523. const wordScore = word.score || 0;
  524. const seedScore = word.seed_score || 0;
  525. // 比较得分决定颜色:组合词得分 > 种子得分 → 绿色,否则 → 红色
  526. const scoreColor = wordScore > seedScore ? '#22c55e' : '#ef4444';
  527. const wordId = `add_${word.text}_r${roundNum}_seed${seedIndex}_${wordIndex}`;
  528. nodes[wordId] = {
  529. type: 'add_word',
  530. query: '[Q] ' + word.text,
  531. level: roundNum * 10 + 3,
  532. relevance_score: wordScore,
  533. evaluationReason: word.reason || '',
  534. strategy: '加词生成',
  535. iteration: roundNum,
  536. is_selected: true,
  537. selected_word: word.selected_word,
  538. seed_score: seedScore, // 原始种子的得分
  539. scoreColor: scoreColor // 用于控制文字颜色
  540. };
  541. edges.push({
  542. from: seedId,
  543. to: wordId,
  544. edge_type: 'seed_to_add_word',
  545. strategy: '组合词'
  546. });
  547. if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
  548. iterations[roundNum * 10 + 3].push(wordId);
  549. });
  550. });
  551. }
  552. // 步骤4: 筛选推荐词进入下轮
  553. const filteredSugs = round.output_q_list?.filter(q => q.from === 'sug') || [];
  554. if (filteredSugs.length > 0) {
  555. const filterStepId = `step_filter_r${roundNum}`;
  556. nodes[filterStepId] = {
  557. type: 'step',
  558. query: `步骤4: 筛选推荐词进入下轮 (${filteredSugs.length}个)`,
  559. level: roundNum * 10 + 1,
  560. relevance_score: 0,
  561. strategy: '筛选推荐词进入下轮',
  562. iteration: roundNum,
  563. is_selected: true
  564. };
  565. edges.push({
  566. from: roundId,
  567. to: filterStepId,
  568. edge_type: 'round_to_step',
  569. strategy: '筛选'
  570. });
  571. iterations[roundNum * 10].push(filterStepId);
  572. // 添加筛选出的sug
  573. filteredSugs.forEach((sug, sugIndex) => {
  574. const sugScore = sug.score || 0;
  575. // 尝试从sug_details中找到这个sug对应的父Q得分
  576. let parentQScore = 0;
  577. if (round.sug_details) {
  578. for (const [qText, sugs] of Object.entries(round.sug_details)) {
  579. const matchingSug = sugs.find(s => s.text === sug.text);
  580. if (matchingSug) {
  581. // 找到父Q的得分
  582. let qData = {};
  583. if (roundNum === 0) {
  584. qData = round.q_list_1?.find(q => q.text === qText) || {};
  585. } else {
  586. qData = round.input_queries?.find(q => q.text === qText) || {};
  587. }
  588. parentQScore = qData.score || 0;
  589. break;
  590. }
  591. }
  592. }
  593. // 比较得分决定颜色:SUG得分 > Q得分 → 绿色,否则 → 红色
  594. const scoreColor = (sugScore >= parentQScore + REQUIRED_SCORE_GAIN) ? '#22c55e' : '#ef4444';
  595. const sugId = `filtered_sug_${sug.text}_r${roundNum}_${sugIndex}`;
  596. nodes[sugId] = {
  597. type: 'filtered_sug',
  598. query: '[SUG] ' + sug.text,
  599. level: roundNum * 10 + 2,
  600. relevance_score: sugScore,
  601. strategy: '进入下轮',
  602. iteration: roundNum,
  603. is_selected: true,
  604. scoreColor: scoreColor, // 新增:用于控制文字颜色
  605. parentQScore: parentQScore // 新增:保存父Q得分用于调试
  606. };
  607. edges.push({
  608. from: filterStepId,
  609. to: sugId,
  610. edge_type: 'step_to_filtered_sug',
  611. strategy: '进入下轮'
  612. });
  613. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  614. iterations[roundNum * 10 + 2].push(sugId);
  615. });
  616. }
  617. // 步骤4: 构建下一轮(Round 1+)
  618. const highScoreCombinations = round.high_score_combinations || [];
  619. const highGainSugs = round.high_gain_sugs || [];
  620. const nextRoundItems = [...highScoreCombinations, ...highGainSugs];
  621. if (nextRoundItems.length > 0) {
  622. const nextRoundStepId = `step_next_round_r${roundNum}`;
  623. nodes[nextRoundStepId] = {
  624. type: 'step',
  625. query: `步骤4: 构建下一轮 (${nextRoundItems.length}个查询)`,
  626. level: roundNum * 10 + 1,
  627. relevance_score: 0,
  628. strategy: '构建下一轮',
  629. iteration: roundNum,
  630. is_selected: true
  631. };
  632. edges.push({
  633. from: roundId,
  634. to: nextRoundStepId,
  635. edge_type: 'round_to_step',
  636. strategy: '构建下一轮'
  637. });
  638. iterations[roundNum * 10].push(nextRoundStepId);
  639. // 创建查询节点
  640. nextRoundItems.forEach((item, index) => {
  641. const itemId = `next_round_${item.text}_r${roundNum}_${index}`;
  642. const isSugItem = item.type === 'sug';
  643. nodes[itemId] = {
  644. type: 'next_round_item',
  645. query: '[Q] ' + item.text,
  646. level: roundNum * 10 + 2,
  647. relevance_score: item.score || 0,
  648. strategy: item.type === 'combination' ? '域内组合' : '高增益SUG',
  649. iteration: roundNum,
  650. is_selected: true,
  651. type_label: item.type_label || '',
  652. item_type: item.type,
  653. is_suggestion: isSugItem,
  654. suggestion_label: isSugItem ? '[suggestion]' : ''
  655. };
  656. edges.push({
  657. from: nextRoundStepId,
  658. to: itemId,
  659. edge_type: 'step_to_next_round',
  660. strategy: item.type === 'combination' ? '域内组合' : 'SUG'
  661. });
  662. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  663. iterations[roundNum * 10 + 2].push(itemId);
  664. });
  665. }
  666. }
  667. });
  668. return {
  669. nodes,
  670. edges,
  671. iterations
  672. };
  673. }
  674. /**
  675. * 简化版转换:专注于query和post的演化
  676. * - 合并所有query节点(不区分seg/sug/add_word)
  677. * - 合并相同的帖子节点
  678. * - 步骤信息放在边上
  679. * - 隐藏Round/Step节点
  680. */
  681. function convertV8ToGraphSimplified(runContext, searchResults, extractionData) {
  682. const mergedNodes = {};
  683. const edges = [];
  684. const iterations = {};
  685. extractionData = extractionData || {}; // 默认为空对象
  686. const o = runContext.o || '原始问题';
  687. const rounds = runContext.rounds || [];
  688. // 添加原始问题根节点
  689. const rootId = 'root_o';
  690. mergedNodes[rootId] = {
  691. type: 'root',
  692. query: o,
  693. level: 0,
  694. relevance_score: 1.0,
  695. strategy: '原始问题',
  696. iteration: 0,
  697. is_selected: true,
  698. occurrences: [{round: 0, role: 'root', score: 1.0}]
  699. };
  700. iterations[0] = [rootId];
  701. // 用于记录节点之间的演化关系
  702. const queryEvolution = {}; // {text: {occurrences: [], parentTexts: [], childTexts: []}}
  703. const postMap = {}; // {note_id: {...}}
  704. // 第一遍:收集所有query和post
  705. rounds.forEach((round, roundIndex) => {
  706. const roundNum = round.round_num || roundIndex;
  707. if (round.type === 'initialization') {
  708. // Round 0: 收集分词结果
  709. (round.q_list_1 || []).forEach(q => {
  710. if (!queryEvolution[q.text]) {
  711. queryEvolution[q.text] = {
  712. occurrences: [],
  713. parentTexts: new Set([o]), // 来自原始问题
  714. childTexts: new Set()
  715. };
  716. }
  717. queryEvolution[q.text].occurrences.push({
  718. round: roundNum,
  719. role: 'segmentation',
  720. strategy: '分词',
  721. score: q.score,
  722. reason: q.reason,
  723. type_label: q.type_label || q.typeLabel || ''
  724. });
  725. });
  726. } else {
  727. // Round 1+
  728. // 收集sug_details (推荐词)
  729. Object.entries(round.sug_details || {}).forEach(([parentText, sugs]) => {
  730. sugs.forEach(sug => {
  731. if (!queryEvolution[sug.text]) {
  732. queryEvolution[sug.text] = {
  733. occurrences: [],
  734. parentTexts: new Set(),
  735. childTexts: new Set()
  736. };
  737. }
  738. queryEvolution[sug.text].occurrences.push({
  739. round: roundNum,
  740. role: 'sug',
  741. strategy: '调用sug',
  742. score: sug.score,
  743. reason: sug.reason,
  744. type_label: sug.type_label || sug.typeLabel || ''
  745. });
  746. queryEvolution[sug.text].parentTexts.add(parentText);
  747. if (queryEvolution[parentText]) {
  748. queryEvolution[parentText].childTexts.add(sug.text);
  749. }
  750. });
  751. });
  752. // 收集add_word_details (加词结果)
  753. Object.entries(round.add_word_details || {}).forEach(([seedText, words]) => {
  754. words.forEach(word => {
  755. if (!queryEvolution[word.text]) {
  756. queryEvolution[word.text] = {
  757. occurrences: [],
  758. parentTexts: new Set(),
  759. childTexts: new Set()
  760. };
  761. }
  762. queryEvolution[word.text].occurrences.push({
  763. round: roundNum,
  764. role: 'add_word',
  765. strategy: '加词',
  766. score: word.score,
  767. reason: word.reason,
  768. selectedWord: word.selected_word,
  769. seedScore: word.seed_score, // 添加原始种子的得分
  770. type_label: word.type_label || word.typeLabel || ''
  771. });
  772. queryEvolution[word.text].parentTexts.add(seedText);
  773. if (queryEvolution[seedText]) {
  774. queryEvolution[seedText].childTexts.add(word.text);
  775. }
  776. });
  777. });
  778. // 收集搜索结果和帖子
  779. const roundSearchResults = round.search_results || searchResults;
  780. if (roundSearchResults && Array.isArray(roundSearchResults)) {
  781. roundSearchResults.forEach(search => {
  782. const searchText = search.text;
  783. // 标记这个query被用于搜索
  784. if (queryEvolution[searchText]) {
  785. queryEvolution[searchText].occurrences.push({
  786. round: roundNum,
  787. role: 'search',
  788. strategy: '执行搜索',
  789. score: search.score_with_o,
  790. postCount: search.post_list ? search.post_list.length : 0
  791. });
  792. }
  793. // 收集帖子
  794. if (search.post_list && search.post_list.length > 0) {
  795. search.post_list.forEach(post => {
  796. if (!postMap[post.note_id]) {
  797. postMap[post.note_id] = {
  798. ...post,
  799. foundByQueries: new Set(),
  800. foundInRounds: new Set()
  801. };
  802. }
  803. postMap[post.note_id].foundByQueries.add(searchText);
  804. postMap[post.note_id].foundInRounds.add(roundNum);
  805. // 建立query到post的关系
  806. if (!queryEvolution[searchText].posts) {
  807. queryEvolution[searchText].posts = new Set();
  808. }
  809. queryEvolution[searchText].posts.add(post.note_id);
  810. });
  811. }
  812. });
  813. }
  814. }
  815. });
  816. // 第二遍:创建合并后的节点
  817. Object.entries(queryEvolution).forEach(([text, data]) => {
  818. const nodeId = `query_${text}`;
  819. // 获取最新的分数
  820. const latestOccurrence = data.occurrences[data.occurrences.length - 1] || {};
  821. const hasSearchResults = data.posts && data.posts.size > 0;
  822. mergedNodes[nodeId] = {
  823. type: 'query',
  824. query: '[Q] ' + text,
  825. level: Math.max(...data.occurrences.map(o => o.round), 0) * 10 + 2,
  826. relevance_score: latestOccurrence.score || 0,
  827. evaluationReason: latestOccurrence.reason || '',
  828. strategy: data.occurrences.map(o => o.strategy).join(' + '),
  829. primaryStrategy: latestOccurrence.strategy || '未知', // 添加主要策略字段
  830. iteration: Math.max(...data.occurrences.map(o => o.round), 0),
  831. is_selected: true,
  832. occurrences: data.occurrences,
  833. hasSearchResults: hasSearchResults,
  834. postCount: data.posts ? data.posts.size : 0,
  835. selectedWord: data.occurrences.find(o => o.selectedWord)?.selectedWord || '',
  836. seed_score: data.occurrences.find(o => o.seedScore)?.seedScore, // 添加原始种子的得分
  837. type_label: latestOccurrence.type_label || '' // 使用最新的 type_label
  838. };
  839. // 添加到对应的轮次
  840. const maxRound = Math.max(...data.occurrences.map(o => o.round), 0);
  841. const iterKey = maxRound * 10 + 2;
  842. if (!iterations[iterKey]) iterations[iterKey] = [];
  843. iterations[iterKey].push(nodeId);
  844. });
  845. // 创建帖子节点
  846. Object.entries(postMap).forEach(([noteId, post]) => {
  847. const postId = `post_${noteId}`;
  848. const imageList = (post.images || []).map(url => ({
  849. image_url: url
  850. }));
  851. mergedNodes[postId] = {
  852. type: 'post',
  853. query: '[R] ' + post.title,
  854. level: 100, // 放在最后
  855. relevance_score: 0,
  856. strategy: '帖子',
  857. iteration: Math.max(...Array.from(post.foundInRounds)),
  858. is_selected: true,
  859. note_id: post.note_id,
  860. note_url: post.note_url,
  861. body_text: post.body_text || '',
  862. images: post.images || [],
  863. image_list: imageList,
  864. interact_info: post.interact_info || {},
  865. foundByQueries: Array.from(post.foundByQueries),
  866. foundInRounds: Array.from(post.foundInRounds),
  867. // 附加多模态提取数据
  868. extraction: extractionData && extractionData[post.note_id] ? extractionData[post.note_id] : null,
  869. // 评估数据
  870. is_knowledge: post.is_knowledge !== undefined ? post.is_knowledge : null,
  871. knowledge_reason: post.knowledge_reason || '',
  872. post_relevance_score: post.relevance_score !== undefined ? post.relevance_score : null,
  873. relevance_level: post.relevance_level || '',
  874. relevance_reason: post.relevance_reason || ''
  875. };
  876. if (!iterations[100]) iterations[100] = [];
  877. iterations[100].push(postId);
  878. // 如果有提取数据,创建对应的分析节点
  879. if (extractionData && extractionData[post.note_id]) {
  880. const analysisId = `analysis_${noteId}`;
  881. mergedNodes[analysisId] = {
  882. type: 'analysis',
  883. query: '[AI分析] ' + post.title,
  884. level: 101,
  885. relevance_score: 0,
  886. strategy: 'AI分析',
  887. iteration: Math.max(...Array.from(post.foundInRounds)),
  888. is_selected: true,
  889. note_id: post.note_id,
  890. note_url: post.note_url,
  891. title: post.title,
  892. body_text: post.body_text || '',
  893. interact_info: post.interact_info || {},
  894. extraction: extractionData[post.note_id],
  895. image_list: imageList
  896. };
  897. edges.push({
  898. from: postId,
  899. to: analysisId,
  900. edge_type: 'post_to_analysis',
  901. strategy: 'AI分析',
  902. label: 'AI分析',
  903. round: Math.max(...Array.from(post.foundInRounds))
  904. });
  905. if (!iterations[101]) iterations[101] = [];
  906. iterations[101].push(analysisId);
  907. }
  908. });
  909. // 第三遍:创建边
  910. // 1. 原始问题 -> 分词结果
  911. Object.entries(queryEvolution).forEach(([text, data]) => {
  912. const nodeId = `query_${text}`;
  913. const segOccurrence = data.occurrences.find(o => o.role === 'segmentation');
  914. if (segOccurrence && data.parentTexts.has(o)) {
  915. edges.push({
  916. from: rootId,
  917. to: nodeId,
  918. edge_type: 'segmentation',
  919. strategy: '分词',
  920. label: '分词',
  921. round: 0
  922. });
  923. }
  924. });
  925. // 2. Query演化关系
  926. Object.entries(queryEvolution).forEach(([text, data]) => {
  927. const nodeId = `query_${text}`;
  928. data.parentTexts.forEach(parentText => {
  929. if (parentText === o) return; // 跳过原始问题(已处理)
  930. const parentNodeId = `query_${parentText}`;
  931. if (!mergedNodes[parentNodeId]) return;
  932. // 找到这个演化的策略和轮次
  933. const occurrence = data.occurrences.find(o =>
  934. o.role === 'sug' || o.role === 'add_word'
  935. );
  936. edges.push({
  937. from: parentNodeId,
  938. to: nodeId,
  939. edge_type: occurrence?.role || 'evolution',
  940. strategy: occurrence?.strategy || '演化',
  941. label: `${occurrence?.strategy || '演化'} (R${occurrence?.round || 0})`,
  942. round: occurrence?.round || 0
  943. });
  944. });
  945. });
  946. // 3. Query -> Post (搜索关系)
  947. Object.entries(queryEvolution).forEach(([text, data]) => {
  948. const nodeId = `query_${text}`;
  949. if (data.posts && data.posts.size > 0) {
  950. const searchOccurrence = data.occurrences.find(o => o.role === 'search');
  951. data.posts.forEach(noteId => {
  952. const postId = `post_${noteId}`;
  953. edges.push({
  954. from: nodeId,
  955. to: postId,
  956. edge_type: 'search',
  957. strategy: '搜索',
  958. label: `搜索 (${data.posts.size}个帖子)`,
  959. round: searchOccurrence?.round || 0
  960. });
  961. });
  962. }
  963. });
  964. return {
  965. nodes: mergedNodes,
  966. edges,
  967. iterations
  968. };
  969. }
  970. module.exports = { convertV8ToGraphV2, convertV8ToGraphSimplified };