convert_v8_to_graph_v3.js 37 KB

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