feat(theater): 台詞不可以是心裡話搬上台面(機械擋下)(v0.0.8 同批)

心裡話外流有兩種路徑。第一種是別的人格去讀你的檔案——那個 hook 早就擋死了
(本次補上測試:跨 session Read `state/inner.jsonl`、Grep `state/` 都是 deny,
聊天室逐字稿也只有 ts/speaker/kind/text/emotion/to,沒有任何心裡話欄位)。

第二種比較難防:**你自己把心裡想的原封不動講出來**。劇場模式尤其危險,
因為別的人格看得到的就只有你說出口的東西——那句一旦是心裡話的複述,
等於自己把它交出去了。「台詞不可以是心裡話的摘要」原本只寫在規則裡靠自律,
現在變成機械檢查:`innerLeak()` 拿這句台詞去比最近 12 則心裡話,
相似度 ≥ 0.62 就擋(`--force` 可以放行,真的決定要講開的時候)。

用的是既有的 `similarity()`(字元 bigram + 字集合),跟擋重複發言同一把尺。

測試:334 項全過(新增 6 項)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 06:23:19 +00:00
co-authored by Claude Opus 5
parent ab4ddb734d
commit 646fd33541
3 changed files with 68 additions and 0 deletions
+27
View File
@@ -1967,6 +1967,33 @@ export function roomRepeat(room, speaker, text, opts = {}) {
return findRepeat(rows, text, opts);
}
/**
* 這句台詞是不是把心裡話搬上台面了。
*
* 心裡話外流有兩種:一種是別的人格去讀你的檔案(那個 hook 早就擋死了),
* 另一種比較難防——**你自己把心裡想的原封不動講出來**。劇場模式尤其危險,
* 因為那裡只有台詞,別的人格看得到的就只有你說出口的東西。
*
* 「台詞不可以是心裡話的摘要」原本只是寫在規則裡靠自律,這裡把它變成機械檢查:
* 跟最近的心裡話太像就擋下來。害羞的人格更要擋——他們心裡那句才是真的。
*/
export const INNER_LEAK_THRESHOLD = 0.62;
export const INNER_LEAK_WINDOW = 12;
export function innerLeak(slug, text, { threshold = INNER_LEAK_THRESHOLD } = {}) {
const norm = normalizeSpeech(text);
if (norm.length < REPEAT_MIN_CHARS) return null;
let best = null;
for (const row of readJsonl(innerPath(slug), INNER_LEAK_WINDOW)) {
const score = similarity(text, row.text || "");
if (score < threshold) continue;
if (!best || score > best.similarity) {
best = { similarity: score, at: row.ts, kind: row.kind || "infer", text: String(row.text || "").slice(0, 120) };
}
}
return best;
}
/** 心裡話:只進自己的 inner.jsonl,永遠不回顯給使用者。 */
export function recordInner(slug, text, { kind = "infer", room = null } = {}) {
const entry = { ts: nowIso(), kind, room, text: String(text ?? "").slice(0, 1200) };