Skip to content

Commit e34ba4e

Browse files
authored
Merge pull request #69 from FB208/dev
适配龙猫新模型
2 parents 97586dd + 2f162f2 commit e34ba4e

3 files changed

Lines changed: 187 additions & 24 deletions

File tree

client/electron/services/outlineGenerationTask.cjs

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,50 @@ function formatOldOutlineForPrompt(oldOutline) {
1010
return typeof oldOutline === 'string' ? oldOutline : JSON.stringify(oldOutline, null, 2);
1111
}
1212

13+
function formatOutlineItemLabel(item, fallback = '未命名目录') {
14+
const id = String(item?.id || '').trim();
15+
const title = String(item?.title || '').trim() || fallback;
16+
return id ? `${id} ${title}` : title;
17+
}
18+
19+
function childrenOutlineJsonExample(parentId) {
20+
const id = String(parentId || '1').trim() || '1';
21+
return `{
22+
"children": [
23+
{
24+
"id": "${id}.1",
25+
"title": "二级目录标题",
26+
"description": "二级目录说明",
27+
"children": [
28+
{
29+
"id": "${id}.1.1",
30+
"title": "三级目录标题",
31+
"description": "三级目录说明"
32+
},
33+
{
34+
"id": "${id}.1.2",
35+
"title": "三级目录标题",
36+
"description": "三级目录说明"
37+
}
38+
]
39+
}
40+
]
41+
}`;
42+
}
43+
44+
function childrenOutlineStructureRules(parentId) {
45+
const id = String(parentId || '1').trim() || '1';
46+
return `结构要求:
47+
1. 顶层 children 只能放当前一级目录的直接子目录,也就是二级目录。
48+
2. 每个二级目录都必须包含非空 children 数组,children 内是三级目录。
49+
3. 不要把评分细项直接作为没有子节点的二级目录;应先归纳二级主题,再在其下展开三级响应要点、实施措施、证明材料或验收标准。
50+
4. 三级目录只包含 id、title、description,不要继续包含 children。
51+
5. 编号必须以当前一级目录编号 ${id} 为前缀,例如二级 ${id}.1,三级 ${id}.1.1。
52+
53+
返回示例:
54+
${childrenOutlineJsonExample(id)}`;
55+
}
56+
1357
const KNOWLEDGE_RESUME_MAX_CHARS = 220;
1458
const MAX_KNOWLEDGE_ADDITIONS = 30;
1559

