ANSWER 后验:扫描所有引用,验证是否有效
(text: string)
| 115 | |
| 116 | /** ANSWER 后验:扫描所有引用,验证是否有效 */ |
| 117 | async function* validateAnswerRefs(text: string): AsyncGenerator<AgentEvent> { |
| 118 | const refs = collectRefs([text]); |
| 119 | if (refs.length === 0) return; |
| 120 | // 先把已读过的标为 valid,未读过的批量调 HTTP 验证 |
| 121 | const toCheck: string[] = []; |
| 122 | const resultMap = new Map<string, { ok: boolean; read: boolean }>(); |
| 123 | for (const r of refs) { |
| 124 | if (availableSources.has(r.ref.path)) { |
| 125 | resultMap.set(r.ref.path, { ok: true, read: true }); |
| 126 | } else { |
| 127 | toCheck.push(r.ref.path); |
| 128 | } |
| 129 | } |
| 130 | if (toCheck.length > 0) { |
| 131 | const exists = await validatePaths(toCheck); |
| 132 | for (const [p, ok] of exists) { |
| 133 | resultMap.set(p, { ok, read: false }); |
| 134 | } |
| 135 | } |
| 136 | const broken = refs |
| 137 | .filter((r) => { |
| 138 | const res = resultMap.get(r.ref.path); |
| 139 | return !res || !res.ok; |
| 140 | }) |
| 141 | .map((r) => r.ref.path); |
| 142 | const unread = refs |
| 143 | .filter((r) => { |
| 144 | const res = resultMap.get(r.ref.path); |
| 145 | return res && res.ok && !res.read; |
| 146 | }) |
| 147 | .map((r) => r.ref.path); |
| 148 | |
| 149 | // 块级 + 语义核对(基线,与 mode 无关):对每条块级引用 read_block 验锚点存在性, |
| 150 | // 再用 启发式→LLM判官 两级判断块内容是否支撑论断;不支撑的 key 进 downgraded, |
| 151 | // 由 UI(downgradeRefAnchors)去掉假精度、改成整页链接。 |
| 152 | // 只挑**块级**锚点(p/t/c/f-seq-hash):按锚点形态判别,兼容模型漏写 ^ 的 [[x#p-3-abc]] |
| 153 | // (isBlock 标志只看 ^,会漏);排除 h-(heading 指向整节,read_block 只回标题行,不宜按数字降级)。 |
| 154 | const BLOCK_ANCHOR_RE = /^[ptcf]-\d+(?:-\d+)?-[a-z0-9]/; |
| 155 | const blockRefs = refs |
| 156 | .filter((r) => !!r.ref.anchor && BLOCK_ANCHOR_RE.test(r.ref.anchor as string)) |
| 157 | .map((r) => ({ key: r.key, path: r.ref.path, anchor: r.ref.anchor as string })); |
| 158 | let downgraded: string[] = []; |
| 159 | if (blockRefs.length > 0) { |
| 160 | try { |
| 161 | ({ downgraded } = await verifyBlockCitations(text, blockRefs)); |
| 162 | } catch { |
| 163 | downgraded = []; |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | if (broken.length > 0 || unread.length > 0 || downgraded.length > 0) { |
| 168 | const segs: string[] = []; |
| 169 | if (broken.length) segs.push(`${broken.length} 条文件失效`); |
| 170 | if (unread.length) segs.push(`${unread.length} 条未读`); |
| 171 | if (downgraded.length) segs.push(`${downgraded.length} 条锚点不支撑论断→已降级`); |
| 172 | yield { |
| 173 | kind: "status", |
| 174 | text: `引用验证:${segs.join(" / ")}`, |
no test coverage detected