fix(citations): 识别全角参考文献角标(全角数字/合并/区间)
正文里的引用角标识别此前全角括号只支持单个半角数字 【1】, 导致 【1】(全角数字)、【1,2】/【1,2】/【1、2】(合并)、 【1-3】/【2~5】(区间) 等中文常见写法无法识别成参考文献。 补齐全角括号的「全角数字 / 合并 / 区间」三种形态,转成半角后交给 原有半角逻辑统一处理;仍保留 isValidIndex 闸门,避免把正文里的 【3】(越界)、【附录】(非数字) 误判为引用。新增 inline-citations.test.ts。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b5e682cfd2
commit
3e68260afa
2 files changed
+129
-3
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { applyInlineCitations, type RuntimeDisplaySource } from "./runtime";
|
||||
|
||||
function makeSources(count: number): RuntimeDisplaySource[] {
|
||||
return Array.from({ length: count }, (_unused, position) => ({
|
||||
index: position + 1,
|
||||
skillName: "knowledge-base-search",
|
||||
mode: "citation",
|
||||
title: `参考文献 ${position + 1}`,
|
||||
type: "result",
|
||||
snippet: "",
|
||||
raw: {},
|
||||
}));
|
||||
}
|
||||
|
||||
function citation(index: number): string {
|
||||
return `[citation:${index}](#reference-${index})`;
|
||||
}
|
||||
|
||||
test("全角单编号 【1】【2】 解析为引用徽章", () => {
|
||||
const out = applyInlineCitations("结论见【1】【2】。", makeSources(2));
|
||||
assert.equal(out.includes(citation(1)), true);
|
||||
assert.equal(out.includes(citation(2)), true);
|
||||
assert.equal(out.includes("【1】"), false);
|
||||
assert.equal(out.includes("【2】"), false);
|
||||
});
|
||||
|
||||
test("全角数字 【1】 也能识别", () => {
|
||||
const out = applyInlineCitations("参考【1】和【2】。", makeSources(2));
|
||||
assert.equal(out.includes(citation(1)), true);
|
||||
assert.equal(out.includes(citation(2)), true);
|
||||
});
|
||||
|
||||
test("全角括号合并编号 【1,2】/【1,2】/【1、2】 拆成多个引用", () => {
|
||||
for (const marker of ["【1,2】", "【1,2】", "【1、2】"]) {
|
||||
const out = applyInlineCitations(`见${marker}。`, makeSources(2));
|
||||
assert.equal(out.includes(citation(1)), true, marker);
|
||||
assert.equal(out.includes(citation(2)), true, marker);
|
||||
}
|
||||
});
|
||||
|
||||
test("全角区间编号 【1-3】/【2~5】 展开为连续引用", () => {
|
||||
const dash = applyInlineCitations("见【1-3】。", makeSources(3));
|
||||
assert.equal(dash.includes(citation(1)), true);
|
||||
assert.equal(dash.includes(citation(2)), true);
|
||||
assert.equal(dash.includes(citation(3)), true);
|
||||
|
||||
const tilde = applyInlineCitations("见【2~4】。", makeSources(4));
|
||||
assert.equal(tilde.includes(citation(2)), true);
|
||||
assert.equal(tilde.includes(citation(3)), true);
|
||||
assert.equal(tilde.includes(citation(4)), true);
|
||||
});
|
||||
|
||||
test("超出来源范围的全角编号保持原样,不误判正文", () => {
|
||||
// 只有 1 条来源,【3】 没有对应来源 → 不应转换
|
||||
const out = applyInlineCitations("正文【3】不是引用。", makeSources(1));
|
||||
assert.equal(out.includes("【3】"), true);
|
||||
assert.equal(out.includes("#reference-3"), false);
|
||||
});
|
||||
|
||||
test("非数字的全角括号不被改写", () => {
|
||||
const out = applyInlineCitations("见【附录】小节。", makeSources(2));
|
||||
assert.equal(out.includes("【附录】"), true);
|
||||
});
|
||||
|
||||
test("半角写法仍然正常工作", () => {
|
||||
const out = applyInlineCitations("见[1][2]和[1,3]。", makeSources(3));
|
||||
assert.equal(out.includes(citation(1)), true);
|
||||
assert.equal(out.includes(citation(2)), true);
|
||||
assert.equal(out.includes(citation(3)), true);
|
||||
});
|
||||
@@ -623,9 +623,62 @@ function normalizeLooseCitationTokens(
|
||||
return indexes.map((index) => `[${index}]`).join("");
|
||||
};
|
||||
|
||||
// 全角括号写法:【1】→ [1]
|
||||
let next = content.replace(/【\s*(\d{1,3})\s*】/g, (match, rawIndex: string) =>
|
||||
isValidIndex(Number(rawIndex)) ? `[${rawIndex}]` : match,
|
||||
// 全角数字转半角:123 → 123(仅用于括号内的编号识别)
|
||||
const toAsciiDigits = (value: string) =>
|
||||
value.replace(/[0-9]/g, (digit) =>
|
||||
String.fromCharCode(digit.charCodeAt(0) - 0xfee0),
|
||||
);
|
||||
|
||||
// 全角括号写法(含全角数字):
|
||||
// 【1】/【1】 → [1]
|
||||
// 【1,2】/【1,2】/【1、2】 → [1][2]
|
||||
// 【1-3】/【2~5】 → [1][2][3]
|
||||
// 三种形态互斥(单编号内不含分隔符),且都要求编号能对应到参考来源,
|
||||
// 避免把正文里的【…】误判成引用。转成半角后交给下面的半角逻辑统一处理。
|
||||
|
||||
// 全角单编号:【1】/【1】
|
||||
let next = content.replace(
|
||||
/【\s*([0-90-9]{1,3})\s*】/g,
|
||||
(match, rawIndex: string) => {
|
||||
const index = Number(toAsciiDigits(rawIndex));
|
||||
return isValidIndex(index) ? `[${index}]` : match;
|
||||
},
|
||||
);
|
||||
|
||||
// 全角合并编号:【1,2】/【1,2】/【1、2】
|
||||
next = next.replace(
|
||||
/【\s*([0-90-9]{1,3}(?:\s*[,,、]\s*[0-90-9]{1,3})+)\s*】/g,
|
||||
(match, rawList: string) => {
|
||||
const indexes = rawList
|
||||
.split(/[,,、]/)
|
||||
.map((part) => Number(toAsciiDigits(part.trim())));
|
||||
if (indexes.some((index) => !isValidIndex(index))) {
|
||||
return match;
|
||||
}
|
||||
return indexes.map((index) => `[${index}]`).join("");
|
||||
},
|
||||
);
|
||||
|
||||
// 全角区间编号:【1-3】/【2~5】
|
||||
next = next.replace(
|
||||
/【\s*([0-90-9]{1,3})\s*[-–~~]\s*([0-90-9]{1,3})\s*】/g,
|
||||
(match, rawStart: string, rawEnd: string) => {
|
||||
const start = Number(toAsciiDigits(rawStart));
|
||||
const end = Number(toAsciiDigits(rawEnd));
|
||||
if (
|
||||
end <= start ||
|
||||
end - start > 20 ||
|
||||
!isValidIndex(start) ||
|
||||
!isValidIndex(end)
|
||||
) {
|
||||
return match;
|
||||
}
|
||||
const tokens: string[] = [];
|
||||
for (let index = start; index <= end; index++) {
|
||||
tokens.push(`[${index}]`);
|
||||
}
|
||||
return tokens.join("");
|
||||
},
|
||||
);
|
||||
|
||||
// 合并写法:[1,2,3] / [1,2] / [1、2] → [1][2][3]
|
||||
|
||||
Reference in new issue
Block a user