diff --git a/hooks/prompt_submit.mjs b/hooks/prompt_submit.mjs index 6f8660e..e4d6155 100644 --- a/hooks/prompt_submit.mjs +++ b/hooks/prompt_submit.mjs @@ -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({ diff --git a/scripts/persona-lib.mjs b/scripts/persona-lib.mjs index ca9f1c5..1da6a16 100644 --- a/scripts/persona-lib.mjs +++ b/scripts/persona-lib.mjs @@ -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} 個動作,而且兩個要來自**同一種情緒**、有遞進關係` + "(否認 → 轉移、笑 → 補一句、留白 → 不追問);兩種情緒各演一個是演戲。" + + "名字後面括號裡的動作**算在這個額度裡**——括號演一個、句子裡再演兩個就是三個。" + "強度不到就不演。", ); }