docs(persona-chat): clarify auto-hook boundary #13
+13
-4
@@ -1985,7 +1985,12 @@ export const CN_WORDS = {
|
|||||||
賦能: "具體說讓誰能做到什麼", 閉環: "具體說從哪裡接到哪裡", 抓手: "切入點", 顆粒度: "細緻度",
|
賦能: "具體說讓誰能做到什麼", 閉環: "具體說從哪裡接到哪裡", 抓手: "切入點", 顆粒度: "細緻度",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}]/gu;
|
// 一個 emoji = 底字 + 變體選擇子/膚色 + ZWJ 接上去的後續。
|
||||||
|
// 以前 `\u{FE0F}` 直接寫在字元類裡,`❤️`(U+2764 + VS16)會被算成兩個而誤擋;
|
||||||
|
// 👨👩👦 這種 ZWJ 家族同理。
|
||||||
|
const EMOJI_BASE = "[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]";
|
||||||
|
const EMOJI_TAIL = "(?:[\\u{1F3FB}-\\u{1F3FF}]|\\u{FE0F}|\\u{20E3})*";
|
||||||
|
const EMOJI_RE = new RegExp(`${EMOJI_BASE}${EMOJI_TAIL}(?:\\u{200D}${EMOJI_BASE}${EMOJI_TAIL})*`, "gu");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 只留「真的說出口、而且是在使用而不是在討論」的部分。
|
* 只留「真的說出口、而且是在使用而不是在討論」的部分。
|
||||||
@@ -1993,11 +1998,13 @@ const EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}]/gu;
|
|||||||
* 引號裡的原話與 `code` 一律拿掉再比對——「我最近戒掉『賦能』這個詞」是在**提及**那個詞,
|
* 引號裡的原話與 `code` 一律拿掉再比對——「我最近戒掉『賦能』這個詞」是在**提及**那個詞,
|
||||||
* 不是在用它。這一條是 speak-human-tw 自己踩到的坑:它的文件裡出現「...」與彎引號,
|
* 不是在用它。這一條是 speak-human-tw 自己踩到的坑:它的文件裡出現「...」與彎引號,
|
||||||
* 正是因為那幾行在說「不要用這些」。
|
* 正是因為那幾行在說「不要用這些」。
|
||||||
|
*
|
||||||
|
* 書名號《》也算:「我在看《說到底》這本書」講的是書名,不是在說教。
|
||||||
*/
|
*/
|
||||||
function speechBody(text) {
|
function speechBody(text) {
|
||||||
return String(text ?? "")
|
return String(text ?? "")
|
||||||
.replace(/`[^`]*`/g, " ")
|
.replace(/`[^`]*`/g, " ")
|
||||||
.replace(/「[^」]*」|『[^』]*』|"[^"]*"/g, " ");
|
.replace(/「[^」]*」|『[^』]*』|《[^》]*》|"[^"]*"/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2015,14 +2022,16 @@ export function speechLint(text) {
|
|||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
sentences.forEach((sentence, index) => {
|
sentences.forEach((sentence, index) => {
|
||||||
const bare = sentence.replace(/[,、,;;::「」『』()()\s"'~~—-]/g, "");
|
const body = speechBody(sentence);
|
||||||
|
// 句長算的是「自己講的字」:引述使用者原話與 `code` 不該算進去,
|
||||||
|
// 不然「他跟我說『……(很長一段)』」必被擋,等於不准引述。
|
||||||
|
const bare = body.replace(/[,、,;;::「」『』《》()()\s"'~~—-]/g, "");
|
||||||
if ([...bare].length > MAX_SENTENCE_CHARS) {
|
if ([...bare].length > MAX_SENTENCE_CHARS) {
|
||||||
issues.push({ kind: "long", level: "block", chars: [...bare].length, text: sentence.slice(0, 30) });
|
issues.push({ kind: "long", level: "block", chars: [...bare].length, text: sentence.slice(0, 30) });
|
||||||
}
|
}
|
||||||
const head = sentence.replace(/^[「『((]+/, "");
|
const head = sentence.replace(/^[「『((]+/, "");
|
||||||
const opener = EXPLAINER_OPENERS.find((w) => head.startsWith(w));
|
const opener = EXPLAINER_OPENERS.find((w) => head.startsWith(w));
|
||||||
if (opener) issues.push({ kind: "explain", level: "block", opener, text: sentence.slice(0, 30) });
|
if (opener) issues.push({ kind: "explain", level: "block", opener, text: sentence.slice(0, 30) });
|
||||||
const body = speechBody(sentence);
|
|
||||||
const hedges = HEDGE_WORDS.filter((w) => body.includes(w));
|
const hedges = HEDGE_WORDS.filter((w) => body.includes(w));
|
||||||
if (hedges.length >= 2) {
|
if (hedges.length >= 2) {
|
||||||
issues.push({ kind: "hedge", level: "block", words: hedges, text: sentence.slice(0, 30) });
|
issues.push({ kind: "hedge", level: "block", words: hedges, text: sentence.slice(0, 30) });
|
||||||
|
|||||||
@@ -513,6 +513,11 @@ const SF_CASES = [
|
|||||||
["中國用語", "那個視頻我看完了。", "cn"],
|
["中國用語", "那個視頻我看完了。", "cn"],
|
||||||
["半形標點", "我看完了,你呢?", "halfwidth"],
|
["半形標點", "我看完了,你呢?", "halfwidth"],
|
||||||
["排版殘留", "**重點**:鐘修好了。", "markdown"],
|
["排版殘留", "**重點**:鐘修好了。", "markdown"],
|
||||||
|
// 放寬《》與句長之後,這幾條仍然要擋——豁免只針對「提及/引述」,不是整句放行
|
||||||
|
["書名號之外真的在說教", "我在看《時間簡史》。說到底,問題不在零件。", "preach"],
|
||||||
|
["自己講的長句(引號之外)", `我${"想跟你說一件從去年就一直放在心上沒講出口的事情".repeat(2)}。`, "long"],
|
||||||
|
["兩個 emoji", "修好了 🔧 開心 🎉", "emoji"],
|
||||||
|
["一個 emoji 加一個帶變體選擇子的", "修好了 🔧 給你 ❤️", "emoji"],
|
||||||
];
|
];
|
||||||
for (const [label, text, kind] of SF_CASES) {
|
for (const [label, text, kind] of SF_CASES) {
|
||||||
const issues = pl.speechLint(text);
|
const issues = pl.speechLint(text);
|
||||||
@@ -536,6 +541,16 @@ const SNF_CASES = [
|
|||||||
["一次「不是 A 而是 B」", "我要的不是最快,而是修得久。"],
|
["一次「不是 A 而是 B」", "我要的不是最快,而是修得久。"],
|
||||||
["一個破折號", "那台鐘——我留著。"],
|
["一個破折號", "那台鐘——我留著。"],
|
||||||
["日常短句", "桌上那杯茶冷了。要不要再泡一杯?"],
|
["日常短句", "桌上那杯茶冷了。要不要再泡一杯?"],
|
||||||
|
// 書名號也是「提及 ≠ 使用」:以前豁免清單漏了《》,這句被判 preach
|
||||||
|
["書名裡剛好有黑名單的詞", "我在看《說到底》這本書。"],
|
||||||
|
["書名裡的中國用語", "他推薦我看《質量與信息》。"],
|
||||||
|
// ❤️ 是 U+2764 + VS16;以前 EMOJI_RE 含 \u{FE0F},這一個字被算成兩個而誤擋
|
||||||
|
["帶變體選擇子的單一 emoji", "這個給你 ❤️"],
|
||||||
|
["ZWJ 組成的單一 emoji", "路上遇到他們 👨👩👦"],
|
||||||
|
["帶膚色的單一 emoji", "收到 👍🏽"],
|
||||||
|
// 句長以前算的是整句(含引號內容),引述使用者原話必被擋
|
||||||
|
["引述使用者的長原話", `他傳來「${"我今天真的很累而且不知道要從哪裡開始講起".repeat(2)}」,我看了兩次。`],
|
||||||
|
["引述長 code", `跑的是 \`${"x".repeat(80)}\` 這一行。`],
|
||||||
];
|
];
|
||||||
for (const [label, text] of SNF_CASES) {
|
for (const [label, text] of SNF_CASES) {
|
||||||
const issues = pl.speechBlockers(text);
|
const issues = pl.speechBlockers(text);
|
||||||
|
|||||||
Reference in New Issue
Block a user