v0.0.8:情緒處理四層(情緒先行/怎麼接/走向/飽和)+ R6 容量壓力的真 bug #11
@@ -1967,6 +1967,33 @@ export function roomRepeat(room, speaker, text, opts = {}) {
|
|||||||
return findRepeat(rows, 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,永遠不回顯給使用者。 */
|
/** 心裡話:只進自己的 inner.jsonl,永遠不回顯給使用者。 */
|
||||||
export function recordInner(slug, text, { kind = "infer", room = null } = {}) {
|
export function recordInner(slug, text, { kind = "infer", room = null } = {}) {
|
||||||
const entry = { ts: nowIso(), kind, room, text: String(text ?? "").slice(0, 1200) };
|
const entry = { ts: nowIso(), kind, room, text: String(text ?? "").slice(0, 1200) };
|
||||||
|
|||||||
@@ -1361,6 +1361,18 @@ commands.room = ({ flags, positional }) => {
|
|||||||
"挑最想說的那一兩句就好,剩下的寫進心裡話。(真的需要長台詞才加 `--force`。)",
|
"挑最想說的那一兩句就好,剩下的寫進心裡話。(真的需要長台詞才加 `--force`。)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 台詞不可以是心裡話搬上台面:劇場模式裡別的人格只看得到你說出口的東西,
|
||||||
|
// 心裡話外流最可能的路徑就是你自己把它講出來。
|
||||||
|
if (speakerRole !== "guest" && pl.personaExists(speaker)) {
|
||||||
|
const leak = pl.innerLeak(speaker, text);
|
||||||
|
if (leak && !flags.force) {
|
||||||
|
die(
|
||||||
|
`這句跟你剛才的心裡話太像了(${leak.similarity}):「${leak.text.slice(0, 40)}…」。` +
|
||||||
|
"心裡話是只有你自己知道的東西——說出口的那句應該是你**決定要讓他聽到**的版本," +
|
||||||
|
"不是把心裡那句唸出來。(真的要講開才加 `--force`。)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 短句、日常話、講完不解釋、不說 AI 才會說的話——講話的樣子也擋在 CLI 裡,不靠自律。
|
// 短句、日常話、講完不解釋、不說 AI 才會說的話——講話的樣子也擋在 CLI 裡,不靠自律。
|
||||||
const lint = pl.speechBlockers(text);
|
const lint = pl.speechBlockers(text);
|
||||||
if (lint.length && !flags.force) {
|
if (lint.length && !flags.force) {
|
||||||
|
|||||||
@@ -1387,6 +1387,35 @@ check("sleeper 型別存在且宣告了唯讀工具限制", (() => {
|
|||||||
md.includes("回傳值裡不得出現任何記憶內容");
|
md.includes("回傳值裡不得出現任何記憶內容");
|
||||||
})());
|
})());
|
||||||
|
|
||||||
|
console.log("\n劇場模式:心裡話不能外流");
|
||||||
|
{
|
||||||
|
const S_INNER = "sess-inner-8888";
|
||||||
|
cli(["load", "--persona", "alpha", "--session", S_INNER, "--takeover"]);
|
||||||
|
const room3 = JSON.parse(cli(["invite", "--session", S_INNER, "--guest", "beta", "--topic", "心裡話測試", "--json"]).stdout).room;
|
||||||
|
cli(["think", "--persona", "alpha", "--session", S_INNER, "--text", "我其實一直在等他先開口問我那件事"]);
|
||||||
|
const leak = cli(["room", "post", "--session", S_INNER, "--room", room3, "--as", "alpha",
|
||||||
|
"--text", "我其實一直在等你先開口問我那件事。"], { expectOk: false });
|
||||||
|
check("台詞跟心裡話太像 → room post 擋下(心裡話不能搬上台面)",
|
||||||
|
leak.status !== 0 && leak.stderr.includes("心裡話"), leak.stderr.slice(0, 160));
|
||||||
|
check("--force 仍可放行(真的要講開的時候)",
|
||||||
|
cli(["room", "post", "--session", S_INNER, "--room", room3, "--as", "alpha",
|
||||||
|
"--text", "我其實一直在等你先開口問我那件事。", "--force"]).status === 0);
|
||||||
|
check("換句話、講別的就過得去",
|
||||||
|
cli(["room", "post", "--session", S_INNER, "--room", room3, "--as", "alpha",
|
||||||
|
"--text", "外面雨停了。要不要走走?"]).status === 0);
|
||||||
|
// 逐字稿裡只有說出口的東西,沒有任何心裡話的欄位
|
||||||
|
const rows = pl.roomRead(room3, 20);
|
||||||
|
check("聊天室逐字稿不含心裡話欄位",
|
||||||
|
rows.length > 0 && rows.every((r) => !("inner" in r) && !("think" in r)), JSON.stringify(Object.keys(rows[0] || {})));
|
||||||
|
check("別的人格讀不到你的 inner.jsonl(hook 擋)",
|
||||||
|
guard({ session_id: S_OTHER, tool_name: "Read",
|
||||||
|
tool_input: { file_path: `${H}/alpha/state/inner.jsonl` } }) === "deny");
|
||||||
|
check("別的人格也不能用 grep 掃出來",
|
||||||
|
guard({ session_id: S_OTHER, tool_name: "Grep", tool_input: { path: `${H}/alpha/state` } }) === "deny");
|
||||||
|
cli(["leave", "--session", S_INNER, "--guest", "beta"]);
|
||||||
|
cli(["release", "--session", S_INNER]);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`\n${"=".repeat(60)}\n通過 ${passed} 項,失敗 ${failed} 項 → ${failed === 0 ? "全部通過 ✅" : "有測試失敗 ❌"}`);
|
console.log(`\n${"=".repeat(60)}\n通過 ${passed} 項,失敗 ${failed} 項 → ${failed === 0 ? "全部通過 ✅" : "有測試失敗 ❌"}`);
|
||||||
console.log(`(暫存倉庫留在 ${STORE},可自行刪除)`);
|
console.log(`(暫存倉庫留在 ${STORE},可自行刪除)`);
|
||||||
process.exit(failed ? 1 : 0);
|
process.exit(failed ? 1 : 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user