feat: 語氣診療 skill、聊天室的發言權、講話再加四條規則
三件事,都是「講話要像人在講話」的延伸。 1. `persona-therapist`(新 skill):以第三方心理醫生的身分診斷一段對話。 雙方各給姓名/關係/情緒/逐字稿,用三把尺逐句標記——與關係不符 (語氣層錯位)、與事實不符(絕對化、讀心)、與目的不符(他要被理解, 講出來的話保證換到防衛)。標記掛在說那句話的人身上,修正只寫給他, 格式是「你其實想要的 → 對方收到的 → 改寫」,改寫必須留在同一個語氣層。 基線直接用 `TONE_TABLE`(已載入人格可 `relation show` 唯讀取得), 破壞模式表 P1–P16 放在 `reference/patterns.md`。不附身、不取鎖、不寫回, 出現自傷/暴力/受控訊號時停掉語氣分析改為安全優先。 2. 聊天室的發言權:同一個空間裡也會有一對一。`room post --to <他>` 進 一對一(旁人插話直接被擋,要帶 `--barge-in "<理由>"`,理由留在逐字稿)、 `--to all` 把話題開回全場。新指令 `room floor` 回報誰對誰在講、該誰接、 誰先安靜、誰隔了幾輪沒開口。同一份現況每輪注入 `<persona-context>`, 並明講「不要替沒被指名的人生成台詞,也不要為此啟動他的 sub agent」。 三人以上時 `room script` 標出對象(`🪼 Alpha(喜悅42) → Beta:…`), 兩個人的聊天室不標。舊逐字稿沒有 `to` → 視為全場,行為不變。 3. 講話的樣子再加四條:短句(一句 45 字內)、日常用詞、多講看得見的 東西(人、動作、物件、當下的場面)、**不要解釋自己的話**。前兩條與 第四條的句長/「我的意思是」這類開頭由 `speechLint()` 機械攔截 (`room post` 擋下、`said check` 事前警告,`--force` 例外);用詞與 具體度抓不到規則,走每輪注入的說話規則。 selftest 244 項全綠(新增 16 項:發言權 10、講話的樣子 6)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -239,6 +239,45 @@ check("多 guest 同時邀請成功",
|
||||
const multiMeta = pl.readJson(pl.roomMembersPath(multiRoom)) ?? {};
|
||||
check("同一個 room 內可同時掛多位 guest", Array.isArray(multiMeta.members) &&
|
||||
["alpha", "beta", "omega"].every((s) => multiMeta.members.includes(s)), JSON.stringify(multiMeta));
|
||||
|
||||
// 同一個空間裡的一對一:對象只有一個人時,旁人不該有台詞。
|
||||
cli(["room", "post", "--session", S_HOST, "--room", multiRoom, "--as", "alpha",
|
||||
"--to", "beta", "--text", "beta,那台鐘後來怎麼了?"]);
|
||||
const floorDyad = JSON.parse(cli(["room", "floor", "--session", S_HOST, "--room", multiRoom, "--json"]).stdout);
|
||||
check("room floor 認得一對一(誰該接、誰先安靜)",
|
||||
floorDyad.mode === "dyad" && floorDyad.next === "beta" && floorDyad.silent.includes("omega") &&
|
||||
!floorDyad.silent.includes("alpha"), JSON.stringify(floorDyad));
|
||||
const floorText = cli(["room", "floor", "--session", S_HOST, "--room", multiRoom]).stdout;
|
||||
check("room floor 的人話版本說清楚誰該接、誰安靜",
|
||||
floorText.includes("一對一") && floorText.includes("該接話的只有 Beta") && floorText.includes("先安靜:Omega"),
|
||||
JSON.stringify(floorText));
|
||||
const barge = cli(["room", "post", "--session", S_HOST, "--room", multiRoom, "--as", "omega", "--as-guest",
|
||||
"--text", "我也覺得那台鐘怪。"], { expectOk: false });
|
||||
check("一對一進行中,旁人插話會被擋下",
|
||||
barge.status !== 0 && barge.stderr.includes("一對一"), barge.stderr.slice(0, 200));
|
||||
check("被指名的人可以接話",
|
||||
cli(["room", "post", "--session", S_HOST, "--room", multiRoom, "--as", "beta", "--as-guest",
|
||||
"--to", "alpha", "--text", "還在慢三分鐘,我不修了。"]).status === 0);
|
||||
check("帶理由的 --barge-in 可以插話(話題放大)",
|
||||
cli(["room", "post", "--session", S_HOST, "--room", multiRoom, "--as", "omega", "--as-guest",
|
||||
"--barge-in", "話題轉到大家都要決定的事", "--to", "all", "--text", "那我們要不要一起去看那座鐘?"]).status === 0);
|
||||
const floorOpen = JSON.parse(cli(["room", "floor", "--session", S_HOST, "--room", multiRoom, "--json"]).stdout);
|
||||
check("--to all 之後發言權回到全場", floorOpen.mode === "open" && floorOpen.silent.length === 0,
|
||||
JSON.stringify(floorOpen));
|
||||
check("插話的理由留在逐字稿裡",
|
||||
pl.roomRead(multiRoom).some((m) => m.speaker === "omega" && m.barge_in?.includes("大家都要決定")));
|
||||
const multiScript = cli(["room", "script", "--session", S_HOST, "--room", multiRoom]).stdout;
|
||||
check("三人以上的對話稿會標出對象(Alpha → Beta)", /Alpha([^)]*) → Beta:/.test(multiScript),
|
||||
JSON.stringify(multiScript.slice(0, 200)));
|
||||
check("--to 不在場的人會被擋下",
|
||||
cli(["room", "post", "--session", S_HOST, "--room", multiRoom, "--as", "alpha",
|
||||
"--to", "不在場的人", "--text", "在嗎?"], { expectOk: false }).status !== 0);
|
||||
check("劇場模式的 context 會說明一對一時旁人不要有台詞", (() => {
|
||||
cli(["room", "post", "--session", S_HOST, "--room", multiRoom, "--as", "alpha",
|
||||
"--to", "beta", "--text", "beta,你自己一個人去嗎?"], { expectOk: true });
|
||||
const ctxRoom = pl.turnContext("alpha", S_HOST, "繼續");
|
||||
return ctxRoom.includes("一對一") && ctxRoom.includes("不要替他們生成台詞") && ctxRoom.includes("room floor");
|
||||
})(), pl.turnContext("alpha", S_HOST, "繼續").slice(-500));
|
||||
cli(["leave", "--session", S_HOST, "--guest", "beta"]);
|
||||
cli(["leave", "--session", S_HOST, "--guest", "omega"]);
|
||||
|
||||
@@ -354,6 +393,30 @@ const longPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--
|
||||
"--text", "第一句在這。第二句在這。第三句在這。第四句在這。"], { expectOk: false });
|
||||
check("room post 擋下超過三句的發言",
|
||||
longPost.status !== 0 && longPost.stderr.includes("超過"), longPost.stderr.slice(0, 160));
|
||||
// 講話的樣子:短句、日常話、講完不解釋。
|
||||
const longSentence = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||
"--text", "這件事情其實從很久以前就一直放在我心裡面沒有講出來過,因為我一直覺得時機不太對而且你最近也真的很忙。"],
|
||||
{ expectOk: false });
|
||||
check("room post 擋下太長的句子",
|
||||
longSentence.status !== 0 && longSentence.stderr.includes("字"), longSentence.stderr.slice(0, 200));
|
||||
const explainPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||
"--text", "我的意思是,那台鐘該修了。"], { expectOk: false });
|
||||
check("room post 擋下解釋自己的話(我的意思是…)",
|
||||
explainPost.status !== 0 && explainPost.stderr.includes("講完就算了"), explainPost.stderr.slice(0, 200));
|
||||
check("--force 仍可放行(真的需要長台詞時)",
|
||||
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||
"--text", "我的意思是,那台鐘該修了。", "--force"]).status === 0);
|
||||
check("日常短句照樣通過",
|
||||
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||
"--text", "桌上那杯茶都冷了。要不要再泡一杯?"]).status === 0);
|
||||
const lintCheck = JSON.parse(cli(["said", "check", "--persona", "alpha", "--session", S_SPEAK,
|
||||
"--text", "換句話說,我等一下再過去。", "--json"]).stdout);
|
||||
check("said check 也會報講話的樣子",
|
||||
lintCheck.ok === false && lintCheck.lint.some((i) => i.kind === "explain"), JSON.stringify(lintCheck.lint));
|
||||
check("說話規則有注入「短句/日常用詞/看得見的東西/不要解釋」", (() => {
|
||||
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
||||
return ["短句", "日常用詞", "看得見的東西", "不要解釋自己剛講的話"].every((w) => ctxSay.includes(w));
|
||||
})(), pl.turnContext("alpha", S_SPEAK, "隨便講").slice(0, 600));
|
||||
check("說出口的話有進 said.jsonl",
|
||||
pl.readJsonl(pl.saidPath("alpha")).some((r) => String(r.text).includes("鐘修得比上次穩")));
|
||||
hook("turn_end.mjs", { session_id: S_SPEAK, last_assistant_message: "🪼 Alpha(平靜50):那就這樣說定了。" });
|
||||
|
||||
Reference in New Issue
Block a user