convert_v8_to_graph_v3.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  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. };
  405. edges.push({
  406. from: searchWordId,
  407. to: postId,
  408. edge_type: 'search_word_to_post',
  409. strategy: '搜索结果'
  410. });
  411. // 如果有提取数据,创建对应的分析节点
  412. if (extractionData && extractionData[post.note_id]) {
  413. const analysisId = `analysis_${post.note_id}_${searchIndex}_${postIndex}`;
  414. nodes[analysisId] = {
  415. type: 'analysis',
  416. query: '[AI分析] ' + post.title,
  417. level: roundNum * 10 + 4,
  418. relevance_score: 0,
  419. strategy: 'AI分析',
  420. iteration: roundNum,
  421. is_selected: true,
  422. note_id: post.note_id,
  423. note_url: post.note_url,
  424. title: post.title,
  425. body_text: post.body_text || '',
  426. interact_info: post.interact_info || {},
  427. extraction: extractionData[post.note_id],
  428. image_list: imageList
  429. };
  430. edges.push({
  431. from: postId,
  432. to: analysisId,
  433. edge_type: 'post_to_analysis',
  434. strategy: 'AI分析'
  435. });
  436. if (!iterations[roundNum * 10 + 4]) iterations[roundNum * 10 + 4] = [];
  437. iterations[roundNum * 10 + 4].push(analysisId);
  438. }
  439. if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
  440. iterations[roundNum * 10 + 3].push(postId);
  441. });
  442. }
  443. });
  444. }
  445. }
  446. // 步骤3: 加词生成新查询
  447. if (round.add_word_details && Object.keys(round.add_word_details).length > 0) {
  448. const addWordStepId = `step_add_r${roundNum}`;
  449. const totalAddWords = Object.values(round.add_word_details).reduce((sum, list) => sum + list.length, 0);
  450. nodes[addWordStepId] = {
  451. type: 'step',
  452. query: `步骤3: 加词生成新查询 (${totalAddWords}个)`,
  453. level: roundNum * 10 + 1,
  454. relevance_score: 0,
  455. strategy: '加词生成新查询',
  456. iteration: roundNum,
  457. is_selected: true
  458. };
  459. edges.push({
  460. from: roundId,
  461. to: addWordStepId,
  462. edge_type: 'round_to_step',
  463. strategy: '加词'
  464. });
  465. iterations[roundNum * 10].push(addWordStepId);
  466. // 为每个 Seed 创建节点
  467. Object.keys(round.add_word_details).forEach((seedText, seedIndex) => {
  468. const seedId = `seed_${seedText}_r${roundNum}_${seedIndex}`;
  469. // 查找seed的来源信息和分数 - 动态从正确的轮次查找
  470. let seedInfo = {};
  471. if (roundNum === 1) {
  472. // Round 1:种子来自 Round 0 的 seed_list
  473. const round0 = rounds.find(r => r.round_num === 0 || r.type === 'initialization');
  474. seedInfo = round0?.seed_list?.find(s => s.text === seedText) || {};
  475. } else {
  476. // Round 2+:种子来自前一轮的 seed_list_next
  477. const prevRound = rounds.find(r => r.round_num === roundNum - 1);
  478. seedInfo = prevRound?.seed_list_next?.find(s => s.text === seedText) || {};
  479. }
  480. const fromType = seedInfo.from_type || seedInfo.from || 'unknown';
  481. // 根据来源设置strategy
  482. let strategy;
  483. if (fromType === 'seg') {
  484. strategy = '初始分词';
  485. } else if (fromType === 'add') {
  486. strategy = '加词';
  487. } else if (fromType === 'sug') {
  488. strategy = '调用sug';
  489. } else {
  490. strategy = 'Seed'; // 默认灰色
  491. }
  492. nodes[seedId] = {
  493. type: 'seed',
  494. query: seedText,
  495. level: roundNum * 10 + 2,
  496. relevance_score: seedInfo.score || 0, // 从seedInfo读取种子的得分
  497. strategy: strategy,
  498. iteration: roundNum,
  499. is_selected: true
  500. };
  501. edges.push({
  502. from: addWordStepId,
  503. to: seedId,
  504. edge_type: 'step_to_seed',
  505. strategy: 'Seed'
  506. });
  507. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  508. iterations[roundNum * 10 + 2].push(seedId);
  509. // 为每个 Seed 的组合词创建节点
  510. const combinedWords = round.add_word_details[seedText] || [];
  511. combinedWords.forEach((word, wordIndex) => {
  512. const wordScore = word.score || 0;
  513. const seedScore = word.seed_score || 0;
  514. // 比较得分决定颜色:组合词得分 > 种子得分 → 绿色,否则 → 红色
  515. const scoreColor = wordScore > seedScore ? '#22c55e' : '#ef4444';
  516. const wordId = `add_${word.text}_r${roundNum}_seed${seedIndex}_${wordIndex}`;
  517. nodes[wordId] = {
  518. type: 'add_word',
  519. query: '[Q] ' + word.text,
  520. level: roundNum * 10 + 3,
  521. relevance_score: wordScore,
  522. evaluationReason: word.reason || '',
  523. strategy: '加词生成',
  524. iteration: roundNum,
  525. is_selected: true,
  526. selected_word: word.selected_word,
  527. seed_score: seedScore, // 原始种子的得分
  528. scoreColor: scoreColor // 用于控制文字颜色
  529. };
  530. edges.push({
  531. from: seedId,
  532. to: wordId,
  533. edge_type: 'seed_to_add_word',
  534. strategy: '组合词'
  535. });
  536. if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
  537. iterations[roundNum * 10 + 3].push(wordId);
  538. });
  539. });
  540. }
  541. // 步骤4: 筛选推荐词进入下轮
  542. const filteredSugs = round.output_q_list?.filter(q => q.from === 'sug') || [];
  543. if (filteredSugs.length > 0) {
  544. const filterStepId = `step_filter_r${roundNum}`;
  545. nodes[filterStepId] = {
  546. type: 'step',
  547. query: `步骤4: 筛选推荐词进入下轮 (${filteredSugs.length}个)`,
  548. level: roundNum * 10 + 1,
  549. relevance_score: 0,
  550. strategy: '筛选推荐词进入下轮',
  551. iteration: roundNum,
  552. is_selected: true
  553. };
  554. edges.push({
  555. from: roundId,
  556. to: filterStepId,
  557. edge_type: 'round_to_step',
  558. strategy: '筛选'
  559. });
  560. iterations[roundNum * 10].push(filterStepId);
  561. // 添加筛选出的sug
  562. filteredSugs.forEach((sug, sugIndex) => {
  563. const sugScore = sug.score || 0;
  564. // 尝试从sug_details中找到这个sug对应的父Q得分
  565. let parentQScore = 0;
  566. if (round.sug_details) {
  567. for (const [qText, sugs] of Object.entries(round.sug_details)) {
  568. const matchingSug = sugs.find(s => s.text === sug.text);
  569. if (matchingSug) {
  570. // 找到父Q的得分
  571. let qData = {};
  572. if (roundNum === 0) {
  573. qData = round.q_list_1?.find(q => q.text === qText) || {};
  574. } else {
  575. qData = round.input_queries?.find(q => q.text === qText) || {};
  576. }
  577. parentQScore = qData.score || 0;
  578. break;
  579. }
  580. }
  581. }
  582. // 比较得分决定颜色:SUG得分 > Q得分 → 绿色,否则 → 红色
  583. const scoreColor = (sugScore >= parentQScore + REQUIRED_SCORE_GAIN) ? '#22c55e' : '#ef4444';
  584. const sugId = `filtered_sug_${sug.text}_r${roundNum}_${sugIndex}`;
  585. nodes[sugId] = {
  586. type: 'filtered_sug',
  587. query: '[SUG] ' + sug.text,
  588. level: roundNum * 10 + 2,
  589. relevance_score: sugScore,
  590. strategy: '进入下轮',
  591. iteration: roundNum,
  592. is_selected: true,
  593. scoreColor: scoreColor, // 新增:用于控制文字颜色
  594. parentQScore: parentQScore // 新增:保存父Q得分用于调试
  595. };
  596. edges.push({
  597. from: filterStepId,
  598. to: sugId,
  599. edge_type: 'step_to_filtered_sug',
  600. strategy: '进入下轮'
  601. });
  602. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  603. iterations[roundNum * 10 + 2].push(sugId);
  604. });
  605. }
  606. // 步骤4: 构建下一轮(Round 1+)
  607. const highScoreCombinations = round.high_score_combinations || [];
  608. const highGainSugs = round.high_gain_sugs || [];
  609. const nextRoundItems = [...highScoreCombinations, ...highGainSugs];
  610. if (nextRoundItems.length > 0) {
  611. const nextRoundStepId = `step_next_round_r${roundNum}`;
  612. nodes[nextRoundStepId] = {
  613. type: 'step',
  614. query: `步骤4: 构建下一轮 (${nextRoundItems.length}个查询)`,
  615. level: roundNum * 10 + 1,
  616. relevance_score: 0,
  617. strategy: '构建下一轮',
  618. iteration: roundNum,
  619. is_selected: true
  620. };
  621. edges.push({
  622. from: roundId,
  623. to: nextRoundStepId,
  624. edge_type: 'round_to_step',
  625. strategy: '构建下一轮'
  626. });
  627. iterations[roundNum * 10].push(nextRoundStepId);
  628. // 创建查询节点
  629. nextRoundItems.forEach((item, index) => {
  630. const itemId = `next_round_${item.text}_r${roundNum}_${index}`;
  631. const isSugItem = item.type === 'sug';
  632. nodes[itemId] = {
  633. type: 'next_round_item',
  634. query: '[Q] ' + item.text,
  635. level: roundNum * 10 + 2,
  636. relevance_score: item.score || 0,
  637. strategy: item.type === 'combination' ? '域内组合' : '高增益SUG',
  638. iteration: roundNum,
  639. is_selected: true,
  640. type_label: item.type_label || '',
  641. item_type: item.type,
  642. is_suggestion: isSugItem,
  643. suggestion_label: isSugItem ? '[suggestion]' : ''
  644. };
  645. edges.push({
  646. from: nextRoundStepId,
  647. to: itemId,
  648. edge_type: 'step_to_next_round',
  649. strategy: item.type === 'combination' ? '域内组合' : 'SUG'
  650. });
  651. if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
  652. iterations[roundNum * 10 + 2].push(itemId);
  653. });
  654. }
  655. }
  656. });
  657. return {
  658. nodes,
  659. edges,
  660. iterations
  661. };
  662. }
  663. /**
  664. * 简化版转换:专注于query和post的演化
  665. * - 合并所有query节点(不区分seg/sug/add_word)
  666. * - 合并相同的帖子节点
  667. * - 步骤信息放在边上
  668. * - 隐藏Round/Step节点
  669. */
  670. function convertV8ToGraphSimplified(runContext, searchResults, extractionData) {
  671. const mergedNodes = {};
  672. const edges = [];
  673. const iterations = {};
  674. extractionData = extractionData || {}; // 默认为空对象
  675. const o = runContext.o || '原始问题';
  676. const rounds = runContext.rounds || [];
  677. // 添加原始问题根节点
  678. const rootId = 'root_o';
  679. mergedNodes[rootId] = {
  680. type: 'root',
  681. query: o,
  682. level: 0,
  683. relevance_score: 1.0,
  684. strategy: '原始问题',
  685. iteration: 0,
  686. is_selected: true,
  687. occurrences: [{round: 0, role: 'root', score: 1.0}]
  688. };
  689. iterations[0] = [rootId];
  690. // 用于记录节点之间的演化关系
  691. const queryEvolution = {}; // {text: {occurrences: [], parentTexts: [], childTexts: []}}
  692. const postMap = {}; // {note_id: {...}}
  693. // 第一遍:收集所有query和post
  694. rounds.forEach((round, roundIndex) => {
  695. const roundNum = round.round_num || roundIndex;
  696. if (round.type === 'initialization') {
  697. // Round 0: 收集分词结果
  698. (round.q_list_1 || []).forEach(q => {
  699. if (!queryEvolution[q.text]) {
  700. queryEvolution[q.text] = {
  701. occurrences: [],
  702. parentTexts: new Set([o]), // 来自原始问题
  703. childTexts: new Set()
  704. };
  705. }
  706. queryEvolution[q.text].occurrences.push({
  707. round: roundNum,
  708. role: 'segmentation',
  709. strategy: '分词',
  710. score: q.score,
  711. reason: q.reason,
  712. type_label: q.type_label || q.typeLabel || ''
  713. });
  714. });
  715. } else {
  716. // Round 1+
  717. // 收集sug_details (推荐词)
  718. Object.entries(round.sug_details || {}).forEach(([parentText, sugs]) => {
  719. sugs.forEach(sug => {
  720. if (!queryEvolution[sug.text]) {
  721. queryEvolution[sug.text] = {
  722. occurrences: [],
  723. parentTexts: new Set(),
  724. childTexts: new Set()
  725. };
  726. }
  727. queryEvolution[sug.text].occurrences.push({
  728. round: roundNum,
  729. role: 'sug',
  730. strategy: '调用sug',
  731. score: sug.score,
  732. reason: sug.reason,
  733. type_label: sug.type_label || sug.typeLabel || ''
  734. });
  735. queryEvolution[sug.text].parentTexts.add(parentText);
  736. if (queryEvolution[parentText]) {
  737. queryEvolution[parentText].childTexts.add(sug.text);
  738. }
  739. });
  740. });
  741. // 收集add_word_details (加词结果)
  742. Object.entries(round.add_word_details || {}).forEach(([seedText, words]) => {
  743. words.forEach(word => {
  744. if (!queryEvolution[word.text]) {
  745. queryEvolution[word.text] = {
  746. occurrences: [],
  747. parentTexts: new Set(),
  748. childTexts: new Set()
  749. };
  750. }
  751. queryEvolution[word.text].occurrences.push({
  752. round: roundNum,
  753. role: 'add_word',
  754. strategy: '加词',
  755. score: word.score,
  756. reason: word.reason,
  757. selectedWord: word.selected_word,
  758. seedScore: word.seed_score, // 添加原始种子的得分
  759. type_label: word.type_label || word.typeLabel || ''
  760. });
  761. queryEvolution[word.text].parentTexts.add(seedText);
  762. if (queryEvolution[seedText]) {
  763. queryEvolution[seedText].childTexts.add(word.text);
  764. }
  765. });
  766. });
  767. // 收集搜索结果和帖子
  768. const roundSearchResults = round.search_results || searchResults;
  769. if (roundSearchResults && Array.isArray(roundSearchResults)) {
  770. roundSearchResults.forEach(search => {
  771. const searchText = search.text;
  772. // 标记这个query被用于搜索
  773. if (queryEvolution[searchText]) {
  774. queryEvolution[searchText].occurrences.push({
  775. round: roundNum,
  776. role: 'search',
  777. strategy: '执行搜索',
  778. score: search.score_with_o,
  779. postCount: search.post_list ? search.post_list.length : 0
  780. });
  781. }
  782. // 收集帖子
  783. if (search.post_list && search.post_list.length > 0) {
  784. search.post_list.forEach(post => {
  785. if (!postMap[post.note_id]) {
  786. postMap[post.note_id] = {
  787. ...post,
  788. foundByQueries: new Set(),
  789. foundInRounds: new Set()
  790. };
  791. }
  792. postMap[post.note_id].foundByQueries.add(searchText);
  793. postMap[post.note_id].foundInRounds.add(roundNum);
  794. // 建立query到post的关系
  795. if (!queryEvolution[searchText].posts) {
  796. queryEvolution[searchText].posts = new Set();
  797. }
  798. queryEvolution[searchText].posts.add(post.note_id);
  799. });
  800. }
  801. });
  802. }
  803. }
  804. });
  805. // 第二遍:创建合并后的节点
  806. Object.entries(queryEvolution).forEach(([text, data]) => {
  807. const nodeId = `query_${text}`;
  808. // 获取最新的分数
  809. const latestOccurrence = data.occurrences[data.occurrences.length - 1] || {};
  810. const hasSearchResults = data.posts && data.posts.size > 0;
  811. mergedNodes[nodeId] = {
  812. type: 'query',
  813. query: '[Q] ' + text,
  814. level: Math.max(...data.occurrences.map(o => o.round), 0) * 10 + 2,
  815. relevance_score: latestOccurrence.score || 0,
  816. evaluationReason: latestOccurrence.reason || '',
  817. strategy: data.occurrences.map(o => o.strategy).join(' + '),
  818. primaryStrategy: latestOccurrence.strategy || '未知', // 添加主要策略字段
  819. iteration: Math.max(...data.occurrences.map(o => o.round), 0),
  820. is_selected: true,
  821. occurrences: data.occurrences,
  822. hasSearchResults: hasSearchResults,
  823. postCount: data.posts ? data.posts.size : 0,
  824. selectedWord: data.occurrences.find(o => o.selectedWord)?.selectedWord || '',
  825. seed_score: data.occurrences.find(o => o.seedScore)?.seedScore, // 添加原始种子的得分
  826. type_label: latestOccurrence.type_label || '' // 使用最新的 type_label
  827. };
  828. // 添加到对应的轮次
  829. const maxRound = Math.max(...data.occurrences.map(o => o.round), 0);
  830. const iterKey = maxRound * 10 + 2;
  831. if (!iterations[iterKey]) iterations[iterKey] = [];
  832. iterations[iterKey].push(nodeId);
  833. });
  834. // 创建帖子节点
  835. Object.entries(postMap).forEach(([noteId, post]) => {
  836. const postId = `post_${noteId}`;
  837. const imageList = (post.images || []).map(url => ({
  838. image_url: url
  839. }));
  840. mergedNodes[postId] = {
  841. type: 'post',
  842. query: '[R] ' + post.title,
  843. level: 100, // 放在最后
  844. relevance_score: 0,
  845. strategy: '帖子',
  846. iteration: Math.max(...Array.from(post.foundInRounds)),
  847. is_selected: true,
  848. note_id: post.note_id,
  849. note_url: post.note_url,
  850. body_text: post.body_text || '',
  851. images: post.images || [],
  852. image_list: imageList,
  853. interact_info: post.interact_info || {},
  854. foundByQueries: Array.from(post.foundByQueries),
  855. foundInRounds: Array.from(post.foundInRounds),
  856. // 附加多模态提取数据
  857. extraction: extractionData && extractionData[post.note_id] ? extractionData[post.note_id] : null
  858. };
  859. if (!iterations[100]) iterations[100] = [];
  860. iterations[100].push(postId);
  861. // 如果有提取数据,创建对应的分析节点
  862. if (extractionData && extractionData[post.note_id]) {
  863. const analysisId = `analysis_${noteId}`;
  864. mergedNodes[analysisId] = {
  865. type: 'analysis',
  866. query: '[AI分析] ' + post.title,
  867. level: 101,
  868. relevance_score: 0,
  869. strategy: 'AI分析',
  870. iteration: Math.max(...Array.from(post.foundInRounds)),
  871. is_selected: true,
  872. note_id: post.note_id,
  873. note_url: post.note_url,
  874. title: post.title,
  875. body_text: post.body_text || '',
  876. interact_info: post.interact_info || {},
  877. extraction: extractionData[post.note_id],
  878. image_list: imageList
  879. };
  880. edges.push({
  881. from: postId,
  882. to: analysisId,
  883. edge_type: 'post_to_analysis',
  884. strategy: 'AI分析',
  885. label: 'AI分析',
  886. round: Math.max(...Array.from(post.foundInRounds))
  887. });
  888. if (!iterations[101]) iterations[101] = [];
  889. iterations[101].push(analysisId);
  890. }
  891. });
  892. // 第三遍:创建边
  893. // 1. 原始问题 -> 分词结果
  894. Object.entries(queryEvolution).forEach(([text, data]) => {
  895. const nodeId = `query_${text}`;
  896. const segOccurrence = data.occurrences.find(o => o.role === 'segmentation');
  897. if (segOccurrence && data.parentTexts.has(o)) {
  898. edges.push({
  899. from: rootId,
  900. to: nodeId,
  901. edge_type: 'segmentation',
  902. strategy: '分词',
  903. label: '分词',
  904. round: 0
  905. });
  906. }
  907. });
  908. // 2. Query演化关系
  909. Object.entries(queryEvolution).forEach(([text, data]) => {
  910. const nodeId = `query_${text}`;
  911. data.parentTexts.forEach(parentText => {
  912. if (parentText === o) return; // 跳过原始问题(已处理)
  913. const parentNodeId = `query_${parentText}`;
  914. if (!mergedNodes[parentNodeId]) return;
  915. // 找到这个演化的策略和轮次
  916. const occurrence = data.occurrences.find(o =>
  917. o.role === 'sug' || o.role === 'add_word'
  918. );
  919. edges.push({
  920. from: parentNodeId,
  921. to: nodeId,
  922. edge_type: occurrence?.role || 'evolution',
  923. strategy: occurrence?.strategy || '演化',
  924. label: `${occurrence?.strategy || '演化'} (R${occurrence?.round || 0})`,
  925. round: occurrence?.round || 0
  926. });
  927. });
  928. });
  929. // 3. Query -> Post (搜索关系)
  930. Object.entries(queryEvolution).forEach(([text, data]) => {
  931. const nodeId = `query_${text}`;
  932. if (data.posts && data.posts.size > 0) {
  933. const searchOccurrence = data.occurrences.find(o => o.role === 'search');
  934. data.posts.forEach(noteId => {
  935. const postId = `post_${noteId}`;
  936. edges.push({
  937. from: nodeId,
  938. to: postId,
  939. edge_type: 'search',
  940. strategy: '搜索',
  941. label: `搜索 (${data.posts.size}个帖子)`,
  942. round: searchOccurrence?.round || 0
  943. });
  944. });
  945. }
  946. });
  947. return {
  948. nodes: mergedNodes,
  949. edges,
  950. iterations
  951. };
  952. }
  953. module.exports = { convertV8ToGraphV2, convertV8ToGraphSimplified };