convert_v8_to_graph_v3.js 39 KB

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