@@ -277,10 +321,10 @@ function generateAlignedChildrenMessages({ overview, requirements, parentItem, g
277321
1. 一级目录标题和顺序已经固定,不能修改、重命名、合并或删除一级目录
278322
2. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身
279323
3. 二级和三级目录要覆盖当前技术评分大类及其细项,不能越界写入其他评分大类内容
280-
4. 返回标准 JSON,格式为 {"children": [...]},children 中只能包含当前一级目录的直接子目录
281-
5. 每个节点必须包含 id、title、description,三级目录继续使用 children 字段
282-
6. 章节编号必须以给定的一级目录编号为前缀,例如父级是 2,则二级目录编号从 2.1 开始,三级目录编号从 2.1.1 开始
283-
7. 除了 JSON 结果外,不要输出任何其他内容`;
324+
4. 返回标准 JSON,格式为 {"children": [...]},每个节点必须包含 id、title、description
325+
5. 除了 JSON 结果外,不要输出任何其他内容
326+
327+
${childrenOutlineStructureRules(parentItem.id)}`;
284328
const messages = [
285329
{ role: 'system', content: systemPrompt },
286330
{ role: 'user', content: `项目概述:\n${overview}` },
@@ -290,9 +334,9 @@ function generateAlignedChildrenMessages({ overview, requirements, parentItem, g
290334
];
291335
if (formattedOldOutline) {
292336
messages.push({ role: 'user', content: `用户自己编写的目录参考:\n${formattedOldOutline}` });
293-
messages.push({ role: 'user', content: `请在覆盖当前技术评分大类细项的前提下,参考用户目录优化当前一级目录下的二级、三级目录,但不得修改当前一级目录标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
337+
messages.push({ role: 'user', content: `请在覆盖当前技术评分大类细项的前提下,参考用户目录优化当前一级目录下的二级、三级目录;每个二级目录必须包含三级目录,不得修改当前一级目录标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
294338
} else {
295-
messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,一级目录标题必须保持为当前给定标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
339+
messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录;每个二级目录必须包含三级目录,一级目录标题必须保持为当前给定标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
296340
}
297341
return messages;
298342
}
@@ -303,11 +347,10 @@ function generateChildrenMessages({ overview, requirements, parentItem, oldOutli
303347
304348
要求:
305349
1. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身
306-
2. 返回标准 JSON,格式为 {"children": [...]}
307-
3. children 中只能包含当前一级目录的直接子目录,每个节点必须包含 id、title、description
308-
4. 二级目录下如有三级目录,同样使用 children 字段
309-
5. 章节编号必须以给定的一级目录编号为前缀,例如父级是 2,则二级目录编号从 2.1 开始,三级目录编号从 2.1.1 开始
310-
6. 除了 JSON 结果外,不要输出任何其他内容`;
350+
2. 返回标准 JSON,格式为 {"children": [...]},每个节点必须包含 id、title、description
351+
3. 除了 JSON 结果外,不要输出任何其他内容
352+
353+
${childrenOutlineStructureRules(parentItem.id)}`;
311354
const messages = [
312355
{ role: 'system', content: systemPrompt },
313356
{ role: 'user', content: `项目概述:\n${overview}` },
@@ -316,13 +359,53 @@ function generateChildrenMessages({ overview, requirements, parentItem, oldOutli
316359
];
317360
if (formattedOldOutline) {
318361
messages.push({ role: 'user', content: `用户自己编写的目录:\n${formattedOldOutline}` });
319-
messages.push({ role: 'user', content: `请在满足技术评分要求的前提下,充分结合用户自己编写的目录,仅生成该一级目录下的二级、三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
362+
messages.push({ role: 'user', content: `请在满足技术评分要求的前提下,充分结合用户自己编写的目录,仅生成该一级目录下的二级、三级目录;每个二级目录必须包含三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
320363
} else {
321-
messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
364+
messages.push({ role: 'user', content: `请仅生成该一级目录下的二级、三级目录;每个二级目录必须包含三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` });
322365
}
323366
return messages;
324367
}
325368

369+
function generateChildrenStructureRepairMessages({ invalidContent, issues }, parentItem, group) {
370+
const detailLines = (group?.detail_points || [])
371+
.filter((item) => typeof item === 'string' && item.trim())
372+
.map((item) => `- ${item}`)
373+
.join('\n');
374+
const groupBlock = group ? `
375+
当前对应的技术评分大类:
376+
requirement_id:${group.requirement_id || ''}
377+
标题:${group.title || ''}
378+
描述:${group.description || ''}
379+
细项:
380+
${detailLines || '- 未提供明确细项'}` : '';
381+
return [
382+
{
383+
role: 'user',
384+
content: `你是一个严格的 JSON 修复器。请把模型输出修复为“当前一级目录下的二级和三级目录”JSON。
385+
386+
必须满足:
387+
1. 顶层只能有 children 数组,不要输出一级目录本身
388+
2. 顶层 children 是二级目录,每个二级目录都必须包含非空 children 数组
389+
3. 二级目录的 children 内是三级目录,三级目录只包含 id、title、description,不要继续包含 children
390+
4. 优先保留原结果中的二级目录标题、说明和顺序,只在每个二级目录下补齐合理三级目录
391+
5. 不要把评分细项直接作为没有子节点的二级目录
392+
6. 只返回 JSON,不要输出解释文字
393+
394+
${childrenOutlineStructureRules(parentItem?.id)}`,
395+
},
396+
{ role: 'user', content: `当前一级目录:
397+
编号:${parentItem?.id || ''}
398+
标题:${parentItem?.title || ''}
399+
描述:${parentItem?.description || ''}${groupBlock}` },
400+
{ role: 'user', content: `错误列表:
401+
${(issues || []).map((item, index) => `${index + 1}. ${item}`).join('\n')}` },
402+
{ role: 'user', content: `待修复内容:
403+
\`\`\`json
404+
${String(invalidContent || '').slice(0, 60000)}
405+
\`\`\`` },
406+
];
407+
}
408+
326409
function reviewOutlineMessages({ overview, requirements, outline }) {
327410
const systemPrompt = `你是一个严格的招标文件目录审核专家。请审核目录是否符合项目概述和技术评分要求。
328411
@@ -695,18 +778,34 @@ function outlineDepth(items) {
695778
return items?.length ? 1 + Math.max(...items.map((item) => outlineDepth(item.children || []))) : 0;
696779
}
697780

781+
function formatMissingOutlineLabels(items, limit = 8) {
782+
const labels = (items || []).map((item, index) => formatOutlineItemLabel(item, `第 ${index + 1} 个目录`));
783+
const visible = labels.slice(0, limit).join('、');
784+
return labels.length > limit ? `${visible}${labels.length} 个目录` : visible;
785+
}
786+
698787
function validateCompleteOutline(payload) {
699788
const outline = payload.outline || [];
700789
if (!outline.length) throw new Error('目录不能为空');
701790
if (outlineDepth(outline) < 3) throw new Error('完整目录至少需要三级结构');
791+
const shallowItems = outline.filter((item) => outlineDepth([item]) < 3);
792+
if (shallowItems.length) {
793+
throw new Error(`完整目录至少需要三级结构,以下一级目录缺少三级目录:${formatMissingOutlineLabels(shallowItems)}`);
794+
}
702795
}
703796

704797
function validateTopLevelOutline(payload) {
705798
if (!(payload.outline || []).length) throw new Error('一级目录不能为空');
706799
}
707800

708801
function validateChildrenOutline(payload) {
709-
if (!(payload.children || []).length) throw new Error('二级目录不能为空');
802+
const children = payload.children || [];
803+
if (!children.length) throw new Error('二级目录不能为空');
804+
const secondLevelWithoutThird = children.filter((item) => !(item.children || []).length);
805+
if (secondLevelWithoutThird.length) {
806+
throw new Error(`二级目录必须包含三级目录,缺失三级目录:${formatMissingOutlineLabels(secondLevelWithoutThird)}`);
807+
}
808+
if (outlineDepth(children) < 2) throw new Error('二级目录必须包含三级目录');
710809
}
711810

712811
function validateRequirementGroups(payload) {
@@ -861,6 +960,7 @@ async function generateChildren(aiService, payload, parentItem, suggestions, log
861960
temperature: 0.7,
862961
normalizer: (value) => normalizeChildrenResponse(value, new Set()),
863962
validator: validateChildrenOutline,
963+
repairMessagesBuilder: (context) => generateChildrenStructureRepairMessages(context, parentItem),
864964
progressCallback: (message) => log(message, progress),
865965
progressLabel: `章节 ${parentItem.title || '未命名章节'} 子目录`,
866966
failureMessage: '模型返回的目录数据格式无效',
@@ -973,6 +1073,7 @@ async function generateAlignedChildrenForGroup(aiService, payload, parentItem, g
9731073
temperature: 0.7,
9741074
normalizer: (value) => normalizeChildrenResponse(value, new Set()),
9751075
validator: validateChildrenOutline,
1076+
repairMessagesBuilder: (context) => generateChildrenStructureRepairMessages(context, parentItem, group),
9761077
progressCallback: (message) => log(message, progress),
9771078
progressLabel: `章节 ${parentItem.title || '未命名章节'} 子目录`,
9781079
failureMessage: '模型返回的目录数据格式无效',

client/src/features/technical-plan/services/outlineWorkflow.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ function outlineDepth(items: OutlineItem[]): number {
4545
return 1 + Math.max(...items.map((item) => outlineDepth(item.children || [])));
4646
}
4747

48+
function formatOutlineItemLabel(item: OutlineItem, fallback: string) {
49+
return item.id ? `${item.id} ${item.title || fallback}` : item.title || fallback;
50+
}
51+
52+
function formatMissingOutlineLabels(items: OutlineItem[], limit = 8) {
53+
const labels = items.map((item, index) => formatOutlineItemLabel(item, `第 ${index + 1} 个目录`));
54+
const visible = labels.slice(0, limit).join('、');
55+
return labels.length > limit ? `${visible}${labels.length} 个目录` : visible;
56+
}
57+
4858
function validateOutline(outline: OutlineData) {
4959
if (!outline.outline?.length) {
5060
throw new Error('目录不能为空');
@@ -53,6 +63,11 @@ function validateOutline(outline: OutlineData) {
5363
if (outlineDepth(outline.outline) < 3) {
5464
throw new Error('完整目录至少需要三级结构');
5565
}
66+
67+
const shallowItems = outline.outline.filter((item) => outlineDepth([item]) < 3);
68+
if (shallowItems.length) {
69+
throw new Error(`完整目录至少需要三级结构,以下一级目录缺少三级目录:${formatMissingOutlineLabels(shallowItems)}`);
70+
}
5671
}
5772

5873
function validateTopLevelOutline(outline: OutlineData) {
@@ -65,6 +80,15 @@ function validateChildren(payload: ChildrenResponse) {
6580
if (!payload.children?.length) {
6681
throw new Error('二级目录不能为空');
6782
}
83+
84+
const secondLevelWithoutThird = payload.children.filter((item) => !item.children?.length);
85+
if (secondLevelWithoutThird.length) {
86+
throw new Error(`二级目录必须包含三级目录,缺失三级目录:${formatMissingOutlineLabels(secondLevelWithoutThird)}`);
87+
}
88+
89+
if (outlineDepth(payload.children) < 2) {
90+
throw new Error('二级目录必须包含三级目录');
91+
}
6892
}
6993

7094
function renumberItems(items: OutlineItem[], parentPrefix = ''): OutlineItem[] {

client/src/shared/prompts/outlinePrompts.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,44 @@ function formatSuggestions(suggestions?: string[]) {
2020
return `\n\n本轮修正建议:\n${suggestions.map((item, index) => `${index + 1}. ${item}`).join('\n')}`;
2121
}
2222

23+
function childrenOutlineJsonExample(parentId?: string) {
24+
const id = String(parentId || '1').trim() || '1';
25+
return `{
26+
"children": [
27+
{
28+
"id": "${id}.1",
29+
"title": "二级目录标题",
30+
"description": "二级目录说明",
31+
"children": [
32+
{
33+
"id": "${id}.1.1",
34+
"title": "三级目录标题",
35+
"description": "三级目录说明"
36+
},
37+
{
38+
"id": "${id}.1.2",
39+
"title": "三级目录标题",
40+
"description": "三级目录说明"
41+
}
42+
]
43+
}
44+
]
45+
}`;
46+
}
47+
48+
function childrenOutlineStructureRules(parentId?: string) {
49+
const id = String(parentId || '1').trim() || '1';
50+
return `结构要求:
51+
1. 顶层 children 只能放当前一级目录的直接子目录,也就是二级目录。
52+
2. 每个二级目录都必须包含非空 children 数组,children 内是三级目录。
53+
3. 不要把评分细项直接作为没有子节点的二级目录;应先归纳二级主题,再在其下展开三级响应要点、实施措施、证明材料或验收标准。
54+
4. 三级目录只包含 id、title、description,不要继续包含 children。
55+
5. 编号必须以当前一级目录编号 ${id} 为前缀,例如二级 ${id}.1,三级 ${id}.1.1。
56+
57+
返回示例:
58+
${childrenOutlineJsonExample(id)}`;
59+
}
60+
2361
function outlineSystemPrompt() {
2462
return `你是一个专业的标书编写专家。根据提供的项目概述和技术评分要求,生成投标文件中技术标部分的目录结构。
2563
如果用户提供了自己编写的目录,你要保证目录满足技术评分要求,并充分结合用户自己编写的目录。
@@ -131,17 +169,16 @@ export function buildChildrenOutlineMessages({ overview, requirements, parentIte
131169
132170
要求:
133171
1. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身。
134-
2. 返回标准 JSON,格式为 {"children": [...]}。
135-
3. children 中只能包含当前一级目录的直接子目录,每个节点必须包含 id、title、description。
136-
4. 二级目录下如有三级目录,同样使用 children 字段。
137-
5. 章节编号必须以给定的一级目录编号为前缀,例如父级是 2,则二级目录编号从 2.1 开始,三级目录编号从 2.1.1 开始。
138-
6. 只返回 JSON,不要输出其他内容。`,
172+
2. 返回标准 JSON,格式为 {"children": [...]},每个节点必须包含 id、title、description。
173+
3. 只返回 JSON,不要输出其他内容。
174+
175+
${childrenOutlineStructureRules(parentId)}`,
139176
},
140177
{ role: 'user', content: `项目概述:\n${overview}` },
141178
{ role: 'user', content: `技术评分要求:\n${requirements}` },
142179
...(oldOutline ? [{ role: 'user' as const, content: `用户自己编写的目录:\n${oldOutline}` }] : []),
143180
{ role: 'user', content: `当前一级目录:\n编号:${parentId}\n标题:${parentTitle}\n描述:${parentDescription}` },
144-
{ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` },
181+
{ role: 'user', content: `请仅生成该一级目录下的二级、三级目录;每个二级目录必须包含三级目录,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` },
145182
];
146183
}
147184

@@ -157,16 +194,17 @@ export function buildAlignedChildrenOutlineMessages({ overview, requirements, pa
157194
1. 一级目录标题和顺序已经固定,不能修改、重命名、合并或删除一级目录。
158195
2. 只输出当前一级目录下的二级和三级目录,不要重复输出一级目录本身。
159196
3. 二级和三级目录要覆盖当前技术评分大类及其细项,不能越界写入其他评分大类内容。
160-
4. 返回标准 JSON,格式为 {"children": [...]}。
161-
5. 章节编号必须以给定的一级目录编号为前缀。
162-
6. 只返回 JSON,不要输出其他内容。`,
197+
4. 返回标准 JSON,格式为 {"children": [...]},每个节点必须包含 id、title、description。
198+
5. 只返回 JSON,不要输出其他内容。
199+
200+
${childrenOutlineStructureRules(parentItem.id)}`,
163201
},
164202
{ role: 'user', content: `项目概述:\n${overview}` },
165203
{ role: 'user', content: `技术评分要求原文:\n${requirements}` },
166204
...(oldOutline ? [{ role: 'user' as const, content: `用户自己编写的目录参考:\n${oldOutline}` }] : []),
167205
{ role: 'user', content: `当前固定一级目录:\n编号:${parentItem.id}\n标题:${parentItem.title}\n描述:${parentItem.description}` },
168206
{ role: 'user', content: `当前对应的技术评分大类:\nrequirement_id:${requirementGroup?.requirement_id || ''}\n标题:${requirementGroup?.title || ''}\n描述:${requirementGroup?.description || ''}\n细项:\n${detailPoints}` },
169-
{ role: 'user', content: `请仅生成该一级目录下的二级、三级目录,一级目录标题必须保持为当前给定标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` },
207+
{ role: 'user', content: `请仅生成该一级目录下的二级、三级目录;每个二级目录必须包含三级目录,一级目录标题必须保持为当前给定标题,返回格式必须是 {"children": [...]}。${formatSuggestions(suggestions)}` },
170208
];
171209
}
172210

0 commit comments

Comments
 (0)