outlineGenerationTask.cjs 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  1. function formatSuggestions(suggestions) {
  2. if (!suggestions?.length) return '';
  3. return `\n\n本轮修正建议:\n${suggestions.map((item, index) => `${index + 1}. ${item}`).join('\n')}`;
  4. }
  5. const KNOWLEDGE_RESUME_MAX_CHARS = 220;
  6. const MAX_KNOWLEDGE_ADDITIONS = 30;
  7. function truncateText(value, maxLength) {
  8. const text = String(value || '').replace(/\s+/g, ' ').trim();
  9. return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
  10. }
  11. function renderKnowledgeItemsForPrompt(items) {
  12. if (!items?.length) return '';
  13. return items.map((item, index) => [
  14. `${index + 1}. title: ${item.title}`,
  15. ` resume: ${truncateText(item.resume, KNOWLEDGE_RESUME_MAX_CHARS)}`,
  16. ].join('\n')).join('\n');
  17. }
  18. function collectKnowledgeAdditionParents(items) {
  19. const parents = [];
  20. function visit(nodes, level = 1, ancestors = []) {
  21. (nodes || []).forEach((item) => {
  22. const id = String(item?.id || '').trim();
  23. const title = String(item?.title || '').trim();
  24. if (id && level === 2) {
  25. parents.push({
  26. id,
  27. title,
  28. parentTitle: ancestors[0]?.title || '',
  29. childTitles: (item.children || []).map((child) => String(child?.title || '').trim()).filter(Boolean),
  30. });
  31. }
  32. if (item?.children?.length) visit(item.children, level + 1, [...ancestors, { id, title }]);
  33. });
  34. }
  35. visit(items || []);
  36. return parents;
  37. }
  38. function formatKnowledgeAdditionParents(parents) {
  39. return (parents || []).map((item) => [
  40. `- ${item.id} ${item.title || '未命名二级目录'}(所属一级:${item.parentTitle || '未命名一级目录'})`,
  41. ` 已有三级目录:${item.childTitles.length ? item.childTitles.join(';') : '无'}`,
  42. ].join('\n')).join('\n');
  43. }
  44. function normalizeReferenceDocumentIds(payload) {
  45. return Array.isArray(payload?.reference_knowledge_document_ids)
  46. ? [...new Set(payload.reference_knowledge_document_ids.map((id) => String(id || '').trim()).filter(Boolean))]
  47. : [];
  48. }
  49. function loadOutlineKnowledgeItems(knowledgeBaseService, documentIds, log) {
  50. if (!documentIds.length) return [];
  51. if (!knowledgeBaseService?.getOutlineReferences) {
  52. log('未找到知识库读取服务,跳过参考知识库。', 6);
  53. return [];
  54. }
  55. try {
  56. log(`正在读取 ${documentIds.length} 个参考知识库文档。`, 6);
  57. const result = knowledgeBaseService.getOutlineReferences(documentIds);
  58. const items = Array.isArray(result?.items) ? result.items : [];
  59. log(items.length ? `已读取 ${items.length} 条轻量知识条目。` : '未读取到可用知识库条目,将按普通目录生成。', 7);
  60. return items;
  61. } catch (error) {
  62. log(`读取参考知识库失败,将按普通目录生成:${error.message || String(error)}`, 7);
  63. return [];
  64. }
  65. }
  66. function outlineSystemPrompt() {
  67. return `你是一个专业的标书编写专家。根据提供的项目概述和技术评分要求,生成投标文件中技术标部分的目录结构。
  68. 如果用户提供了自己编写的目录,你要保证目录满足技术评分要求,并充分结合用户自己编写的目录。
  69. 要求:
  70. 1. 目录结构要全面覆盖技术标的所有必要章节
  71. 2. 章节名称要专业、准确,符合投标文件规范
  72. 3. 一级目录名称要与技术评分要求中的章节名称一致;如果技术评分要求中没有明确章节名称,则结合内容总结一级目录名称
  73. 4. 一共包括三级目录
  74. 5. 返回标准 JSON 格式,包含章节编号、标题、描述和子章节
  75. 6. 除了 JSON 结果外,不要输出任何其他内容
  76. JSON 格式要求:
  77. {
  78. "outline": [
  79. {
  80. "id": "1",
  81. "title": "",
  82. "description": "",
  83. "children": [
  84. {
  85. "id": "1.1",
  86. "title": "",
  87. "description": "",
  88. "children": [
  89. {
  90. "id": "1.1.1",
  91. "title": "",
  92. "description": ""
  93. }
  94. ]
  95. }
  96. ]
  97. }
  98. ]
  99. }`;
  100. }
  101. function topLevelOutlineSystemPrompt() {
  102. return `你是一个专业的标书编写专家。根据提供的项目概述和技术评分要求,生成投标文件中技术标部分的一级目录结构。
  103. 如果用户提供了自己编写的目录,你要保证一级目录满足技术评分要求,并充分结合用户自己编写的目录。
  104. 要求:
  105. 1. 只生成一级目录,不要生成二级和三级目录
  106. 2. 一级目录名称要专业、准确,符合投标文件规范
  107. 3. 一级目录名称要尽量与技术评分要求中的章节名称一致;如果技术评分要求中没有明确章节名称,则结合内容总结一级目录名称
  108. 4. 返回标准 JSON 格式,使用 outline 字段,每个一级目录必须包含 id、title、description
  109. 5. 除了 JSON 结果外,不要输出任何其他内容
  110. JSON 格式要求:
  111. {
  112. "outline": [
  113. {
  114. "id": "1",
  115. "title": "",
  116. "description": ""
  117. }
  118. ]
  119. }`;
  120. }
  121. function generateOutlineMessages({ overview, requirements, suggestions }) {
  122. return [
  123. { role: 'system', content: outlineSystemPrompt() },
  124. { role: 'user', content: `项目概述:\n${overview}` },
  125. { role: 'user', content: `技术评分要求:\n${requirements}` },
  126. { role: 'user', content: `请生成完整的技术标目录结构,确保覆盖所有技术评分要点。${formatSuggestions(suggestions)}` },
  127. ];
  128. }
  129. function generateTopLevelOutlineMessages({ overview, requirements, suggestions }) {
  130. return [
  131. { role: 'system', content: topLevelOutlineSystemPrompt() },
  132. { role: 'user', content: `项目概述:\n${overview}` },
  133. { role: 'user', content: `技术评分要求:\n${requirements}` },
  134. { role: 'user', content: `请仅生成一级目录列表,不要生成二级和三级目录。返回的 JSON 仍然使用 outline 字段,每个一级目录都必须包含 id、title、description。${formatSuggestions(suggestions)}` },
  135. ];
  136. }
  137. function extractRequirementGroupsMessages(requirements, suggestions) {
  138. const systemPrompt = `你是一个专业的招标文件分析专家。请从技术评分要求中提取适合作为技术标一级目录的评分大类。
  139. 要求:
  140. 1. 只提取技术评分大类,不要提取商务、报价、资质、售后服务等非技术类条目
  141. 2. 每个大类都必须适合作为技术标一级目录标题,标题要专业、简洁、完整
  142. 3. 同一大类下的细项、子项、分值说明、评分标准要归入 detail_points,不要拆成多个一级目录
  143. 4. requirement_id 必须唯一,使用 R1、R2、R3 这种格式
  144. 5. description 需要概括该大类关注的核心内容
  145. 6. detail_points 中保留该大类下的关键评分细项,使用简洁短句
  146. 7. 只返回 JSON,格式必须为 {"groups": [...]},不要输出任何其他内容
  147. JSON 格式要求:
  148. {
  149. "groups": [
  150. {
  151. "requirement_id": "R1",
  152. "title": "",
  153. "description": "",
  154. "detail_points": ["", ""]
  155. }
  156. ]
  157. }`;
  158. return [
  159. { role: 'system', content: systemPrompt },
  160. { role: 'user', content: `技术评分要求:\n${requirements}` },
  161. { role: 'user', content: `请提取所有适合作为技术标一级目录的技术评分大类,保持顺序稳定,并把每个大类下的评分细项归入 detail_points。${formatSuggestions(suggestions)}` },
  162. ];
  163. }
  164. function generateAlignedChildrenMessages({ overview, requirements, parentItem, group, suggestions }) {
  165. const detailLines = (group.detail_points || [])
  166. .filter((item) => typeof item === 'string' && item.trim())
  167. .map((item) => `- ${item}`)
  168. .join('\n');
  169. const detailContent = detailLines || '- 未提供明确细项,请根据评分大类描述合理展开';
  170. const systemPrompt = `你是一个专业的标书编写专家。请围绕指定的技术评分大类,为已经固定好的一级目录生成二级和三级目录。
  171. 要求:
  172. 1. 一级目录标题和顺序已经固定,不能修改、重命名、合并或删除一级目录
  173. 2. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身
  174. 3. 二级和三级目录要覆盖当前技术评分大类及其细项,不能越界写入其他评分大类内容
  175. 4. 返回标准 JSON,格式为 {"children": [...]},children 中只能包含当前一级目录的直接子目录
  176. 5. 每个节点必须包含 id、title、description,三级目录继续使用 children 字段
  177. 6. 章节编号必须以给定的一级目录编号为前缀,例如父级是 2,则二级目录编号从 2.1 开始,三级目录编号从 2.1.1 开始
  178. 7. 除了 JSON 结果外,不要输出任何其他内容`;
  179. const messages = [
  180. { role: 'system', content: systemPrompt },
  181. { role: 'user', content: `项目概述:\n${overview}` },
  182. { role: 'user', content: `技术评分要求原文:\n${requirements}` },
  183. { role: 'user', content: `当前固定一级目录:\n编号:${parentItem.id}\n标题:${parentItem.title}\n描述:${parentItem.description || ''}` },
  184. { role: 'user', content: `当前对应的技术评分大类:\nrequirement_id:${group.requirement_id}\n标题:${group.title}\n描述:${group.description}\n细项:\n${detailContent}` },
  185. ];
  186. messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,一级目录标题必须保持为当前给定标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
  187. return messages;
  188. }
  189. function generateChildrenMessages({ overview, requirements, parentItem, suggestions }) {
  190. const systemPrompt = `你是一个专业的标书编写专家。请围绕指定的一级目录,生成其下属的二级目录和三级目录。
  191. 要求:
  192. 1. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身
  193. 2. 返回标准 JSON,格式为 {"children": [...]}
  194. 3. children 中只能包含当前一级目录的直接子目录,每个节点必须包含 id、title、description
  195. 4. 二级目录下如有三级目录,同样使用 children 字段
  196. 5. 章节编号必须以给定的一级目录编号为前缀,例如父级是 2,则二级目录编号从 2.1 开始,三级目录编号从 2.1.1 开始
  197. 6. 除了 JSON 结果外,不要输出任何其他内容`;
  198. const messages = [
  199. { role: 'system', content: systemPrompt },
  200. { role: 'user', content: `项目概述:\n${overview}` },
  201. { role: 'user', content: `技术评分要求:\n${requirements}` },
  202. { role: 'user', content: `当前一级目录:\n编号:${parentItem.id}\n标题:${parentItem.title}\n描述:${parentItem.description || ''}` },
  203. ];
  204. messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
  205. return messages;
  206. }
  207. function reviewOutlineMessages({ overview, requirements, outline }) {
  208. const systemPrompt = `你是一个严格的招标文件目录审核专家。请审核目录是否符合项目概述和技术评分要求。
  209. 要求:
  210. 1. 重点检查目录是否完整覆盖技术评分要点
  211. 2. 检查一级目录名称是否专业、准确,是否尽量与评分项原文保持一致
  212. 3. 检查目录层级是否清晰,是否达到三级目录要求,是否存在明显遗漏、错位、重复或不合理章节
  213. 4. 只返回 JSON,格式为:{"passed": true, "suggestions": []}
  214. 5. 若不通过,suggestions 中必须给出具体、可执行的修改建议
  215. 6. 除了 JSON 外,不要输出任何其他内容`;
  216. return [
  217. { role: 'system', content: systemPrompt },
  218. { role: 'user', content: `项目概述:\n${overview}` },
  219. { role: 'user', content: `技术评分要求:\n${requirements}` },
  220. { role: 'user', content: `待审核目录 JSON:\n${JSON.stringify(outline)}` },
  221. { role: 'user', content: '请判断该目录是否满足要求。若满足则返回 passed=true;若不满足则返回 passed=false,并给出具体修改建议。' },
  222. ];
  223. }
  224. function reviewAlignedOutlineMessages({ overview, requirements, groups, outline }) {
  225. const systemPrompt = `你是一个严格的招标文件目录审核专家。请审核目录是否与技术评分大类一一对应,并判断二三级目录是否覆盖各评分大类的细项。
  226. 要求:
  227. 1. 一级目录必须与提供的技术评分大类一一对应,数量一致、顺序一致、标题必须完全一致
  228. 2. 不允许缺失技术评分大类,也不允许新增、合并、改写一级目录
  229. 3. 二级和三级目录要围绕各自对应的技术评分大类与细项展开,避免错位、遗漏和明显重复
  230. 4. 检查完整目录是否层级清晰,整体是否达到三级目录要求
  231. 5. 只返回 JSON,格式为:{"passed": true, "suggestions": []}
  232. 6. 若不通过,suggestions 中必须给出具体、可执行的修改建议,重点说明哪个评分大类覆盖不足或结构不合理
  233. 7. 除了 JSON 外,不要输出任何其他内容`;
  234. return [
  235. { role: 'system', content: systemPrompt },
  236. { role: 'user', content: `项目概述:\n${overview}` },
  237. { role: 'user', content: `技术评分要求:\n${requirements}` },
  238. { role: 'user', content: `技术评分大类 JSON:\n${JSON.stringify({ groups })}` },
  239. { role: 'user', content: `待审核目录 JSON:\n${JSON.stringify(outline)}` },
  240. { role: 'user', content: '请判断该目录是否满足一一对应要求。若满足则返回 passed=true;若不满足则返回 passed=false,并给出具体修改建议。' },
  241. ];
  242. }
  243. function generateKnowledgeAdditionMessages({ overview, requirements, outline, knowledgeItems }) {
  244. const additionParents = collectKnowledgeAdditionParents(outline.outline || []);
  245. const sampleParent = additionParents[0]?.id || '';
  246. const instructionPrompt = `你是一个严格的标书目录补充专家。你只能根据参考知识库判断现有二级目录下是否缺少三级目录,并只输出新增三级目录。
  247. 要求:
  248. 1. 已有一级目录和二级目录都已经固定,不允许新增、删除、重命名、合并或调整顺序
  249. 2. 只能新增三级目录,parent_id 必须逐字复制“可补充二级目录 parent_id”中的某一个 ID
  250. 3. 不允许输出 bindings、knowledge_item_ids、id、children、outline 或完整目录
  251. 4. 不要把知识库条目绑定到目录;知识库只作为判断是否缺少三级目录的参考材料
  252. 5. 只补充与招标项目、评分项、现有二级目录主题强相关且当前三级目录确实缺失的内容
  253. 6. 不要重复已有三级目录,也不要输出同义重复目录
  254. 7. 如果没有确实需要补充的三级目录,返回空 additions 数组
  255. 8. 只返回 JSON,不要输出解释文字
  256. 返回格式:
  257. {
  258. "additions": [
  259. { "parent_id": "${sampleParent}", "title": "新增三级目录标题", "description": "新增三级目录说明" }
  260. ]
  261. }`;
  262. return [
  263. { role: 'user', content: instructionPrompt },
  264. { role: 'user', content: `项目概述:\n${overview}` },
  265. { role: 'user', content: `技术评分要求:\n${requirements}` },
  266. { role: 'user', content: `当前完整目录 JSON:\n${JSON.stringify(outline, null, 2)}` },
  267. { role: 'user', content: `可补充二级目录 parent_id(只能逐字复制以下 ID,并在其下新增三级目录):\n${formatKnowledgeAdditionParents(additionParents)}` },
  268. { role: 'user', content: `参考知识库轻量条目如下。注意:这些只是参考资料,不要输出知识库 ID,也不要绑定知识库条目。\n${renderKnowledgeItemsForPrompt(knowledgeItems)}` },
  269. { role: 'user', content: '请只返回知识库补充三级目录 JSON:additions。每条 additions 只能包含 parent_id、title、description。' },
  270. ];
  271. }
  272. function generateKnowledgeAdditionRepairMessages({ invalidContent, issues }, additionParents) {
  273. return [
  274. {
  275. role: 'user',
  276. content: `你是一个严格的 JSON 修复器。请把模型输出修复为“知识库补充三级目录”JSON。
  277. 必须满足:
  278. 1. 顶层只能有 additions 数组
  279. 2. 每条 additions 只能有 parent_id、title、description
  280. 3. parent_id 必须逐字复制允许的二级目录 ID
  281. 4. 禁止输出 bindings、knowledge_item_ids、id、children、outline 或完整目录
  282. 5. 如果没有可补充三级目录,返回 {"additions":[]}
  283. 6. 只返回 JSON,不要输出解释文字
  284. 允许的二级目录 parent_id:
  285. ${formatKnowledgeAdditionParents(additionParents)}`,
  286. },
  287. { role: 'user', content: `错误列表:\n${issues}` },
  288. { role: 'user', content: `待修复内容:\n\`\`\`json\n${String(invalidContent || '').slice(0, 60000)}\n\`\`\`` },
  289. ];
  290. }
  291. function requireObject(value, label) {
  292. if (!value || typeof value !== 'object' || Array.isArray(value)) {
  293. throw new Error(`${label} 必须是对象`);
  294. }
  295. return value;
  296. }
  297. function requireArray(value, label) {
  298. if (!Array.isArray(value)) {
  299. throw new Error(`${label} 必须是数组`);
  300. }
  301. return value;
  302. }
  303. function requireField(value, label) {
  304. if (value === undefined || value === null) {
  305. throw new Error(`${label} 缺失`);
  306. }
  307. return String(value);
  308. }
  309. function normalizeKnowledgeItemIds(value, allowedKnowledgeIds) {
  310. if (!Array.isArray(value)) {
  311. return [];
  312. }
  313. const ids = value.map((id) => String(id || '').trim()).filter(Boolean);
  314. if (allowedKnowledgeIds instanceof Set) {
  315. return [...new Set(ids.filter((id) => allowedKnowledgeIds.has(id)))];
  316. }
  317. return [...new Set(ids)];
  318. }
  319. function normalizeOutlineItem(item, path = 'outline[]', allowedKnowledgeIds) {
  320. const raw = requireObject(item, path);
  321. const normalized = {
  322. id: requireField(raw.id, `${path}.id`),
  323. title: requireField(raw.title, `${path}.title`),
  324. description: requireField(raw.description, `${path}.description`),
  325. };
  326. if (raw.source_requirement_id !== undefined && raw.source_requirement_id !== null) {
  327. normalized.source_requirement_id = String(raw.source_requirement_id);
  328. }
  329. if (raw.source_requirement_title !== undefined && raw.source_requirement_title !== null) {
  330. normalized.source_requirement_title = String(raw.source_requirement_title);
  331. }
  332. if (raw.content !== undefined && raw.content !== null) {
  333. normalized.content = String(raw.content);
  334. }
  335. const knowledgeItemIds = normalizeKnowledgeItemIds(raw.knowledge_item_ids, allowedKnowledgeIds);
  336. if (knowledgeItemIds.length) {
  337. normalized.knowledge_item_ids = knowledgeItemIds;
  338. }
  339. if (raw.children !== undefined && raw.children !== null) {
  340. const children = requireArray(raw.children, `${path}.children`);
  341. if (children.length) {
  342. normalized.children = children.map((child, index) => normalizeOutlineItem(child, `${path}.children[${index}]`, allowedKnowledgeIds));
  343. }
  344. }
  345. return normalized;
  346. }
  347. function normalizeOutlineResponse(payload, allowedKnowledgeIds) {
  348. const raw = requireObject(payload, 'OutlineResponse');
  349. const outline = requireArray(raw.outline, 'outline');
  350. return { outline: outline.map((item, index) => normalizeOutlineItem(item, `outline[${index}]`, allowedKnowledgeIds)) };
  351. }
  352. function normalizeChildrenResponse(payload, allowedKnowledgeIds) {
  353. const raw = requireObject(payload, 'OutlineChildrenResponse');
  354. const children = requireArray(raw.children, 'children');
  355. return { children: children.map((item, index) => normalizeOutlineItem(item, `children[${index}]`, allowedKnowledgeIds)) };
  356. }
  357. function normalizeReviewResponse(payload) {
  358. const raw = requireObject(payload, 'OutlineReviewResponse');
  359. let passed = raw.passed;
  360. if (typeof passed === 'string') {
  361. passed = passed.toLowerCase() === 'true';
  362. }
  363. if (typeof passed !== 'boolean') {
  364. throw new Error('passed 必须是布尔值');
  365. }
  366. const suggestions = raw.suggestions === undefined || raw.suggestions === null
  367. ? []
  368. : requireArray(raw.suggestions, 'suggestions').map((item) => String(item));
  369. return { passed, suggestions };
  370. }
  371. function normalizeRequirementGroupsResponse(payload) {
  372. const raw = requireObject(payload, 'TechnicalRequirementGroupResponse');
  373. const groups = requireArray(raw.groups, 'groups').map((group, index) => {
  374. const item = requireObject(group, `groups[${index}]`);
  375. return {
  376. requirement_id: requireField(item.requirement_id, `groups[${index}].requirement_id`),
  377. title: requireField(item.title, `groups[${index}].title`),
  378. description: requireField(item.description, `groups[${index}].description`),
  379. detail_points: item.detail_points === undefined || item.detail_points === null
  380. ? []
  381. : requireArray(item.detail_points, `groups[${index}].detail_points`).map((point) => String(point)),
  382. };
  383. });
  384. return { groups };
  385. }
  386. function createOutlineNodeMap(items) {
  387. const map = new Map();
  388. function visit(nodes, level = 1, parent = null) {
  389. (nodes || []).forEach((item) => {
  390. const id = String(item?.id || '').trim();
  391. if (id) {
  392. map.set(id, { item, level, parent });
  393. }
  394. if (item?.children?.length) {
  395. visit(item.children, level + 1, item);
  396. }
  397. });
  398. }
  399. visit(items || []);
  400. return map;
  401. }
  402. function normalizeTitleKey(value) {
  403. return String(value || '').replace(/\s+/g, '').toLowerCase();
  404. }
  405. function countNestedArrayEntries(value, fieldName) {
  406. if (!value || typeof value !== 'object') return 0;
  407. if (Array.isArray(value)) {
  408. return value.reduce((sum, item) => sum + countNestedArrayEntries(item, fieldName), 0);
  409. }
  410. return Object.entries(value).reduce((sum, [key, child]) => {
  411. const current = key === fieldName && Array.isArray(child) ? child.length : 0;
  412. return sum + current + countNestedArrayEntries(child, fieldName);
  413. }, 0);
  414. }
  415. function summarizeRawKnowledgeAdditions(payload) {
  416. const raw = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
  417. return {
  418. additions: Array.isArray(payload) ? payload.length : (Array.isArray(raw.additions) ? raw.additions.length : 0),
  419. bindings: Array.isArray(raw.bindings) ? raw.bindings.length : 0,
  420. knowledge_refs: countNestedArrayEntries(payload, 'knowledge_item_ids'),
  421. children: countNestedArrayEntries(payload, 'children'),
  422. };
  423. }
  424. function formatAdditionSummary(summary) {
  425. return `additions=${summary.additions},bindings=${summary.bindings},knowledge_refs=${summary.knowledge_refs},children=${summary.children}`;
  426. }
  427. function getKnowledgeAdditionCandidates(payload) {
  428. if (Array.isArray(payload)) return payload;
  429. const raw = requireObject(payload, 'KnowledgeAdditionsResponse');
  430. if (raw.additions !== undefined && raw.additions !== null) return requireArray(raw.additions, 'additions');
  431. if (Array.isArray(raw.items)) return raw.items;
  432. if (Array.isArray(raw.directories)) return raw.directories;
  433. return [];
  434. }
  435. function createExistingThirdTitleKeys(outlineItems) {
  436. const keys = new Set();
  437. function visit(nodes, level = 1) {
  438. (nodes || []).forEach((item) => {
  439. const id = String(item?.id || '').trim();
  440. if (level === 2 && id) {
  441. (item.children || []).forEach((child) => {
  442. const key = normalizeTitleKey(child?.title);
  443. if (key) keys.add(`${id}::${key}`);
  444. });
  445. }
  446. if (item?.children?.length) visit(item.children, level + 1);
  447. });
  448. }
  449. visit(outlineItems || []);
  450. return keys;
  451. }
  452. function resolveKnowledgeAdditionParent(parentId, context, stats) {
  453. const parentInfo = context.outlineNodeMap.get(parentId);
  454. if (!parentInfo) return null;
  455. if (parentInfo.level === 2) return { parentId, parentInfo };
  456. if (parentInfo.level === 3 && parentInfo.parent?.id) {
  457. const nextParentId = String(parentInfo.parent.id || '').trim();
  458. const nextParentInfo = context.outlineNodeMap.get(nextParentId);
  459. if (nextParentInfo?.level === 2) {
  460. stats.adjustedParent += 1;
  461. return { parentId: nextParentId, parentInfo: nextParentInfo };
  462. }
  463. }
  464. return null;
  465. }
  466. function normalizeKnowledgeAddition(addition, path, context, stats, seenKeys, issues) {
  467. if (!addition || typeof addition !== 'object' || Array.isArray(addition)) {
  468. stats.dropped += 1;
  469. issues.push(`${path} 必须是对象`);
  470. return null;
  471. }
  472. const rawParentId = String(addition.parent_id || '').trim();
  473. if (!rawParentId) {
  474. stats.dropped += 1;
  475. issues.push(`${path}.parent_id 缺失`);
  476. return null;
  477. }
  478. const resolvedParent = resolveKnowledgeAdditionParent(rawParentId, context, stats);
  479. if (!resolvedParent) {
  480. stats.dropped += 1;
  481. issues.push(`${path}.parent_id=${rawParentId} 不是现有二级目录 ID`);
  482. return null;
  483. }
  484. const title = String(addition.title || addition.name || '').trim();
  485. if (!title) {
  486. stats.dropped += 1;
  487. issues.push(`${path}.title 缺失或为空`);
  488. return null;
  489. }
  490. const dedupeKey = `${resolvedParent.parentId}::${normalizeTitleKey(title)}`;
  491. if (seenKeys.has(dedupeKey)) {
  492. stats.dropped += 1;
  493. return null;
  494. }
  495. seenKeys.add(dedupeKey);
  496. stats.retained += 1;
  497. const description = String(addition.description || addition.summary || addition.resume || title).trim() || title;
  498. return { parent_id: resolvedParent.parentId, title, description };
  499. }
  500. function normalizeKnowledgeAdditionsResponse(payload, context) {
  501. const raw = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
  502. const rawSummary = summarizeRawKnowledgeAdditions(payload);
  503. if (context.rawAttempts) context.rawAttempts.push(rawSummary);
  504. const candidates = getKnowledgeAdditionCandidates(payload);
  505. const stats = { retained: 0, dropped: 0, adjustedParent: 0 };
  506. const issues = [];
  507. const seenKeys = createExistingThirdTitleKeys(context.outline || []);
  508. const additions = [];
  509. candidates.forEach((addition, index) => {
  510. if (additions.length >= MAX_KNOWLEDGE_ADDITIONS) {
  511. stats.dropped += 1;
  512. return;
  513. }
  514. const normalized = normalizeKnowledgeAddition(addition, `additions[${index}]`, context, stats, seenKeys, issues);
  515. if (normalized) additions.push(normalized);
  516. });
  517. if (context.normalizationStats) context.normalizationStats.push(stats);
  518. const shouldRepair = !additions.length && (
  519. raw.outline !== undefined
  520. || raw.bindings !== undefined
  521. || raw.knowledge_item_ids !== undefined
  522. || (candidates.length > 0 && issues.length > 0)
  523. );
  524. if (shouldRepair) {
  525. const reason = issues.length ? issues.join(';') : '模型返回了 bindings/outline/knowledge_item_ids,但没有可应用的三级目录 additions';
  526. if (context.debugLog) context.debugLog(`进入修复:${reason}`);
  527. throw new Error(`知识库补充三级目录格式无效:${reason}`);
  528. }
  529. return { additions };
  530. }
  531. function validateKnowledgeAdditionsResponse(payload) {
  532. requireArray(payload.additions, 'additions');
  533. }
  534. function outlineDepth(items) {
  535. return items?.length ? 1 + Math.max(...items.map((item) => outlineDepth(item.children || []))) : 0;
  536. }
  537. function validateCompleteOutline(payload) {
  538. const outline = payload.outline || [];
  539. if (!outline.length) throw new Error('目录不能为空');
  540. if (outlineDepth(outline) < 3) throw new Error('完整目录至少需要三级结构');
  541. }
  542. function validateTopLevelOutline(payload) {
  543. if (!(payload.outline || []).length) throw new Error('一级目录不能为空');
  544. }
  545. function validateChildrenOutline(payload) {
  546. if (!(payload.children || []).length) throw new Error('二级目录不能为空');
  547. }
  548. function validateRequirementGroups(payload) {
  549. const groups = payload.groups || [];
  550. if (!groups.length) throw new Error('技术评分大类不能为空');
  551. const requirementIds = [];
  552. const titles = [];
  553. groups.forEach((group, index) => {
  554. const requirementId = String(group.requirement_id || '').trim();
  555. const title = String(group.title || '').trim();
  556. const description = String(group.description || '').trim();
  557. if (!requirementId) throw new Error(`第 ${index + 1} 个技术评分大类缺少 requirement_id`);
  558. if (!title) throw new Error(`第 ${index + 1} 个技术评分大类缺少标题`);
  559. if (!description) throw new Error(`第 ${index + 1} 个技术评分大类缺少描述`);
  560. requirementIds.push(requirementId);
  561. titles.push(title);
  562. });
  563. if (new Set(requirementIds).size !== requirementIds.length) throw new Error('技术评分大类 requirement_id 不能重复');
  564. if (new Set(titles).size !== titles.length) throw new Error('技术评分大类标题不能重复');
  565. }
  566. function buildTopLevelOutlineFromGroups(groups) {
  567. return groups.map((group, index) => {
  568. const title = String(group.title || '').trim();
  569. return {
  570. id: String(index + 1),
  571. title,
  572. description: String(group.description || title).trim(),
  573. source_requirement_id: String(group.requirement_id || `R${index + 1}`).trim(),
  574. source_requirement_title: title,
  575. };
  576. });
  577. }
  578. function validateAlignedTopLevelMapping(outlineItems, groups) {
  579. if (outlineItems.length !== groups.length) throw new Error('一级目录数量必须与技术评分大类数量一致');
  580. outlineItems.forEach((item, index) => {
  581. const expectedTitle = String(groups[index].title || '').trim();
  582. const actualTitle = String(item.title || '').trim();
  583. if (actualTitle !== expectedTitle) throw new Error(`第 ${index + 1} 个一级目录标题必须严格等于技术评分大类标题:${expectedTitle}`);
  584. const expectedRequirementId = String(groups[index].requirement_id || '').trim();
  585. const actualRequirementId = String(item.source_requirement_id || '').trim();
  586. if (actualRequirementId !== expectedRequirementId) throw new Error(`第 ${index + 1} 个一级目录映射的技术评分大类ID不正确:${expectedRequirementId}`);
  587. });
  588. }
  589. function renumber(items, parent = '') {
  590. return (items || []).map((item, index) => {
  591. const id = parent ? `${parent}.${index + 1}` : `${index + 1}`;
  592. const next = { ...item, id };
  593. if (item.children?.length) next.children = renumber(item.children, id);
  594. else delete next.children;
  595. return next;
  596. });
  597. }
  598. function cloneOutlineItems(items) {
  599. return (items || []).map((item) => ({
  600. ...item,
  601. ...(item.knowledge_item_ids?.length ? { knowledge_item_ids: [...item.knowledge_item_ids] } : {}),
  602. ...(item.children?.length ? { children: cloneOutlineItems(item.children) } : {}),
  603. }));
  604. }
  605. function createOutlineItemFromKnowledgeAddition(addition) {
  606. return {
  607. id: '',
  608. title: addition.title,
  609. description: addition.description,
  610. };
  611. }
  612. function validateTopLevelPreserved(beforeItems, afterItems) {
  613. if ((beforeItems || []).length !== (afterItems || []).length) {
  614. throw new Error('知识库补目录不允许改变一级目录数量');
  615. }
  616. (beforeItems || []).forEach((beforeItem, index) => {
  617. const afterItem = afterItems[index];
  618. if (String(beforeItem.title || '').trim() !== String(afterItem?.title || '').trim()) {
  619. throw new Error('知识库补目录不允许修改一级目录标题');
  620. }
  621. });
  622. }
  623. function applyKnowledgeAdditions(outlinePayload, patch) {
  624. const beforeOutline = outlinePayload.outline || [];
  625. const outline = cloneOutlineItems(beforeOutline);
  626. const nodeMap = createOutlineNodeMap(outline);
  627. (patch.additions || []).forEach((addition) => {
  628. const parent = nodeMap.get(addition.parent_id);
  629. if (!parent || parent.level !== 2) {
  630. return;
  631. }
  632. const nextItem = createOutlineItemFromKnowledgeAddition(addition);
  633. parent.item.children = [...(parent.item.children || []), nextItem];
  634. });
  635. const normalized = normalizeOutlineResponse({ outline: renumber(outline) }, new Set());
  636. validateCompleteOutline(normalized);
  637. validateTopLevelPreserved(beforeOutline, normalized.outline);
  638. return normalized;
  639. }
  640. async function collectJson(aiService, options) {
  641. return aiService.collectJsonResponse ? aiService.collectJsonResponse(options) : aiService.requestJson(options);
  642. }
  643. async function generateFull(aiService, payload, suggestions, log, progress = 20) {
  644. log('正在一次性生成完整目录。', progress);
  645. return collectJson(aiService, {
  646. messages: generateOutlineMessages({ ...payload, suggestions }),
  647. temperature: 0.7,
  648. normalizer: (value) => normalizeOutlineResponse(value, new Set()),
  649. validator: validateCompleteOutline,
  650. progressCallback: (message) => log(message, progress),
  651. progressLabel: '完整目录',
  652. failureMessage: '模型返回的目录数据格式无效',
  653. });
  654. }
  655. async function generateTopLevel(aiService, payload, suggestions, log) {
  656. return collectJson(aiService, {
  657. messages: generateTopLevelOutlineMessages({ ...payload, suggestions }),
  658. temperature: 0.7,
  659. normalizer: (value) => normalizeOutlineResponse(value, new Set()),
  660. validator: validateTopLevelOutline,
  661. progressCallback: (message) => log(message, 25),
  662. progressLabel: '一级目录',
  663. failureMessage: '模型返回的目录数据格式无效',
  664. });
  665. }
  666. async function generateChildren(aiService, payload, parentItem, suggestions, log, progress) {
  667. const response = await collectJson(aiService, {
  668. messages: generateChildrenMessages({ ...payload, parentItem, suggestions }),
  669. temperature: 0.7,
  670. normalizer: (value) => normalizeChildrenResponse(value, new Set()),
  671. validator: validateChildrenOutline,
  672. progressCallback: (message) => log(message, progress),
  673. progressLabel: `章节 ${parentItem.title || '未命名章节'} 子目录`,
  674. failureMessage: '模型返回的目录数据格式无效',
  675. });
  676. return response;
  677. }
  678. async function generateFallback(aiService, payload, suggestions, log, progressRange = { start: 30, end: 75 }, topProgress = 25) {
  679. log('正在分步生成目录,先生成一级目录。', topProgress);
  680. const top = await generateTopLevel(aiService, payload, suggestions, log);
  681. const assembled = [];
  682. for (const [index, item] of top.outline.entries()) {
  683. const progress = progressRange.start + Math.round((index / Math.max(top.outline.length, 1)) * (progressRange.end - progressRange.start));
  684. log(`正在生成第 ${index + 1}/${top.outline.length} 个一级目录的二三级目录:${item.title || '未命名章节'}。`, progress);
  685. const childrenResponse = await generateChildren(aiService, payload, item, suggestions, log, progress);
  686. const children = childrenResponse.children || [];
  687. assembled.push({ id: item.id, title: item.title, description: item.description, ...(children.length ? { children } : {}) });
  688. }
  689. log('分步目录生成完成,正在整理目录编号。', progressRange.end);
  690. const outline = normalizeOutlineResponse({ outline: renumber(assembled) }, new Set());
  691. validateCompleteOutline(outline);
  692. return outline;
  693. }
  694. async function generateByMode(aiService, payload, mode, suggestions, log, progressOptions = {}) {
  695. const fullProgress = progressOptions.fullProgress ?? 20;
  696. const fallbackRange = progressOptions.fallbackRange || { start: 30, end: 75 };
  697. const fallbackTopProgress = progressOptions.fallbackTopProgress ?? 25;
  698. const fallbackNoticeProgress = progressOptions.fallbackNoticeProgress ?? 24;
  699. if (mode === 'full') return [await generateFull(aiService, payload, suggestions, log, fullProgress), 'full'];
  700. if (mode === 'fallback') return [await generateFallback(aiService, payload, suggestions, log, fallbackRange, fallbackTopProgress), 'fallback'];
  701. try {
  702. return [await generateFull(aiService, payload, suggestions, log, fullProgress), 'full'];
  703. } catch (error) {
  704. if (error.message !== '模型返回的目录数据格式无效') throw error;
  705. log('一次性生成完整目录失败,切换为分步生成模式。', fallbackNoticeProgress);
  706. return [await generateFallback(aiService, payload, suggestions, log, fallbackRange, fallbackTopProgress), 'fallback'];
  707. }
  708. }
  709. async function reviewOutline(aiService, payload, outline, log, progressLabel, progress = 82) {
  710. return collectJson(aiService, {
  711. messages: reviewOutlineMessages({ ...payload, outline }),
  712. temperature: 0.3,
  713. normalizer: normalizeReviewResponse,
  714. progressCallback: (message) => log(message, progress),
  715. progressLabel,
  716. failureMessage: '模型返回的审核结果格式无效',
  717. });
  718. }
  719. async function reviewAlignedOutline(aiService, payload, groups, outline, log, progressLabel, progress = 82) {
  720. return collectJson(aiService, {
  721. messages: reviewAlignedOutlineMessages({ ...payload, groups, outline }),
  722. temperature: 0.3,
  723. normalizer: normalizeReviewResponse,
  724. progressCallback: (message) => log(message, progress),
  725. progressLabel,
  726. failureMessage: '模型返回的审核结果格式无效',
  727. });
  728. }
  729. async function freeWorkflow(aiService, payload, log) {
  730. log('开始生成目录结构。', 8);
  731. const [first, generationMode] = await generateByMode(aiService, payload, 'auto', undefined, log);
  732. log('首次目录生成完成,开始审核目录质量。', 82);
  733. const firstReview = await reviewOutline(aiService, payload, first, log, '首次审核', 82);
  734. if (firstReview.passed) {
  735. log('目录审核通过,准备返回结果。', 96);
  736. return first;
  737. }
  738. const suggestions = firstReview.suggestions?.length ? firstReview.suggestions : ['请根据项目概述和技术评分要求补全目录覆盖范围,并修正不合理章节。'];
  739. log('目录审核未通过,正在根据修改建议重新生成。', 88);
  740. let second;
  741. try {
  742. [second] = await generateByMode(aiService, payload, generationMode, suggestions, log, {
  743. fullProgress: 90,
  744. fallbackNoticeProgress: 89,
  745. fallbackTopProgress: 90,
  746. fallbackRange: { start: 90, end: 96 },
  747. });
  748. } catch {
  749. log('根据审核建议重新生成失败,已回退到首次生成结果。', 97);
  750. return first;
  751. }
  752. log('二次生成完成,开始最终审核。', 97);
  753. const secondReview = await reviewOutline(aiService, payload, second, log, '最终审核', 97);
  754. log(secondReview.passed ? '最终审核通过,准备返回修正后的结果。' : '最终审核未完全通过,已返回修正后的第二次结果。', 98);
  755. return second;
  756. }
  757. async function extractRequirementGroups(aiService, requirements, suggestions, log) {
  758. const response = await collectJson(aiService, {
  759. messages: extractRequirementGroupsMessages(requirements, suggestions),
  760. temperature: 0.3,
  761. normalizer: normalizeRequirementGroupsResponse,
  762. validator: validateRequirementGroups,
  763. progressCallback: (message) => log(message, 10),
  764. progressLabel: '技术评分大类',
  765. failureMessage: '模型返回的技术评分大类格式无效',
  766. });
  767. return response.groups || [];
  768. }
  769. async function generateAlignedChildrenForGroup(aiService, payload, parentItem, group, suggestions, log, progress) {
  770. const response = await collectJson(aiService, {
  771. messages: generateAlignedChildrenMessages({ ...payload, parentItem, group, suggestions }),
  772. temperature: 0.7,
  773. normalizer: (value) => normalizeChildrenResponse(value, new Set()),
  774. validator: validateChildrenOutline,
  775. progressCallback: (message) => log(message, progress),
  776. progressLabel: `章节 ${parentItem.title || '未命名章节'} 子目录`,
  777. failureMessage: '模型返回的目录数据格式无效',
  778. });
  779. return response;
  780. }
  781. async function buildAligned(aiService, payload, groups, suggestions, log, progressRange = { start: 30, end: 75 }) {
  782. const top = buildTopLevelOutlineFromGroups(groups);
  783. validateAlignedTopLevelMapping(top, groups);
  784. const assembled = [];
  785. for (const [index, item] of top.entries()) {
  786. const progress = progressRange.start + Math.round((index / Math.max(top.length, 1)) * (progressRange.end - progressRange.start));
  787. log(`正在生成第 ${index + 1}/${top.length} 个评分大类的二三级目录:${item.title || '未命名章节'}。`, progress);
  788. const childrenResponse = await generateAlignedChildrenForGroup(aiService, payload, item, groups[index], suggestions, log, progress);
  789. const children = childrenResponse.children || [];
  790. assembled.push({ ...item, ...(children.length ? { children } : {}) });
  791. }
  792. log('评分项对齐目录生成完成,正在整理目录编号。', progressRange.end);
  793. const outline = normalizeOutlineResponse({ outline: renumber(assembled) }, new Set());
  794. validateCompleteOutline(outline);
  795. validateAlignedTopLevelMapping(outline.outline || [], groups);
  796. return outline;
  797. }
  798. async function alignedWorkflow(aiService, payload, log) {
  799. log('开始提取技术评分大类。', 10);
  800. const groups = await extractRequirementGroups(aiService, payload.requirements, undefined, log);
  801. log('技术评分大类提取完成,正在构建一级目录。', 24);
  802. const first = await buildAligned(aiService, payload, groups, undefined, log, { start: 30, end: 75 });
  803. log('目录生成完成,正在审核与技术评分项的对应关系。', 82);
  804. const firstReview = await reviewAlignedOutline(aiService, payload, groups, first, log, '首次审核', 82);
  805. if (firstReview.passed) {
  806. log('目录审核通过,准备返回结果。', 96);
  807. return first;
  808. }
  809. const suggestions = firstReview.suggestions?.length ? firstReview.suggestions : ['请保持一级目录与技术评分大类标题完全一致,并补全各大类下遗漏的评分细项。'];
  810. log('目录审核未通过,正在根据修改建议重新提取技术评分大类并重新生成目录。', 88);
  811. let revisedGroups = groups;
  812. let second;
  813. try {
  814. log('正在根据审核建议重新提取技术评分大类。', 90);
  815. revisedGroups = await extractRequirementGroups(aiService, payload.requirements, suggestions, log);
  816. second = await buildAligned(aiService, payload, revisedGroups, suggestions, log, { start: 91, end: 96 });
  817. } catch {
  818. log('根据审核建议重新生成失败,已回退到首次生成结果。', 97);
  819. return first;
  820. }
  821. log('二次生成完成,开始最终审核。', 97);
  822. const secondReview = await reviewAlignedOutline(aiService, payload, revisedGroups, second, log, '最终审核', 97);
  823. log(secondReview.passed ? '最终审核通过,准备返回修正后的结果。' : '最终审核未完全通过,已返回修正后的第二次结果。', 98);
  824. return second;
  825. }
  826. async function enhanceOutlineWithKnowledgeAdditions(aiService, payload, outline, knowledgeItems, log) {
  827. if (!knowledgeItems.length) return outline;
  828. const outlineNodeMap = createOutlineNodeMap(outline.outline || []);
  829. const additionParents = collectKnowledgeAdditionParents(outline.outline || []);
  830. if (!additionParents.length) {
  831. log('当前目录没有可补充的二级目录,跳过参考知识库。', 98);
  832. return outline;
  833. }
  834. const rawAttempts = [];
  835. const normalizationStats = [];
  836. const isDeveloperMode = Boolean(aiService.isDeveloperMode?.());
  837. const devLog = (message) => {
  838. if (isDeveloperMode) log(`[开发者] ${message}`, 98);
  839. };
  840. log(`开始根据 ${knowledgeItems.length} 条知识库条目补充缺失三级目录。`, 98);
  841. devLog(`知识库补目录:可用二级父级 ${additionParents.length} 个,参考知识条目 ${knowledgeItems.length} 条。`);
  842. const patch = await collectJson(aiService, {
  843. messages: generateKnowledgeAdditionMessages({ ...payload, outline, knowledgeItems }),
  844. temperature: 0.3,
  845. normalizer: (value) => normalizeKnowledgeAdditionsResponse(value, {
  846. outline: outline.outline || [],
  847. outlineNodeMap,
  848. rawAttempts,
  849. normalizationStats,
  850. debugLog: devLog,
  851. }),
  852. validator: validateKnowledgeAdditionsResponse,
  853. repairMessagesBuilder: (context) => generateKnowledgeAdditionRepairMessages(context, additionParents),
  854. progressCallback: (message) => log(message, 98),
  855. progressLabel: '知识库补目录',
  856. failureMessage: '模型返回的知识库补目录格式无效',
  857. });
  858. if (rawAttempts.length) {
  859. devLog(`模型原始返回尝试 ${rawAttempts.length} 次:${rawAttempts.map((item, index) => `#${index + 1} ${formatAdditionSummary(item)}`).join(';')}`);
  860. }
  861. const lastStats = normalizationStats[normalizationStats.length - 1] || { retained: patch.additions.length, dropped: 0, adjustedParent: 0 };
  862. devLog(`程序归一:保留 ${lastStats.retained} 条,删除 ${lastStats.dropped} 条,自动改 parent ${lastStats.adjustedParent} 条。`);
  863. if (rawAttempts.length > 1) {
  864. devLog(`修复后:保留 ${lastStats.retained} 条。`);
  865. }
  866. const enhanced = applyKnowledgeAdditions(outline, patch);
  867. const additionCount = patch.additions.length;
  868. devLog(`最终应用:新增三级目录 ${additionCount} 个。`);
  869. if (!additionCount) {
  870. log('知识库未返回可补充三级目录,保留原目录。', 99);
  871. } else {
  872. log(`知识库补目录已应用:新增三级目录 ${additionCount} 个。`, 99);
  873. }
  874. return enhanced;
  875. }
  876. async function runOutlineGenerationTask({ aiService, workspaceStore, knowledgeBaseService, updateTask, payload }) {
  877. let logs = ['开始生成目录。'];
  878. let currentProgress = 5;
  879. function log(message, progress = currentProgress) {
  880. currentProgress = Math.max(currentProgress, Math.min(progress, 99));
  881. logs = [...logs, message];
  882. const technicalPlan = workspaceStore.updateTechnicalPlan({ outlineGenerationTask: updateTask({ status: 'running', progress: currentProgress, logs }) });
  883. updateTask({ status: 'running', progress: currentProgress, logs }, technicalPlan);
  884. }
  885. const referenceKnowledgeDocumentIds = normalizeReferenceDocumentIds(payload);
  886. let technicalPlan = workspaceStore.updateTechnicalPlan({
  887. outlineMode: payload.mode,
  888. referenceKnowledgeDocumentIds,
  889. outlineGenerationTask: updateTask({ status: 'running', progress: 5, logs }),
  890. });
  891. updateTask({ status: 'running', progress: 5, logs }, technicalPlan);
  892. const taskPayload = {
  893. ...payload,
  894. reference_knowledge_document_ids: referenceKnowledgeDocumentIds,
  895. };
  896. let outline = taskPayload.mode === 'aligned' ? await alignedWorkflow(aiService, taskPayload, log) : await freeWorkflow(aiService, taskPayload, log);
  897. const knowledgeItems = loadOutlineKnowledgeItems(knowledgeBaseService, referenceKnowledgeDocumentIds, log);
  898. outline = await enhanceOutlineWithKnowledgeAdditions(aiService, taskPayload, outline, knowledgeItems, log);
  899. technicalPlan = workspaceStore.updateTechnicalPlan({
  900. outlineData: { ...outline, project_overview: payload.overview },
  901. contentGenerationTask: undefined,
  902. contentGenerationSections: {},
  903. contentGenerationPlans: {},
  904. outlineGenerationTask: updateTask({ status: 'success', progress: 100, logs: [...logs, '目录生成完成。'] }),
  905. });
  906. updateTask({ status: 'success', progress: 100, logs: [...logs, '目录生成完成。'] }, technicalPlan);
  907. }
  908. module.exports = { runOutlineGenerationTask };