Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | /**
* 写作指令一致性校验器
*
* 解决 issue:rich-outline 给每个节点分配了独立的 writingInstructions,
* 但没有检查不同节点的指令是否矛盾或重复。
*
* 在大纲生成后,对相邻/相关节点的写作指令做交叉校验:
* - 检测重复覆盖范围(两个节点都要覆盖相同内容)
* - 检测矛盾指令(一个要"简洁"一个要"详细展开")
* - 检测缺失衔接(上一章"引出"的内容下一章没有"承接")
*/
export interface InstructionNode {
number: number;
title: string;
writingInstructions?: {
opening?: string; // 开篇方式
structure?: string; // 结构建议
mustCover?: string[]; // 必覆盖内容
avoidRepeat?: string[]; // 避免重复的内容
keyTakeaway?: string; // 核心收获
};
}
export interface ConsistencyWarning {
type: 'duplicate_coverage' | 'contradictory_direction' | 'missing_bridge' | 'out_of_order';
severity: 'high' | 'medium' | 'low';
nodeA: { number: number; title: string };
nodeB: { number: number; title: string };
description: string;
}
/**
* 校验相邻章节的写作指令是否一致
*/
export function validateInstructionConsistency(
nodes: InstructionNode[]
): ConsistencyWarning[] {
const warnings: ConsistencyWarning[] = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const next = nodes[i + 1];
if (!node.writingInstructions) continue;
const mustCover = node.writingInstructions.mustCover || [];
const avoidRepeat = node.writingInstructions.avoidRepeat || [];
const opening = node.writingInstructions.opening || '';
// 检查与下一章的关系
if (next?.writingInstructions) {
const nextMustCover = next.writingInstructions.mustCover || [];
const nextAvoidRepeat = next.writingInstructions.avoidRepeat || [];
// 1) 检查重复覆盖
const overlap = mustCover.filter(item =>
nextMustCover.some(ni => similarity(item, ni) > 0.7)
);
if (overlap.length > 0) {
warnings.push({
type: 'duplicate_coverage',
severity: 'medium',
nodeA: { number: node.number, title: node.title },
nodeB: { number: next.number, title: next.title },
description: `两章均需覆盖: ${overlap.join('、')},建议明确分工避免内容重复`,
});
}
// 2) 检查矛盾方向
if (
(opening.includes('回顾') || opening.includes('总结')) &&
(next.writingInstructions.opening?.includes('引入新') || next.writingInstructions.opening?.includes('开启新'))
) {
// 正常:前一章总结,下一章开新 = OK
} else if (
(opening.includes('引出') || opening.includes('铺垫')) &&
!nextMustCover.some(item =>
mustCover.some(pi => similarity(item, pi) > 0.5)
) &&
!next.writingInstructions.opening?.includes('承接')
) {
warnings.push({
type: 'missing_bridge',
severity: 'medium',
nodeA: { number: node.number, title: node.title },
nodeB: { number: next.number, title: next.title },
description: `第${node.number}章"引出/铺垫"的内容在第${next.number}章中未见"承接",可能产生衔接断裂`,
});
}
// 3) 检查结构风格矛盾
const nodeStructure = node.writingInstructions.structure || '';
const nextStructure = next.writingInstructions.structure || '';
const conciseWords = ['简洁', '精炼', '扼要', '概括'];
const detailedWords = ['详细', '深入', '全面', '展开'];
const nodeIsConcise = conciseWords.some(w => nodeStructure.includes(w));
const nextIsDetailed = detailedWords.some(w => nextStructure.includes(w));
// 不矛盾(相邻章节可以不同风格),只检查明确矛盾
if (
(nodeIsConcise && nextStructure.includes('简洁')) ||
(nextIsDetailed && nodeStructure.includes('详细'))
) {
// 都简洁或都详细 = OK
}
}
// 4) 检查 mustCover 和 avoidRepeat 内部分歧
const internalOverlap = mustCover.filter(item =>
avoidRepeat.some(ar => similarity(item, ar) > 0.6)
);
if (internalOverlap.length > 0) {
warnings.push({
type: 'contradictory_direction',
severity: 'high',
nodeA: { number: node.number, title: node.title },
nodeB: { number: node.number, title: node.title },
description: `第${node.number}章自身矛盾:既要覆盖"${internalOverlap[0]}"又要避免重复,指令冲突`,
});
}
}
return warnings;
}
/**
* 简单字符串相似度(Jaccard 字符级)
*/
function similarity(a: string, b: string): number {
const aChars = new Set(a.replace(/\s/g, ''));
const bChars = new Set(b.replace(/\s/g, ''));
const intersection = new Set([...aChars].filter(c => bChars.has(c)));
const union = new Set([...aChars, ...bChars]);
return union.size === 0 ? 0 : intersection.size / union.size;
}
|