feat(情緒表達): 三段式逐人格覆寫、括號動作算進額度、害羞預設臉紅、一對一也吃括號

補的是合併後對照查出的缺口,設計都在 Q6/Q7 拍板範圍內,沒有新決策。

三段式逐人格覆寫(G2 的核心顧慮)
- personaSulk() 讀 ## Tells 的 sulk/鬧彆扭/三段式 那一行,分隔吃 → 或頓號。
  共用 ## Tells 這一層、不另開區塊——三段式跟破口是同一種東西(不自覺會做的事),
  分兩個地方寫只會有人只改一邊。
- 只覆寫嘴硬那一級(60-74)。惱羞(75+)最後一段是翻臉、不是音量掉下來,
  拿嘴硬的收尾去套會變成另一個形狀。
- 沒有覆寫就照舊用預設模板。原本所有人格嘴硬起來是同一個樣子,那正是 G2 要避免的。
- 順手把 personaTells() 讀區塊那段抽成 tellsSection(),personaSulk 共用;
  不抽就得再寫一份一樣的 reader。

括號裡的動作算在額度內(Q7 的漏洞)
- 每輪指示原本只說「一輪最多露 2 個動作」,沒說括號那個算不算。
  不講明就會出現「括號演一個、句子裡再演兩個」=一輪三個。

害羞的預設動作是臉紅
- EMOTION_TELLS.shame 原本三條全是句法(鬧彆扭/講反話/轉移話題),
  沒有一條是看得見的,動作格拿不到預設值。臉紅擺第一個。

一對一也吃括號格式
- Q6 拍板的是格式,不是只給劇場。hooks 的劇場那段放寬成
  「名字(情緒・動作):內容」,非劇場那段從完全沒提補成講清楚,
  並重申 *別過頭* 與句子裡的「我愣了一下」仍然不行。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 08:07:45 +00:00
