function formatSuggestions(suggestions) { if (!suggestions?.length) return ''; return `\n\n本轮修正建议:\n${suggestions.map((item, index) => `${index + 1}. ${item}`).join('\n')}`; } const KNOWLEDGE_RESUME_MAX_CHARS = 220; const MAX_KNOWLEDGE_ADDITIONS = 30; function truncateText(value, maxLength) { const text = String(value || '').replace(/\s+/g, ' ').trim(); return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text; } function renderKnowledgeItemsForPrompt(items) { if (!items?.length) return ''; return items.map((item, index) => [ `${index + 1}. title: ${item.title}`, ` resume: ${truncateText(item.resume, KNOWLEDGE_RESUME_MAX_CHARS)}`, ].join('\n')).join('\n'); } function collectKnowledgeAdditionParents(items) { const parents = []; function visit(nodes, level = 1, ancestors = []) { (nodes || []).forEach((item) => { const id = String(item?.id || '').trim(); const title = String(item?.title || '').trim(); if (id && level === 2) { parents.push({ id, title, parentTitle: ancestors[0]?.title || '', childTitles: (item.children || []).map((child) => String(child?.title || '').trim()).filter(Boolean), }); } if (item?.children?.length) visit(item.children, level + 1, [...ancestors, { id, title }]); }); } visit(items || []); return parents; } function formatKnowledgeAdditionParents(parents) { return (parents || []).map((item) => [ `- ${item.id} ${item.title || '未命名二级目录'}(所属一级:${item.parentTitle || '未命名一级目录'})`, ` 已有三级目录:${item.childTitles.length ? item.childTitles.join(';') : '无'}`, ].join('\n')).join('\n'); } function normalizeReferenceDocumentIds(payload) { return Array.isArray(payload?.reference_knowledge_document_ids) ? [...new Set(payload.reference_knowledge_document_ids.map((id) => String(id || '').trim()).filter(Boolean))] : []; } function loadOutlineKnowledgeItems(knowledgeBaseService, documentIds, log) { if (!documentIds.length) return []; if (!knowledgeBaseService?.getOutlineReferences) { log('未找到知识库读取服务,跳过参考知识库。', 6); return []; } try { log(`正在读取 ${documentIds.length} 个参考知识库文档。`, 6); const result = knowledgeBaseService.getOutlineReferences(documentIds); const items = Array.isArray(result?.items) ? result.items : []; log(items.length ? `已读取 ${items.length} 条轻量知识条目。` : '未读取到可用知识库条目,将按普通目录生成。', 7); return items; } catch (error) { log(`读取参考知识库失败,将按普通目录生成:${error.message || String(error)}`, 7); return []; } } function outlineSystemPrompt() { return `你是一个专业的标书编写专家。根据提供的项目概述和技术评分要求,生成投标文件中技术标部分的目录结构。 如果用户提供了自己编写的目录,你要保证目录满足技术评分要求,并充分结合用户自己编写的目录。 要求: 1. 目录结构要全面覆盖技术标的所有必要章节 2. 章节名称要专业、准确,符合投标文件规范 3. 一级目录名称要与技术评分要求中的章节名称一致;如果技术评分要求中没有明确章节名称,则结合内容总结一级目录名称 4. 一共包括三级目录 5. 返回标准 JSON 格式,包含章节编号、标题、描述和子章节 6. 除了 JSON 结果外,不要输出任何其他内容 JSON 格式要求: { "outline": [ { "id": "1", "title": "", "description": "", "children": [ { "id": "1.1", "title": "", "description": "", "children": [ { "id": "1.1.1", "title": "", "description": "" } ] } ] } ] }`; } function topLevelOutlineSystemPrompt() { return `你是一个专业的标书编写专家。根据提供的项目概述和技术评分要求,生成投标文件中技术标部分的一级目录结构。 如果用户提供了自己编写的目录,你要保证一级目录满足技术评分要求,并充分结合用户自己编写的目录。 要求: 1. 只生成一级目录,不要生成二级和三级目录 2. 一级目录名称要专业、准确,符合投标文件规范 3. 一级目录名称要尽量与技术评分要求中的章节名称一致;如果技术评分要求中没有明确章节名称,则结合内容总结一级目录名称 4. 返回标准 JSON 格式,使用 outline 字段,每个一级目录必须包含 id、title、description 5. 除了 JSON 结果外,不要输出任何其他内容 JSON 格式要求: { "outline": [ { "id": "1", "title": "", "description": "" } ] }`; } function generateOutlineMessages({ overview, requirements, suggestions }) { return [ { role: 'system', content: outlineSystemPrompt() }, { role: 'user', content: `项目概述:\n${overview}` }, { role: 'user', content: `技术评分要求:\n${requirements}` }, { role: 'user', content: `请生成完整的技术标目录结构,确保覆盖所有技术评分要点。${formatSuggestions(suggestions)}` }, ]; } function generateTopLevelOutlineMessages({ overview, requirements, suggestions }) { return [ { role: 'system', content: topLevelOutlineSystemPrompt() }, { role: 'user', content: `项目概述:\n${overview}` }, { role: 'user', content: `技术评分要求:\n${requirements}` }, { role: 'user', content: `请仅生成一级目录列表,不要生成二级和三级目录。返回的 JSON 仍然使用 outline 字段,每个一级目录都必须包含 id、title、description。${formatSuggestions(suggestions)}` }, ]; } function extractRequirementGroupsMessages(requirements, suggestions) { const systemPrompt = `你是一个专业的招标文件分析专家。请从技术评分要求中提取适合作为技术标一级目录的评分大类。 要求: 1. 只提取技术评分大类,不要提取商务、报价、资质、售后服务等非技术类条目 2. 每个大类都必须适合作为技术标一级目录标题,标题要专业、简洁、完整 3. 同一大类下的细项、子项、分值说明、评分标准要归入 detail_points,不要拆成多个一级目录 4. requirement_id 必须唯一,使用 R1、R2、R3 这种格式 5. description 需要概括该大类关注的核心内容 6. detail_points 中保留该大类下的关键评分细项,使用简洁短句 7. 只返回 JSON,格式必须为 {"groups": [...]},不要输出任何其他内容 JSON 格式要求: { "groups": [ { "requirement_id": "R1", "title": "", "description": "", "detail_points": ["", ""] } ] }`; return [ { role: 'system', content: systemPrompt }, { role: 'user', content: `技术评分要求:\n${requirements}` }, { role: 'user', content: `请提取所有适合作为技术标一级目录的技术评分大类,保持顺序稳定,并把每个大类下的评分细项归入 detail_points。${formatSuggestions(suggestions)}` }, ]; } function generateAlignedChildrenMessages({ overview, requirements, parentItem, group, suggestions }) { const detailLines = (group.detail_points || []) .filter((item) => typeof item === 'string' && item.trim()) .map((item) => `- ${item}`) .join('\n'); const detailContent = detailLines || '- 未提供明确细项,请根据评分大类描述合理展开'; const systemPrompt = `你是一个专业的标书编写专家。请围绕指定的技术评分大类,为已经固定好的一级目录生成二级和三级目录。 要求: 1. 一级目录标题和顺序已经固定,不能修改、重命名、合并或删除一级目录 2. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身 3. 二级和三级目录要覆盖当前技术评分大类及其细项,不能越界写入其他评分大类内容 4. 返回标准 JSON,格式为 {"children": [...]},children 中只能包含当前一级目录的直接子目录 5. 每个节点必须包含 id、title、description,三级目录继续使用 children 字段 6. 章节编号必须以给定的一级目录编号为前缀,例如父级是 2,则二级目录编号从 2.1 开始,三级目录编号从 2.1.1 开始 7. 除了 JSON 结果外,不要输出任何其他内容`; const messages = [ { role: 'system', content: systemPrompt }, { role: 'user', content: `项目概述:\n${overview}` }, { role: 'user', content: `技术评分要求原文:\n${requirements}` }, { role: 'user', content: `当前固定一级目录:\n编号:${parentItem.id}\n标题:${parentItem.title}\n描述:${parentItem.description || ''}` }, { role: 'user', content: `当前对应的技术评分大类:\nrequirement_id:${group.requirement_id}\n标题:${group.title}\n描述:${group.description}\n细项:\n${detailContent}` }, ]; messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,一级目录标题必须保持为当前给定标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` }); return messages; } function generateChildrenMessages({ overview, requirements, parentItem, suggestions }) { const systemPrompt = `你是一个专业的标书编写专家。请围绕指定的一级目录,生成其下属的二级目录和三级目录。 要求: 1. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身 2. 返回标准 JSON,格式为 {"children": [...]} 3. children 中只能包含当前一级目录的直接子目录,每个节点必须包含 id、title、description 4. 二级目录下如有三级目录,同样使用 children 字段 5. 章节编号必须以给定的一级目录编号为前缀,例如父级是 2,则二级目录编号从 2.1 开始,三级目录编号从 2.1.1 开始 6. 除了 JSON 结果外,不要输出任何其他内容`; const messages = [ { role: 'system', content: systemPrompt }, { role: 'user', content: `项目概述:\n${overview}` }, { role: 'user', content: `技术评分要求:\n${requirements}` }, { role: 'user', content: `当前一级目录:\n编号:${parentItem.id}\n标题:${parentItem.title}\n描述:${parentItem.description || ''}` }, ]; messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` }); return messages; } function reviewOutlineMessages({ overview, requirements, outline }) { const systemPrompt = `你是一个严格的招标文件目录审核专家。请审核目录是否符合项目概述和技术评分要求。 要求: 1. 重点检查目录是否完整覆盖技术评分要点 2. 检查一级目录名称是否专业、准确,是否尽量与评分项原文保持一致 3. 检查目录层级是否清晰,是否达到三级目录要求,是否存在明显遗漏、错位、重复或不合理章节 4. 只返回 JSON,格式为:{"passed": true, "suggestions": []} 5. 若不通过,suggestions 中必须给出具体、可执行的修改建议 6. 除了 JSON 外,不要输出任何其他内容`; return [ { role: 'system', content: systemPrompt }, { role: 'user', content: `项目概述:\n${overview}` }, { role: 'user', content: `技术评分要求:\n${requirements}` }, { role: 'user', content: `待审核目录 JSON:\n${JSON.stringify(outline)}` }, { role: 'user', content: '请判断该目录是否满足要求。若满足则返回 passed=true;若不满足则返回 passed=false,并给出具体修改建议。' }, ]; } function reviewAlignedOutlineMessages({ overview, requirements, groups, outline }) { const systemPrompt = `你是一个严格的招标文件目录审核专家。请审核目录是否与技术评分大类一一对应,并判断二三级目录是否覆盖各评分大类的细项。 要求: 1. 一级目录必须与提供的技术评分大类一一对应,数量一致、顺序一致、标题必须完全一致 2. 不允许缺失技术评分大类,也不允许新增、合并、改写一级目录 3. 二级和三级目录要围绕各自对应的技术评分大类与细项展开,避免错位、遗漏和明显重复 4. 检查完整目录是否层级清晰,整体是否达到三级目录要求 5. 只返回 JSON,格式为:{"passed": true, "suggestions": []} 6. 若不通过,suggestions 中必须给出具体、可执行的修改建议,重点说明哪个评分大类覆盖不足或结构不合理 7. 除了 JSON 外,不要输出任何其他内容`; return [ { role: 'system', content: systemPrompt }, { role: 'user', content: `项目概述:\n${overview}` }, { role: 'user', content: `技术评分要求:\n${requirements}` }, { role: 'user', content: `技术评分大类 JSON:\n${JSON.stringify({ groups })}` }, { role: 'user', content: `待审核目录 JSON:\n${JSON.stringify(outline)}` }, { role: 'user', content: '请判断该目录是否满足一一对应要求。若满足则返回 passed=true;若不满足则返回 passed=false,并给出具体修改建议。' }, ]; } function generateKnowledgeAdditionMessages({ overview, requirements, outline, knowledgeItems }) { const additionParents = collectKnowledgeAdditionParents(outline.outline || []); const sampleParent = additionParents[0]?.id || ''; const instructionPrompt = `你是一个严格的标书目录补充专家。你只能根据参考知识库判断现有二级目录下是否缺少三级目录,并只输出新增三级目录。 要求: 1. 已有一级目录和二级目录都已经固定,不允许新增、删除、重命名、合并或调整顺序 2. 只能新增三级目录,parent_id 必须逐字复制“可补充二级目录 parent_id”中的某一个 ID 3. 不允许输出 bindings、knowledge_item_ids、id、children、outline 或完整目录 4. 不要把知识库条目绑定到目录;知识库只作为判断是否缺少三级目录的参考材料 5. 只补充与招标项目、评分项、现有二级目录主题强相关且当前三级目录确实缺失的内容 6. 不要重复已有三级目录,也不要输出同义重复目录 7. 如果没有确实需要补充的三级目录,返回空 additions 数组 8. 只返回 JSON,不要输出解释文字 返回格式: { "additions": [ { "parent_id": "${sampleParent}", "title": "新增三级目录标题", "description": "新增三级目录说明" } ] }`; return [ { role: 'user', content: instructionPrompt }, { role: 'user', content: `项目概述:\n${overview}` }, { role: 'user', content: `技术评分要求:\n${requirements}` }, { role: 'user', content: `当前完整目录 JSON:\n${JSON.stringify(outline, null, 2)}` }, { role: 'user', content: `可补充二级目录 parent_id(只能逐字复制以下 ID,并在其下新增三级目录):\n${formatKnowledgeAdditionParents(additionParents)}` }, { role: 'user', content: `参考知识库轻量条目如下。注意:这些只是参考资料,不要输出知识库 ID,也不要绑定知识库条目。\n${renderKnowledgeItemsForPrompt(knowledgeItems)}` }, { role: 'user', content: '请只返回知识库补充三级目录 JSON:additions。每条 additions 只能包含 parent_id、title、description。' }, ]; } function generateKnowledgeAdditionRepairMessages({ invalidContent, issues }, additionParents) { return [ { role: 'user', content: `你是一个严格的 JSON 修复器。请把模型输出修复为“知识库补充三级目录”JSON。 必须满足: 1. 顶层只能有 additions 数组 2. 每条 additions 只能有 parent_id、title、description 3. parent_id 必须逐字复制允许的二级目录 ID 4. 禁止输出 bindings、knowledge_item_ids、id、children、outline 或完整目录 5. 如果没有可补充三级目录,返回 {"additions":[]} 6. 只返回 JSON,不要输出解释文字 允许的二级目录 parent_id: ${formatKnowledgeAdditionParents(additionParents)}`, }, { role: 'user', content: `错误列表:\n${issues}` }, { role: 'user', content: `待修复内容:\n\`\`\`json\n${String(invalidContent || '').slice(0, 60000)}\n\`\`\`` }, ]; } function requireObject(value, label) { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`${label} 必须是对象`); } return value; } function requireArray(value, label) { if (!Array.isArray(value)) { throw new Error(`${label} 必须是数组`); } return value; } function requireField(value, label) { if (value === undefined || value === null) { throw new Error(`${label} 缺失`); } return String(value); } function normalizeKnowledgeItemIds(value, allowedKnowledgeIds) { if (!Array.isArray(value)) { return []; } const ids = value.map((id) => String(id || '').trim()).filter(Boolean); if (allowedKnowledgeIds instanceof Set) { return [...new Set(ids.filter((id) => allowedKnowledgeIds.has(id)))]; } return [...new Set(ids)]; } function normalizeOutlineItem(item, path = 'outline[]', allowedKnowledgeIds) { const raw = requireObject(item, path); const normalized = { id: requireField(raw.id, `${path}.id`), title: requireField(raw.title, `${path}.title`), description: requireField(raw.description, `${path}.description`), }; if (raw.source_requirement_id !== undefined && raw.source_requirement_id !== null) { normalized.source_requirement_id = String(raw.source_requirement_id); } if (raw.source_requirement_title !== undefined && raw.source_requirement_title !== null) { normalized.source_requirement_title = String(raw.source_requirement_title); } if (raw.content !== undefined && raw.content !== null) { normalized.content = String(raw.content); } const knowledgeItemIds = normalizeKnowledgeItemIds(raw.knowledge_item_ids, allowedKnowledgeIds); if (knowledgeItemIds.length) { normalized.knowledge_item_ids = knowledgeItemIds; } if (raw.children !== undefined && raw.children !== null) { const children = requireArray(raw.children, `${path}.children`); if (children.length) { normalized.children = children.map((child, index) => normalizeOutlineItem(child, `${path}.children[${index}]`, allowedKnowledgeIds)); } } return normalized; } function normalizeOutlineResponse(payload, allowedKnowledgeIds) { const raw = requireObject(payload, 'OutlineResponse'); const outline = requireArray(raw.outline, 'outline'); return { outline: outline.map((item, index) => normalizeOutlineItem(item, `outline[${index}]`, allowedKnowledgeIds)) }; } function normalizeChildrenResponse(payload, allowedKnowledgeIds) { const raw = requireObject(payload, 'OutlineChildrenResponse'); const children = requireArray(raw.children, 'children'); return { children: children.map((item, index) => normalizeOutlineItem(item, `children[${index}]`, allowedKnowledgeIds)) }; } function normalizeReviewResponse(payload) { const raw = requireObject(payload, 'OutlineReviewResponse'); let passed = raw.passed; if (typeof passed === 'string') { passed = passed.toLowerCase() === 'true'; } if (typeof passed !== 'boolean') { throw new Error('passed 必须是布尔值'); } const suggestions = raw.suggestions === undefined || raw.suggestions === null ? [] : requireArray(raw.suggestions, 'suggestions').map((item) => String(item)); return { passed, suggestions }; } function normalizeRequirementGroupsResponse(payload) { const raw = requireObject(payload, 'TechnicalRequirementGroupResponse'); const groups = requireArray(raw.groups, 'groups').map((group, index) => { const item = requireObject(group, `groups[${index}]`); return { requirement_id: requireField(item.requirement_id, `groups[${index}].requirement_id`), title: requireField(item.title, `groups[${index}].title`), description: requireField(item.description, `groups[${index}].description`), detail_points: item.detail_points === undefined || item.detail_points === null ? [] : requireArray(item.detail_points, `groups[${index}].detail_points`).map((point) => String(point)), }; }); return { groups }; } function createOutlineNodeMap(items) { const map = new Map(); function visit(nodes, level = 1, parent = null) { (nodes || []).forEach((item) => { const id = String(item?.id || '').trim(); if (id) { map.set(id, { item, level, parent }); } if (item?.children?.length) { visit(item.children, level + 1, item); } }); } visit(items || []); return map; } function normalizeTitleKey(value) { return String(value || '').replace(/\s+/g, '').toLowerCase(); } function countNestedArrayEntries(value, fieldName) { if (!value || typeof value !== 'object') return 0; if (Array.isArray(value)) { return value.reduce((sum, item) => sum + countNestedArrayEntries(item, fieldName), 0); } return Object.entries(value).reduce((sum, [key, child]) => { const current = key === fieldName && Array.isArray(child) ? child.length : 0; return sum + current + countNestedArrayEntries(child, fieldName); }, 0); } function summarizeRawKnowledgeAdditions(payload) { const raw = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}; return { additions: Array.isArray(payload) ? payload.length : (Array.isArray(raw.additions) ? raw.additions.length : 0), bindings: Array.isArray(raw.bindings) ? raw.bindings.length : 0, knowledge_refs: countNestedArrayEntries(payload, 'knowledge_item_ids'), children: countNestedArrayEntries(payload, 'children'), }; } function formatAdditionSummary(summary) { return `additions=${summary.additions},bindings=${summary.bindings},knowledge_refs=${summary.knowledge_refs},children=${summary.children}`; } function getKnowledgeAdditionCandidates(payload) { if (Array.isArray(payload)) return payload; const raw = requireObject(payload, 'KnowledgeAdditionsResponse'); if (raw.additions !== undefined && raw.additions !== null) return requireArray(raw.additions, 'additions'); if (Array.isArray(raw.items)) return raw.items; if (Array.isArray(raw.directories)) return raw.directories; return []; } function createExistingThirdTitleKeys(outlineItems) { const keys = new Set(); function visit(nodes, level = 1) { (nodes || []).forEach((item) => { const id = String(item?.id || '').trim(); if (level === 2 && id) { (item.children || []).forEach((child) => { const key = normalizeTitleKey(child?.title); if (key) keys.add(`${id}::${key}`); }); } if (item?.children?.length) visit(item.children, level + 1); }); } visit(outlineItems || []); return keys; } function resolveKnowledgeAdditionParent(parentId, context, stats) { const parentInfo = context.outlineNodeMap.get(parentId); if (!parentInfo) return null; if (parentInfo.level === 2) return { parentId, parentInfo }; if (parentInfo.level === 3 && parentInfo.parent?.id) { const nextParentId = String(parentInfo.parent.id || '').trim(); const nextParentInfo = context.outlineNodeMap.get(nextParentId); if (nextParentInfo?.level === 2) { stats.adjustedParent += 1; return { parentId: nextParentId, parentInfo: nextParentInfo }; } } return null; } function normalizeKnowledgeAddition(addition, path, context, stats, seenKeys, issues) { if (!addition || typeof addition !== 'object' || Array.isArray(addition)) { stats.dropped += 1; issues.push(`${path} 必须是对象`); return null; } const rawParentId = String(addition.parent_id || '').trim(); if (!rawParentId) { stats.dropped += 1; issues.push(`${path}.parent_id 缺失`); return null; } const resolvedParent = resolveKnowledgeAdditionParent(rawParentId, context, stats); if (!resolvedParent) { stats.dropped += 1; issues.push(`${path}.parent_id=${rawParentId} 不是现有二级目录 ID`); return null; } const title = String(addition.title || addition.name || '').trim(); if (!title) { stats.dropped += 1; issues.push(`${path}.title 缺失或为空`); return null; } const dedupeKey = `${resolvedParent.parentId}::${normalizeTitleKey(title)}`; if (seenKeys.has(dedupeKey)) { stats.dropped += 1; return null; } seenKeys.add(dedupeKey); stats.retained += 1; const description = String(addition.description || addition.summary || addition.resume || title).trim() || title; return { parent_id: resolvedParent.parentId, title, description }; } function normalizeKnowledgeAdditionsResponse(payload, context) { const raw = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}; const rawSummary = summarizeRawKnowledgeAdditions(payload); if (context.rawAttempts) context.rawAttempts.push(rawSummary); const candidates = getKnowledgeAdditionCandidates(payload); const stats = { retained: 0, dropped: 0, adjustedParent: 0 }; const issues = []; const seenKeys = createExistingThirdTitleKeys(context.outline || []); const additions = []; candidates.forEach((addition, index) => { if (additions.length >= MAX_KNOWLEDGE_ADDITIONS) { stats.dropped += 1; return; } const normalized = normalizeKnowledgeAddition(addition, `additions[${index}]`, context, stats, seenKeys, issues); if (normalized) additions.push(normalized); }); if (context.normalizationStats) context.normalizationStats.push(stats); const shouldRepair = !additions.length && ( raw.outline !== undefined || raw.bindings !== undefined || raw.knowledge_item_ids !== undefined || (candidates.length > 0 && issues.length > 0) ); if (shouldRepair) { const reason = issues.length ? issues.join(';') : '模型返回了 bindings/outline/knowledge_item_ids,但没有可应用的三级目录 additions'; if (context.debugLog) context.debugLog(`进入修复:${reason}`); throw new Error(`知识库补充三级目录格式无效:${reason}`); } return { additions }; } function validateKnowledgeAdditionsResponse(payload) { requireArray(payload.additions, 'additions'); } function outlineDepth(items) { return items?.length ? 1 + Math.max(...items.map((item) => outlineDepth(item.children || []))) : 0; } function validateCompleteOutline(payload) { const outline = payload.outline || []; if (!outline.length) throw new Error('目录不能为空'); if (outlineDepth(outline) < 3) throw new Error('完整目录至少需要三级结构'); } function validateTopLevelOutline(payload) { if (!(payload.outline || []).length) throw new Error('一级目录不能为空'); } function validateChildrenOutline(payload) { if (!(payload.children || []).length) throw new Error('二级目录不能为空'); } function validateRequirementGroups(payload) { const groups = payload.groups || []; if (!groups.length) throw new Error('技术评分大类不能为空'); const requirementIds = []; const titles = []; groups.forEach((group, index) => { const requirementId = String(group.requirement_id || '').trim(); const title = String(group.title || '').trim(); const description = String(group.description || '').trim(); if (!requirementId) throw new Error(`第 ${index + 1} 个技术评分大类缺少 requirement_id`); if (!title) throw new Error(`第 ${index + 1} 个技术评分大类缺少标题`); if (!description) throw new Error(`第 ${index + 1} 个技术评分大类缺少描述`); requirementIds.push(requirementId); titles.push(title); }); if (new Set(requirementIds).size !== requirementIds.length) throw new Error('技术评分大类 requirement_id 不能重复'); if (new Set(titles).size !== titles.length) throw new Error('技术评分大类标题不能重复'); } function buildTopLevelOutlineFromGroups(groups) { return groups.map((group, index) => { const title = String(group.title || '').trim(); return { id: String(index + 1), title, description: String(group.description || title).trim(), source_requirement_id: String(group.requirement_id || `R${index + 1}`).trim(), source_requirement_title: title, }; }); } function validateAlignedTopLevelMapping(outlineItems, groups) { if (outlineItems.length !== groups.length) throw new Error('一级目录数量必须与技术评分大类数量一致'); outlineItems.forEach((item, index) => { const expectedTitle = String(groups[index].title || '').trim(); const actualTitle = String(item.title || '').trim(); if (actualTitle !== expectedTitle) throw new Error(`第 ${index + 1} 个一级目录标题必须严格等于技术评分大类标题:${expectedTitle}`); const expectedRequirementId = String(groups[index].requirement_id || '').trim(); const actualRequirementId = String(item.source_requirement_id || '').trim(); if (actualRequirementId !== expectedRequirementId) throw new Error(`第 ${index + 1} 个一级目录映射的技术评分大类ID不正确:${expectedRequirementId}`); }); } function renumber(items, parent = '') { return (items || []).map((item, index) => { const id = parent ? `${parent}.${index + 1}` : `${index + 1}`; const next = { ...item, id }; if (item.children?.length) next.children = renumber(item.children, id); else delete next.children; return next; }); } function cloneOutlineItems(items) { return (items || []).map((item) => ({ ...item, ...(item.knowledge_item_ids?.length ? { knowledge_item_ids: [...item.knowledge_item_ids] } : {}), ...(item.children?.length ? { children: cloneOutlineItems(item.children) } : {}), })); } function createOutlineItemFromKnowledgeAddition(addition) { return { id: '', title: addition.title, description: addition.description, }; } function validateTopLevelPreserved(beforeItems, afterItems) { if ((beforeItems || []).length !== (afterItems || []).length) { throw new Error('知识库补目录不允许改变一级目录数量'); } (beforeItems || []).forEach((beforeItem, index) => { const afterItem = afterItems[index]; if (String(beforeItem.title || '').trim() !== String(afterItem?.title || '').trim()) { throw new Error('知识库补目录不允许修改一级目录标题'); } }); } function applyKnowledgeAdditions(outlinePayload, patch) { const beforeOutline = outlinePayload.outline || []; const outline = cloneOutlineItems(beforeOutline); const nodeMap = createOutlineNodeMap(outline); (patch.additions || []).forEach((addition) => { const parent = nodeMap.get(addition.parent_id); if (!parent || parent.level !== 2) { return; } const nextItem = createOutlineItemFromKnowledgeAddition(addition); parent.item.children = [...(parent.item.children || []), nextItem]; }); const normalized = normalizeOutlineResponse({ outline: renumber(outline) }, new Set()); validateCompleteOutline(normalized); validateTopLevelPreserved(beforeOutline, normalized.outline); return normalized; } async function collectJson(aiService, options) { return aiService.collectJsonResponse ? aiService.collectJsonResponse(options) : aiService.requestJson(options); } async function generateFull(aiService, payload, suggestions, log, progress = 20) { log('正在一次性生成完整目录。', progress); return collectJson(aiService, { messages: generateOutlineMessages({ ...payload, suggestions }), temperature: 0.7, normalizer: (value) => normalizeOutlineResponse(value, new Set()), validator: validateCompleteOutline, progressCallback: (message) => log(message, progress), progressLabel: '完整目录', failureMessage: '模型返回的目录数据格式无效', }); } async function generateTopLevel(aiService, payload, suggestions, log) { return collectJson(aiService, { messages: generateTopLevelOutlineMessages({ ...payload, suggestions }), temperature: 0.7, normalizer: (value) => normalizeOutlineResponse(value, new Set()), validator: validateTopLevelOutline, progressCallback: (message) => log(message, 25), progressLabel: '一级目录', failureMessage: '模型返回的目录数据格式无效', }); } async function generateChildren(aiService, payload, parentItem, suggestions, log, progress) { const response = await collectJson(aiService, { messages: generateChildrenMessages({ ...payload, parentItem, suggestions }), temperature: 0.7, normalizer: (value) => normalizeChildrenResponse(value, new Set()), validator: validateChildrenOutline, progressCallback: (message) => log(message, progress), progressLabel: `章节 ${parentItem.title || '未命名章节'} 子目录`, failureMessage: '模型返回的目录数据格式无效', }); return response; } async function generateFallback(aiService, payload, suggestions, log, progressRange = { start: 30, end: 75 }, topProgress = 25) { log('正在分步生成目录,先生成一级目录。', topProgress); const top = await generateTopLevel(aiService, payload, suggestions, log); const assembled = []; for (const [index, item] of top.outline.entries()) { const progress = progressRange.start + Math.round((index / Math.max(top.outline.length, 1)) * (progressRange.end - progressRange.start)); log(`正在生成第 ${index + 1}/${top.outline.length} 个一级目录的二三级目录:${item.title || '未命名章节'}。`, progress); const childrenResponse = await generateChildren(aiService, payload, item, suggestions, log, progress); const children = childrenResponse.children || []; assembled.push({ id: item.id, title: item.title, description: item.description, ...(children.length ? { children } : {}) }); } log('分步目录生成完成,正在整理目录编号。', progressRange.end); const outline = normalizeOutlineResponse({ outline: renumber(assembled) }, new Set()); validateCompleteOutline(outline); return outline; } async function generateByMode(aiService, payload, mode, suggestions, log, progressOptions = {}) { const fullProgress = progressOptions.fullProgress ?? 20; const fallbackRange = progressOptions.fallbackRange || { start: 30, end: 75 }; const fallbackTopProgress = progressOptions.fallbackTopProgress ?? 25; const fallbackNoticeProgress = progressOptions.fallbackNoticeProgress ?? 24; if (mode === 'full') return [await generateFull(aiService, payload, suggestions, log, fullProgress), 'full']; if (mode === 'fallback') return [await generateFallback(aiService, payload, suggestions, log, fallbackRange, fallbackTopProgress), 'fallback']; try { return [await generateFull(aiService, payload, suggestions, log, fullProgress), 'full']; } catch (error) { if (error.message !== '模型返回的目录数据格式无效') throw error; log('一次性生成完整目录失败,切换为分步生成模式。', fallbackNoticeProgress); return [await generateFallback(aiService, payload, suggestions, log, fallbackRange, fallbackTopProgress), 'fallback']; } } async function reviewOutline(aiService, payload, outline, log, progressLabel, progress = 82) { return collectJson(aiService, { messages: reviewOutlineMessages({ ...payload, outline }), temperature: 0.3, normalizer: normalizeReviewResponse, progressCallback: (message) => log(message, progress), progressLabel, failureMessage: '模型返回的审核结果格式无效', }); } async function reviewAlignedOutline(aiService, payload, groups, outline, log, progressLabel, progress = 82) { return collectJson(aiService, { messages: reviewAlignedOutlineMessages({ ...payload, groups, outline }), temperature: 0.3, normalizer: normalizeReviewResponse, progressCallback: (message) => log(message, progress), progressLabel, failureMessage: '模型返回的审核结果格式无效', }); } async function freeWorkflow(aiService, payload, log) { log('开始生成目录结构。', 8); const [first, generationMode] = await generateByMode(aiService, payload, 'auto', undefined, log); log('首次目录生成完成,开始审核目录质量。', 82); const firstReview = await reviewOutline(aiService, payload, first, log, '首次审核', 82); if (firstReview.passed) { log('目录审核通过,准备返回结果。', 96); return first; } const suggestions = firstReview.suggestions?.length ? firstReview.suggestions : ['请根据项目概述和技术评分要求补全目录覆盖范围,并修正不合理章节。']; log('目录审核未通过,正在根据修改建议重新生成。', 88); let second; try { [second] = await generateByMode(aiService, payload, generationMode, suggestions, log, { fullProgress: 90, fallbackNoticeProgress: 89, fallbackTopProgress: 90, fallbackRange: { start: 90, end: 96 }, }); } catch { log('根据审核建议重新生成失败,已回退到首次生成结果。', 97); return first; } log('二次生成完成,开始最终审核。', 97); const secondReview = await reviewOutline(aiService, payload, second, log, '最终审核', 97); log(secondReview.passed ? '最终审核通过,准备返回修正后的结果。' : '最终审核未完全通过,已返回修正后的第二次结果。', 98); return second; } async function extractRequirementGroups(aiService, requirements, suggestions, log) { const response = await collectJson(aiService, { messages: extractRequirementGroupsMessages(requirements, suggestions), temperature: 0.3, normalizer: normalizeRequirementGroupsResponse, validator: validateRequirementGroups, progressCallback: (message) => log(message, 10), progressLabel: '技术评分大类', failureMessage: '模型返回的技术评分大类格式无效', }); return response.groups || []; } async function generateAlignedChildrenForGroup(aiService, payload, parentItem, group, suggestions, log, progress) { const response = await collectJson(aiService, { messages: generateAlignedChildrenMessages({ ...payload, parentItem, group, suggestions }), temperature: 0.7, normalizer: (value) => normalizeChildrenResponse(value, new Set()), validator: validateChildrenOutline, progressCallback: (message) => log(message, progress), progressLabel: `章节 ${parentItem.title || '未命名章节'} 子目录`, failureMessage: '模型返回的目录数据格式无效', }); return response; } async function buildAligned(aiService, payload, groups, suggestions, log, progressRange = { start: 30, end: 75 }) { const top = buildTopLevelOutlineFromGroups(groups); validateAlignedTopLevelMapping(top, groups); const assembled = []; for (const [index, item] of top.entries()) { const progress = progressRange.start + Math.round((index / Math.max(top.length, 1)) * (progressRange.end - progressRange.start)); log(`正在生成第 ${index + 1}/${top.length} 个评分大类的二三级目录:${item.title || '未命名章节'}。`, progress); const childrenResponse = await generateAlignedChildrenForGroup(aiService, payload, item, groups[index], suggestions, log, progress); const children = childrenResponse.children || []; assembled.push({ ...item, ...(children.length ? { children } : {}) }); } log('评分项对齐目录生成完成,正在整理目录编号。', progressRange.end); const outline = normalizeOutlineResponse({ outline: renumber(assembled) }, new Set()); validateCompleteOutline(outline); validateAlignedTopLevelMapping(outline.outline || [], groups); return outline; } async function alignedWorkflow(aiService, payload, log) { log('开始提取技术评分大类。', 10); const groups = await extractRequirementGroups(aiService, payload.requirements, undefined, log); log('技术评分大类提取完成,正在构建一级目录。', 24); const first = await buildAligned(aiService, payload, groups, undefined, log, { start: 30, end: 75 }); log('目录生成完成,正在审核与技术评分项的对应关系。', 82); const firstReview = await reviewAlignedOutline(aiService, payload, groups, first, log, '首次审核', 82); if (firstReview.passed) { log('目录审核通过,准备返回结果。', 96); return first; } const suggestions = firstReview.suggestions?.length ? firstReview.suggestions : ['请保持一级目录与技术评分大类标题完全一致,并补全各大类下遗漏的评分细项。']; log('目录审核未通过,正在根据修改建议重新提取技术评分大类并重新生成目录。', 88); let revisedGroups = groups; let second; try { log('正在根据审核建议重新提取技术评分大类。', 90); revisedGroups = await extractRequirementGroups(aiService, payload.requirements, suggestions, log); second = await buildAligned(aiService, payload, revisedGroups, suggestions, log, { start: 91, end: 96 }); } catch { log('根据审核建议重新生成失败,已回退到首次生成结果。', 97); return first; } log('二次生成完成,开始最终审核。', 97); const secondReview = await reviewAlignedOutline(aiService, payload, revisedGroups, second, log, '最终审核', 97); log(secondReview.passed ? '最终审核通过,准备返回修正后的结果。' : '最终审核未完全通过,已返回修正后的第二次结果。', 98); return second; } async function enhanceOutlineWithKnowledgeAdditions(aiService, payload, outline, knowledgeItems, log) { if (!knowledgeItems.length) return outline; const outlineNodeMap = createOutlineNodeMap(outline.outline || []); const additionParents = collectKnowledgeAdditionParents(outline.outline || []); if (!additionParents.length) { log('当前目录没有可补充的二级目录,跳过参考知识库。', 98); return outline; } const rawAttempts = []; const normalizationStats = []; const isDeveloperMode = Boolean(aiService.isDeveloperMode?.()); const devLog = (message) => { if (isDeveloperMode) log(`[开发者] ${message}`, 98); }; log(`开始根据 ${knowledgeItems.length} 条知识库条目补充缺失三级目录。`, 98); devLog(`知识库补目录:可用二级父级 ${additionParents.length} 个,参考知识条目 ${knowledgeItems.length} 条。`); const patch = await collectJson(aiService, { messages: generateKnowledgeAdditionMessages({ ...payload, outline, knowledgeItems }), temperature: 0.3, normalizer: (value) => normalizeKnowledgeAdditionsResponse(value, { outline: outline.outline || [], outlineNodeMap, rawAttempts, normalizationStats, debugLog: devLog, }), validator: validateKnowledgeAdditionsResponse, repairMessagesBuilder: (context) => generateKnowledgeAdditionRepairMessages(context, additionParents), progressCallback: (message) => log(message, 98), progressLabel: '知识库补目录', failureMessage: '模型返回的知识库补目录格式无效', }); if (rawAttempts.length) { devLog(`模型原始返回尝试 ${rawAttempts.length} 次:${rawAttempts.map((item, index) => `#${index + 1} ${formatAdditionSummary(item)}`).join(';')}`); } const lastStats = normalizationStats[normalizationStats.length - 1] || { retained: patch.additions.length, dropped: 0, adjustedParent: 0 }; devLog(`程序归一:保留 ${lastStats.retained} 条,删除 ${lastStats.dropped} 条,自动改 parent ${lastStats.adjustedParent} 条。`); if (rawAttempts.length > 1) { devLog(`修复后:保留 ${lastStats.retained} 条。`); } const enhanced = applyKnowledgeAdditions(outline, patch); const additionCount = patch.additions.length; devLog(`最终应用:新增三级目录 ${additionCount} 个。`); if (!additionCount) { log('知识库未返回可补充三级目录,保留原目录。', 99); } else { log(`知识库补目录已应用:新增三级目录 ${additionCount} 个。`, 99); } return enhanced; } async function runOutlineGenerationTask({ aiService, workspaceStore, knowledgeBaseService, updateTask, payload }) { let logs = ['开始生成目录。']; let currentProgress = 5; function log(message, progress = currentProgress) { currentProgress = Math.max(currentProgress, Math.min(progress, 99)); logs = [...logs, message]; const technicalPlan = workspaceStore.updateTechnicalPlan({ outlineGenerationTask: updateTask({ status: 'running', progress: currentProgress, logs }) }); updateTask({ status: 'running', progress: currentProgress, logs }, technicalPlan); } const referenceKnowledgeDocumentIds = normalizeReferenceDocumentIds(payload); let technicalPlan = workspaceStore.updateTechnicalPlan({ outlineMode: payload.mode, referenceKnowledgeDocumentIds, outlineGenerationTask: updateTask({ status: 'running', progress: 5, logs }), }); updateTask({ status: 'running', progress: 5, logs }, technicalPlan); const taskPayload = { ...payload, reference_knowledge_document_ids: referenceKnowledgeDocumentIds, }; let outline = taskPayload.mode === 'aligned' ? await alignedWorkflow(aiService, taskPayload, log) : await freeWorkflow(aiService, taskPayload, log); const knowledgeItems = loadOutlineKnowledgeItems(knowledgeBaseService, referenceKnowledgeDocumentIds, log); outline = await enhanceOutlineWithKnowledgeAdditions(aiService, taskPayload, outline, knowledgeItems, log); technicalPlan = workspaceStore.updateTechnicalPlan({ outlineData: { ...outline, project_overview: payload.overview }, contentGenerationTask: undefined, contentGenerationSections: {}, contentGenerationPlans: {}, outlineGenerationTask: updateTask({ status: 'success', progress: 100, logs: [...logs, '目录生成完成。'] }), }); updateTask({ status: 'success', progress: 100, logs: [...logs, '目录生成完成。'] }, technicalPlan); } module.exports = { runOutlineGenerationTask };