v0.0.8:情緒處理四層(情緒先行/怎麼接/走向/飽和)+ R6 容量壓力的真 bug #11

Merged
admin merged 8 commits from develop into master 2026-07-31 06:32:20 +00:00
3 changed files with 68 additions and 0 deletions
Showing only changes of commit 646fd33541 - Show all commits
+27
View File
@@ -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) };
+12
View File
@@ -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) {
+29
View File
@@ -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.jsonlhook 擋)",
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);