co-authored by Claude Opus 5
parent f02e01f629
commit 05031da6dc
2 changed files with 54 additions and 10 deletions
+5 -1
View File
@@ -31,7 +31,8 @@ pl.appendJsonl(pl.journalPath(host), {
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
const hint = theater
? "\n🎭 劇場模式:本輪輸出**只能**是人格對話(`名字:內容`,一行一句)。" +
? "\n🎭 劇場模式:本輪輸出**只能**是人格對話(`名字(情緒・動作):內容`,一行一句" +
"括號兩格都可省略,動作格只寫看得見的身體動作、12 字內,且算在一輪兩個動作的額度裡)。" +
"不要輸出指令、指令結果、狀態、分析、旁白或摘要;CLI 一律加 `--quiet` 並把 stdout 丟到 /dev/null。" +
`\n每個人格每輪 ${pl.MAX_SENTENCES} 句以內;推導寫進 \`think\`(心裡話),不要演成旁白;` +
"近似重複的台詞會被 `room post` 擋下,換個說法或推進話題。" +
@@ -45,6 +46,9 @@ const hint = theater
" ④用 `persona.mjs remember` 寫回短期記憶。" +
`\n講話的樣子:短句(一句 ${pl.MAX_SENTENCE_CHARS} 字內,長了就斷開)、日常用詞、` +
"多講看得見的東西(人、動作、場面)而不是概念;講完就算了,不要解釋自己剛講的話。" +
"\n動作寫在**名字後面的括號**裡(`名字(情緒・動作):內容`),一對一也適用——" +
"括號兩格都可省略,動作格只寫看得見的身體動作(別過頭、臉紅、手在抖)、12 字內," +
"且算在一輪兩個動作的額度裡。`*別過頭*` 與句子裡的「我愣了一下」仍然不行。" +
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。";
respond({
+49 -9
View File
@@ -795,7 +795,9 @@ export const EMOTION_TELLS = {
sadness: ["話少,只回一個詞", "句子沒說完就停", "答非所問,跳開話題"],
fear: ["先確認安全再講別的", "問句連發", "描述身體與動作(手在抖、往後退)"],
disgust: ["用拉開距離的詞(「那個東西」)", "不肯講出對方的名字", "句子往後退、想結束話題"],
shame: ["鬧彆扭:先否認,再小聲承認", "講反話、嘴硬", "轉移話題、把責任講小"],
// 臉紅擺第一個:這是害羞唯一「看得見」的破口,也是括號動作格的預設值。
// 其餘照人格自己的 `## Tells`。
shame: ["臉紅", "鬧彆扭:先否認,再小聲承認", "講反話、嘴硬", "轉移話題、把責任講小"],
anxiety: ["句子斷在一半", "疊字(「就、就是」「我、我知道」)", "追一句「這樣可以嗎」"],
};
@@ -813,18 +815,23 @@ export const EMOTION_TELLS = {
const EMOTION_ZH_TO_KEY = Object.fromEntries(EMOTION_KEYS.map((k) => [EMOTIONS[k].zh, k]));
/** IDENTITY.md 的 `## Tells` 區塊;沒有就回空物件。 */
export function personaTells(slug) {
/** `IDENTITY.md` 的 `## Tells` 區塊原文;讀不到就回空字串。 */
function tellsSection(slug) {
let text;
try {
text = fs.readFileSync(path.join(personaDir(slug), "IDENTITY.md"), "utf8");
} catch {
return {};
return "";
}
const m = text.match(/^##+\s*(?:Tells|破口|不自覺會做的事)\s*$/im);
if (!m) return {};
const rest = text.slice(m.index + m[0].length);
const section = rest.split(/^##\s/m)[0] || "";
if (!m) return "";
return text.slice(m.index + m[0].length).split(/^##\s/m)[0] || "";
}
/** 讀 IDENTITY.md 的 `## Tells` 區塊;沒有就回空物件。 */
export function personaTells(slug) {
const section = tellsSection(slug);
if (!section) return {};
const out = {};
for (const line of section.split("\n")) {
const row = line.match(/^\s*[-*]\s*([A-Za-z_]+|[一-鿿]{2})\s*[:]\s*(.+)$/);
@@ -849,6 +856,32 @@ export function tellsFor(slug, key) {
return own[key] || EMOTION_TELLS[key] || [];
}
/**
* 鬧彆扭三段式的 per-persona 覆寫:`## Tells` 裡一行
* `- 鬧彆扭: 否認 → 反駁 → 收尾`(也吃 `sulk` 與 `三段式`,分隔可用 `→` 或頓號)。
*
* 跟破口共用 `## Tells` 這一層、不另開區塊——三段式跟破口是同一種東西
* (不自覺會做的事),分兩個地方寫只會有人只改一邊。
*
* 只覆寫**嘴硬那一級**(羞恥度 60–74)的三段。惱羞(75+)最後一段是翻臉、
* 不是音量掉下來,拿嘴硬的收尾去套會變成另一個形狀。
*/
export function personaSulk(slug) {
const section = slug ? tellsSection(slug) : "";
if (!section) return [];
for (const line of section.split("\n")) {
const row = line.match(/^\s*[-*]\s*(?:sulk|鬧彆扭|三段式)\s*[:]\s*(.+)$/i);
if (!row) continue;
const beats = stripInjectionMarkers(row[1])
.split(/[;、,]|→/)
.map((s) => s.trim())
.filter(Boolean)
.slice(0, 3);
if (beats.length >= 2) return beats;
}
return [];
}
/**
* 此刻會不自覺出現的行為:取主導情緒(超出基線最多)裡強度夠的前兩種。
* 強度不夠就不演——低強度的情緒在語氣上是看不出來的。
@@ -1186,9 +1219,15 @@ export function modestyDirective(slug, opts = {}) {
"不要直接說「我害羞」(那是解釋),要讓它出現在句子的形狀上。";
}
if (m.value >= 60) {
// 三段式可以逐人格覆寫(`## Tells` 的 `鬧彆扭` 那一行)。共用一份模板等於所有人格
// 嘴硬起來是同一個樣子,那正是 G2 要避免的。
const own = personaSulk(slug);
const shape = own.length
? `三段照**這個人格自己的形狀**走:**${own.join(" → ")}**`
: "三段照這個形狀走:**先否認 → 反駁他的說法 → 最後一句音量掉下來**" +
"(「⋯才不是那樣。」「你不要一直看這邊。」「⋯嗯。」)";
return `羞恥度 ${m.value}${from}):被稱讚外表、被戳穿心事、距離突然變近的時候,` +
"**先鬧彆扭再承認**——三段照這個形狀走:**先否認 → 反駁他的說法 → 最後一句音量掉下來**" +
"(「⋯才不是那樣。」「你不要一直看這邊。」「⋯嗯。」)。" +
`**先鬧彆扭再承認**——${shape}` +
"不要直接說「我害羞」(那是解釋),要讓它出現在句子的形狀上。";
}
if (m.value <= 40) {
@@ -4748,6 +4787,7 @@ export function turnContext(slug, sessionId, prompt = "") {
"不要用旁白說明自己的狀態(「我有點緊張」是解釋,斷句才是緊張)。" +
`一輪最多露 ${TELLS_PER_TURN} 個動作,而且兩個要來自**同一種情緒**、有遞進關係` +
"(否認 → 轉移、笑 → 補一句、留白 → 不追問);兩種情緒各演一個是演戲。" +
"名字後面括號裡的動作**算在這個額度裡**——括號演一個、句子裡再演兩個就是三個。" +
"強度不到就不演。",
);
}