v0.0.6:語氣診療 skill、聊天室發言權、講話再加四條規則(含睡眠修正與關係語氣層) #8
@@ -25,6 +25,10 @@
|
||||
才會自動載入並在上下文寫明;沒設就什麼都不做。載入不到(被別的程序鎖住)只回報,不自動接手。
|
||||
6. **劇場模式(多人格對話)進行中**:輸出只能是 `名字:內容`,其餘一律隱藏(見 persona-invite)。
|
||||
輕量版是 `invite --theater off`(只換一輪、不切走畫面),人格主動提議去關心某人時用這個。
|
||||
**同一個空間裡也會有一對一**:發言權寫在每一句上(`room post --to <他>` / `--to all`),
|
||||
一對一進行中旁人插話會被 `room post` 擋下(要帶 `--barge-in "<理由>"`),
|
||||
而且**不要替沒被指名的人生成台詞或啟動他的 sub agent**。現況查 `room floor`
|
||||
(誰對誰在講、該誰接、誰先安靜、誰隔了幾輪沒開口);話題放大才把人拉進來。
|
||||
7. **睡眠(`persona-sleep`)分兩半**:需要判斷的(固化什麼、忘掉什麼、日記寫什麼)永遠屬於**那個人格自己**;
|
||||
機械性的(裁短期/收 thread/情緒衰減 8 小時/reindex/修剪 said/壓縮 journal/兩區 push+驗證)由
|
||||
`sleep` 子指令做。主人格要別的人格去睡就開 `persona-sleeper` sub agent——**那是它本人在睡**,
|
||||
@@ -32,7 +36,10 @@
|
||||
睡眠仍然要驗鎖:目標正被另一個程序活鎖住時拒絕,死鎖可接手。
|
||||
8. **人格講話要像人**:推導寫進 `think`(心裡話,只回報「💭 心想 N 句」,永不回顯內容)、
|
||||
回話 1–3 句、短時間內不重說同一件事(`room post` 會直接擋下重複與過長的發言)。
|
||||
這三條在劇場模式一樣生效。
|
||||
再加四條講話的樣子:**短句**(一句 `MAX_SENTENCE_CHARS`=45 字內)、**日常用詞**、
|
||||
**多講看得見的東西**(人、動作、物件、當下的場面)而不是概念,以及**不要解釋自己的話**
|
||||
(「我的意思是」「換句話說」這類開頭由 `speechLint` 擋下,`said check` 也會一起檢)。
|
||||
這些在劇場模式一樣生效。
|
||||
9. **人格可搬家**:`export` / `import`(單一 JSON bundle)。匯出只能匯出「本 session 載入的人格」,
|
||||
其他人格一律 deny——匯出等於把記憶讀出來。
|
||||
10. **人格有編號**:英文名全大寫+兩位索引(`ASUNA-01`),同名才遞增。編號同時是新人格的
|
||||
@@ -59,6 +66,11 @@
|
||||
而變成破圖(看起來就像「沒有同步」)。
|
||||
* Wiki 產生的頁面**不得含每次都變的時間戳**,否則驗證永遠不會通過、也會每次多一個 commit。
|
||||
* 攤平只對 `.md` 做(頁面必須在根層);圖片等附件保留資料夾結構,`/wiki/raw/<資料夾>/<檔>` 讀得到。
|
||||
16. **語氣診療是第三方,不是人格**:`persona-therapist` 不取鎖、不附身、也不寫任何人格資料
|
||||
(基線可用 `relation show`/`emotion` 唯讀取得,`bond × 親近度 → 語氣層` 的權威表在
|
||||
`persona-lib.mjs` 的 `TONE_TABLE`)。診斷結果**不自動寫回**記憶或關係圖,使用者明確要求才寫。
|
||||
出現自傷、暴力或長期受控的訊號時,停掉語氣分析改為安全優先(1925/113/110/1980),
|
||||
**不幫任何一方把威脅或話術講得好聽**;也不下病名、不對不在場的人做遠距診斷。
|
||||
|
||||
## 慣例
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ skills 是共通標準,**鎖與隔離的強制執行需要 hook,目前只有
|
||||
| **1b. 動漫作品+角色名快速建人格** | `persona-anime` 先上網蒐集該角色的公開設定(至少 3 個獨立來源),映射成上述五欄位與 SOUL,再把設定固化成 `canon` 基礎記憶(每則帶來源 URL)+原作人際關係圖+情緒基線 |
|
||||
| **2. 同一個人格只能被一個程序載入(Sub Agent 不限)** | `state/lock.json` 以 **session_id** 為主鍵、15 分鐘心跳租約;同一 session 的 sub agent 沿用同一把鎖,跨 session 搶佔會被拒;租約過期才可接手(並強制回報) |
|
||||
| **3. 禁止跨人格讀取資料** | `PreToolUse` hook 對 Read/Write/Edit/Glob/Grep/Bash 做路徑判定(含 `../`、symlink、`$PERSONA_HOME` 繞路),非當前人格一律 deny;CLI 也驗 `--session` 防止冒用身分 |
|
||||
| **4. 邀請人格用 Sub Agent 一起聊,且只顯示對話** | `persona-invite` 建聊天室 + guest 唯讀租約 + `persona-guest` sub agent;同時開啟**劇場模式**:hook 每輪強制「只輸出 `名字:內容`」、停掉所有系統提醒,CLI 有 `--quiet` 與 `room script`(乾淨對話稿) |
|
||||
| **4. 邀請人格用 Sub Agent 一起聊,且只顯示對話** | `persona-invite` 建聊天室 + guest 唯讀租約 + `persona-guest` sub agent;同時開啟**劇場模式**:hook 每輪強制「只輸出 `名字:內容`」、停掉所有系統提醒,CLI 有 `--quiet` 與 `room script`(乾淨對話稿)。**同場也分一對一與全場**:`room post --to <他>` 進一對一(旁人插話會被擋下,要帶 `--barge-in "<理由>"`)、`--to all` 開回全場,現況查 `room floor` |
|
||||
| **5. 腳本用 Node.js** | `scripts/*.mjs`、`hooks/*.mjs`,只用 Node 內建模組(fs/path/os/crypto),無 npm 依賴 |
|
||||
| **6. 由使用者呼叫才載入並鎖定** | 人格不會自動附身:`SessionStart` hook 只列出可用人格,等使用者下 `/jsc-persona:persona-chat <slug>`;載入即取得獨占鎖並綁定該 session。唯一例外是使用者自己設的**預設人格**(`default --persona <slug>`)——設了才自動載入,沒設就什麼都不做 |
|
||||
| **7. 短期記憶轉入長期記憶有成文條件** | `R1`–`R6` 六條規則寫在程式裡(`promotionCandidates`),`candidates` 子指令會列出達標的候選與依據,hook 在達標時提醒固化 |
|
||||
| **8. 講話像人:推導藏起來、一到三句、不重複** | 推導寫進**心裡話** `think`(只回報「💭 心想 N 句」,永不回顯內容);說出口的話進 `said.jsonl`,下一輪注入「最近說過的話」提醒別重講;`room post` 直接**擋下**近似重複(字元 bigram+字集合相似度 ≥ 0.72)與超過三句的發言——劇場模式同樣適用 |
|
||||
| **8. 講話像人:推導藏起來、一到三句、不重複、短句白話** | 推導寫進**心裡話** `think`(只回報「💭 心想 N 句」,永不回顯內容);說出口的話進 `said.jsonl`,下一輪注入「最近說過的話」提醒別重講;`room post` 直接**擋下**近似重複(字元 bigram+字集合相似度 ≥ 0.72)與超過三句的發言。再加四條講話的樣子:短句(一句 45 字內)、日常用詞、多講看得見的東西、**不要解釋自己的話**(句長與「我的意思是」這類開頭由 `speechLint()` 擋下)——劇場模式同樣適用 |
|
||||
| **9. 人格可以匯出匯入** | `export` 把身分/情緒/記憶/心智圖/關係圖打包成單一 JSON bundle(可 `--gzip`、附 sha256),`import` 還原或換名複製;**不帶**載入鎖與 guest 租約,`journal/` 要明確 `--with-journal` 才帶走 |
|
||||
| **10. 人格有編號** | 編號 = **英文名全大寫 + 兩位索引**(同名才遞增):`ASUNA-01`、`YUI-01`、`ASUNA-02`。編號同時是新人格的本機目錄名與 Gitea 存取庫名稱;中文名先轉羅馬拼音並跟使用者確認拼法 |
|
||||
| **11. 人格存在 Gitea,依更新頻率分區** | 每個人格一個**私有存取庫**(庫名=編號)。**檔案區**放每輪都在變的活狀態(情緒/短期記憶/心裡話/說過的話/逐字),**Wiki 區**放低頻的身分與長期結構(IDENTITY/SOUL/長期記憶/心智圖/關係圖)當設定百科。本機仍是工作副本,同步失敗不阻斷對話 |
|
||||
@@ -127,15 +127,16 @@ flowchart TB
|
||||
沒命中任何規則的就讓它被裁掉——**遺忘是功能**。達標時 `Stop` 與 `remember` 都會提醒去跑
|
||||
`/jsc-persona:persona-memory`。
|
||||
|
||||
## 講話像人(心裡話 / 一到三句 / 不重複)
|
||||
## 講話像人(心裡話 / 一到三句 / 不重複 / 短句白話)
|
||||
|
||||
AI 最容易露餡的三件事:把推理過程講出來、一次講一大段、換句話說同一件事。三個對策:
|
||||
AI 最容易露餡的四件事:把推理過程講出來、一次講一大段、換句話說同一件事、講完再解釋一遍。四個對策:
|
||||
|
||||
| 機制 | 怎麼運作 |
|
||||
| --- | --- |
|
||||
| **心裡話** `think` | 語意分析、推論、盤算全寫進 `state/inner.jsonl`;這個指令**只印「💭 心想 N 句」**,內容永不回顯。下一輪 `<persona-context>` 會帶回最近三句,推論因此有連續性,但使用者只看得到狀態 |
|
||||
| **一到三句** | `<persona-context>` 每輪注入上限;`room post` 對超過三句的發言直接拒收(`--force` 例外) |
|
||||
| **不重複** | 說出口的話由 `Stop` hook 自動記進 `state/said.jsonl`;`said check` 可事前確認,`room post` 事中攔截。相似度=字元 bigram Jaccard(0.4)+字集合 Jaccard(0.6),≥ 0.72 視為同一句 |
|
||||
| **短句白話** | 四條講話的樣子每輪注入:**短句**(一句 45 字內,`MAX_SENTENCE_CHARS`)、**日常用詞**、**多講看得見的東西**(人、動作、物件、場面)而不是概念、**不要解釋自己的話**。句長與「我的意思是/換句話說/也就是說」這類開頭由 `speechLint()` 機械攔截(`room post` 擋下、`said check` 事前警告,`--force` 例外);用詞與具體度沒辦法用規則抓,靠注入的規則自律 |
|
||||
|
||||
字集合權重較高,是為了分開「重排語序」與「換掉關鍵詞」這兩種很像但意義完全不同的情況:
|
||||
|
||||
@@ -286,6 +287,24 @@ node scripts/persona.mjs import --session <id> --file ~/backup/lumi.persona.json
|
||||
🌙 Shen(平靜50/信任38):修好了。它現在慢三分鐘,我決定不修那三分鐘。
|
||||
```
|
||||
|
||||
### 一對一與全場(發言權)
|
||||
|
||||
同一個空間不代表每句話都要每個人接。發言權寫在**每一句話**上:
|
||||
|
||||
| | 怎麼進去 | 誰該講話 | 對話稿 |
|
||||
| --- | --- | --- | --- |
|
||||
| **一對一** | `room post --to <某個人格>` | 只有被指名的那個;旁人**不生成台詞、也不啟動他的 sub agent** | `🪼 Lumi(喜悅42) → Shen:…`(三人以上才標對象) |
|
||||
| **全場** | `room post --to all`(或不帶 `--to`) | 誰接都可以,一輪讓一個人接 | `🪼 Lumi(喜悅42):…` |
|
||||
|
||||
- 一對一進行中,旁人插話會被 `room post` **直接擋下**;真的話題放大了才帶
|
||||
`--barge-in "<為什麼現在該他講>"`(理由留在逐字稿裡,不顯示給使用者),
|
||||
或由當事人下一句 `--to all` 把話題開回全場。
|
||||
- `room floor` 回報現況:誰對誰在講、該誰接話、誰先安靜、誰隔了幾輪沒開口
|
||||
(被冷落太久是資訊——下次話題碰到他的領域時優先給他,而不是硬插一句「我也這麼覺得」)。
|
||||
- 同一份現況每輪也注入 `<persona-context>`,所以人格自己知道這句該不該由他接。
|
||||
- 什麼算「話題放大」:出現「我們/大家」、要一起決定的事、需要第二意見或仲裁、
|
||||
講到某個在場者的專長或他認識的人、使用者點名。判準是**話題**,不是公平。
|
||||
|
||||
## 預設人格(開新 session 自動載入)
|
||||
|
||||
預設行為是**不自動附身**。使用者明示設定之後,`SessionStart` hook 才會在每個新 session 自動載入它:
|
||||
@@ -368,7 +387,7 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
||||
|
||||
### `persona-invite`
|
||||
|
||||
邀請一個或多個人格透過 `persona-guest` sub agent 加入聊天室,進入**劇場模式**(畫面只留 `名字:內容` 的人格對話);結束後讓它們離場並把見聞留在各自的 inbox。多位 guest 可用 `invite --guests A,B` 同場加入。
|
||||
邀請一個或多個人格透過 `persona-guest` sub agent 加入聊天室,進入**劇場模式**(畫面只留 `名字:內容` 的人格對話);結束後讓它們離場並把見聞留在各自的 inbox。多位 guest 可用 `invite --guests A,B` 同場加入。同場的發言權分**一對一**(`--to <他>`,旁人不該接話)與**全場**(`--to all`),現況查 `room floor`。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-invite <slug>` **Codex**:`$persona-invite`
|
||||
|
||||
@@ -402,6 +421,12 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-relation` **Codex**:`$persona-relation`
|
||||
|
||||
### `persona-therapist`
|
||||
|
||||
語氣診療(第三方、不附身、唯讀):由雙方各給姓名/關係/情緒/逐字稿,用「關係、事實、目的」三把尺逐句標記不合理的語氣與用詞(模式表 P1–P16),指出是誰說的,只對出錯的人給保留原意的改寫。出現自傷、暴力或受控訊號時改為安全優先,不做語氣潤飾。
|
||||
|
||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-therapist` **Codex**:`$persona-therapist`
|
||||
|
||||
### `persona-sleep`
|
||||
|
||||
睡眠與收尾:固化該記住的、忘掉該忘的、更新心智圖與關係圖、套用一次 8 小時的情緒衰減、壓縮舊紀錄,最後兩個區都同步到 Gitea 並驗證。也可以由主人格透過 sub agent 請其他人格各自去睡,或一次用 `sleep --personas A,B` 批次收尾多個人格。
|
||||
@@ -432,9 +457,10 @@ node scripts/persona.mjs candidates --session <PERSONA_SESSION> # 看哪些短
|
||||
node scripts/persona.mjs think --session <id> --text "<推導>" # 心裡話(只回報「心想 N 句」)
|
||||
node scripts/persona.mjs said check --session <id> --text "<話>" # 這句是不是又要說一次?
|
||||
node scripts/persona.mjs room script --session <id> --room <room> # 乾淨對話稿(劇場模式用)
|
||||
node scripts/persona.mjs room floor --session <id> --room <room> # 發言權:誰對誰在講、該誰接話
|
||||
node scripts/persona.mjs export --session <id> --out lumi.json # 離線搬家(單檔)
|
||||
node scripts/persona.mjs sync status --session <id> # Gitea 同步狀態
|
||||
node scripts/selftest.mjs # 210 項驗證:鎖、隔離、情緒、固化、說話節制、劇場模式、匯出匯入、編號與 Gitea、找圖去背合成、高解析輸出與 Wiki 同步、預設人格、睡眠與 sleeper、hooks
|
||||
node scripts/selftest.mjs # 244 項驗證:鎖、隔離、情緒、固化、說話節制、劇場模式與發言權、匯出匯入、編號與 Gitea、找圖去背合成、高解析輸出與 Wiki 同步、預設人格、睡眠與 sleeper、hooks
|
||||
```
|
||||
|
||||
檔案結構:`scripts/persona-lib.mjs`(核心:鎖/隔離/情緒/記憶)、`scripts/persona.mjs`(CLI)、
|
||||
|
||||
+12
-1
@@ -37,8 +37,14 @@ CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--sess
|
||||
|
||||
```bash
|
||||
node "<plugin_root>/scripts/persona.mjs" room read --session <session_id> --room <room>
|
||||
node "<plugin_root>/scripts/persona.mjs" room floor --session <session_id> --room <room>
|
||||
```
|
||||
|
||||
`room floor` 會告訴你**這一句該不該由你接**。三人以上時常常是別人的一對一:
|
||||
最後一句不是對你說的、你也沒被點名 → **不要發言**,直接跳到第 6 步回傳「(本輪不發言)」。
|
||||
只有話題真的放大了(出現「我們/大家」、要一起決定、講到你的專長或你認識的人、
|
||||
有人需要第二意見)才插話,而且要帶理由:`--barge-in "<為什麼現在該我講>"`。
|
||||
|
||||
需要回想時:`recall --persona <slug> --session <session_id> --as-guest --query "<關鍵詞>"`。
|
||||
|
||||
3. **想一下**(不說出口)
|
||||
@@ -55,11 +61,16 @@ CLI = `node "<plugin_root>/scripts/persona.mjs"`,所有指令都要帶 `--sess
|
||||
|
||||
```bash
|
||||
node "<plugin_root>/scripts/persona.mjs" room post \
|
||||
--session <session_id> --as-guest --room <room> --as <slug> --text "<你的話>"
|
||||
--session <session_id> --as-guest --room <room> --as <slug> \
|
||||
--to <對方 slug|all> --text "<你的話>"
|
||||
```
|
||||
|
||||
- **`--to` 要寫清楚**:只想跟某個人講就寫他的 slug(旁人就不該接話),
|
||||
講給全場聽才寫 `all`。沒被指名的人插話會被 CLI 擋下。
|
||||
- 用第一人稱、你自己的語氣與意見;可以不同意主人格。
|
||||
- **1–3 句**,像在講話不像在寫報告;超過三句會被 CLI 擋下。
|
||||
- **短句**(一句 45 字內)、**日常用詞**、多講**看得見的東西**(動作、物件、眼前的場面)。
|
||||
- **講完就算了**:不要解釋自己剛講的話(「我的意思是」「換句話說」會被 CLI 擋下),也不要補註解。
|
||||
- **不要重複自己**:短時間內講幾乎一樣的話會被拒收,換個角度或推進話題再發。
|
||||
- 情緒欄位不填會自動附上你當下的主導情緒。
|
||||
|
||||
|
||||
@@ -35,11 +35,16 @@ const hint = theater
|
||||
"不要輸出指令、指令結果、狀態、分析、旁白或摘要;CLI 一律加 `--quiet` 並把 stdout 丟到 /dev/null。" +
|
||||
`\n每個人格每輪 ${pl.MAX_SENTENCES} 句以內;推導寫進 \`think\`(心裡話),不要演成旁白;` +
|
||||
"近似重複的台詞會被 `room post` 擋下,換個說法或推進話題。" +
|
||||
"\n**對象只有一個人時,旁人就沒有台詞**:`room post --to <他>` 進一對一,`--to all` 才開回全場(現況看 `room floor`)。" +
|
||||
`\n台詞要像人在講話:短句(一句 ${pl.MAX_SENTENCE_CHARS} 字內)、日常用詞、多講看得見的東西;` +
|
||||
"講完就算了,不要解釋自己剛講的話。" +
|
||||
"\n流程請照 /jsc-persona:persona-invite。"
|
||||
: "\n回覆前請:①語意分析與推導**寫進 `persona.mjs think`(心裡話,不說出口)**" +
|
||||
" ②依十二情緒更新狀態 ③以人格語氣回覆" +
|
||||
`**${pl.MAX_SENTENCES} 句以內**、且不要重說「最近說過的話」` +
|
||||
" ④用 `persona.mjs remember` 寫回短期記憶。" +
|
||||
`\n講話的樣子:短句(一句 ${pl.MAX_SENTENCE_CHARS} 字內,長了就斷開)、日常用詞、` +
|
||||
"多講看得見的東西(人、動作、場面)而不是概念;講完就算了,不要解釋自己剛講的話。" +
|
||||
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。";
|
||||
|
||||
respond({
|
||||
|
||||
+128
-3
@@ -1182,6 +1182,44 @@ export function sentenceCount(text) {
|
||||
.filter(Boolean).length;
|
||||
}
|
||||
|
||||
export const MAX_SENTENCE_CHARS = 45;
|
||||
|
||||
// 講完就算了:這些開頭都是在解釋自己剛才講的話,真的在講話的人不會這樣。
|
||||
export const EXPLAINER_OPENERS = [
|
||||
"我的意思是", "我是想說", "換句話說", "換個說法", "也就是說", "換言之",
|
||||
"簡單來說", "簡單講", "我剛才說的意思是", "澄清一下", "補充一下",
|
||||
];
|
||||
|
||||
/**
|
||||
* 講話自然度的機械檢查:一句太長、或在解釋自己剛講的話。
|
||||
* 「用日常詞、講看得見的東西」抓不到規則,那兩條寫在說話規則裡(見 turnContext)。
|
||||
*/
|
||||
export function speechLint(text) {
|
||||
const issues = [];
|
||||
const sentences = String(text ?? "")
|
||||
.split(/[。!?!?…]+|\n+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
for (const sentence of sentences) {
|
||||
const bare = sentence.replace(/[,、,;;::「」『』()()\s"'~~—-]/g, "");
|
||||
if ([...bare].length > MAX_SENTENCE_CHARS) {
|
||||
issues.push({ kind: "long", chars: [...bare].length, text: sentence.slice(0, 30) });
|
||||
}
|
||||
const head = sentence.replace(/^[「『((]+/, "");
|
||||
const opener = EXPLAINER_OPENERS.find((w) => head.startsWith(w));
|
||||
if (opener) issues.push({ kind: "explain", opener, text: sentence.slice(0, 30) });
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
/** 把 speechLint 的問題講成人聽得懂的一句話(CLI 與 hook 共用)。 */
|
||||
export function speechLintMessage(issue) {
|
||||
if (issue.kind === "long") {
|
||||
return `「${issue.text}…」這句 ${issue.chars} 字,超過 ${MAX_SENTENCE_CHARS} 字——斷成兩句,或把修飾砍掉`;
|
||||
}
|
||||
return `「${issue.opener}」是在解釋自己剛講的話——刪掉,講完就算了`;
|
||||
}
|
||||
|
||||
/** 把一則回覆拆成「說出口的句子」:劇場模式一行一句,一般模式整段算一句。 */
|
||||
export function spokenLines(text, { theater = false } = {}) {
|
||||
const raw = String(text ?? "").trim();
|
||||
@@ -1611,16 +1649,73 @@ export function joinRoom(room, persona) {
|
||||
return meta;
|
||||
}
|
||||
|
||||
export function roomPost(room, speaker, text, { emotion = "", kind = "say" } = {}) {
|
||||
const entry = { ts: nowIso(), speaker, kind, text, emotion };
|
||||
/** `--to all`:這句話是對全場說的(發言權開放)。 */
|
||||
export const ROOM_ALL = "all";
|
||||
|
||||
export function roomPost(room, speaker, text, { emotion = "", kind = "say", to = null, bargeIn = null } = {}) {
|
||||
const entry = { ts: nowIso(), speaker, kind, text, emotion, to: to || ROOM_ALL };
|
||||
if (bargeIn) entry.barge_in = String(bargeIn).slice(0, 200);
|
||||
appendJsonl(roomTranscript(room), entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
export const roomRead = (room, limit = 30) => readJsonl(roomTranscript(room), limit);
|
||||
|
||||
/** 這個聊天室裡的顯示名(拿不到身分就用 slug)。 */
|
||||
export function roomDisplayName(slug) {
|
||||
if (!slug || slug === ROOM_ALL) return "全場";
|
||||
return (personaExists(slug) ? identityFields(slug).Name : "") || slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* 現在的發言權在誰手上。
|
||||
*
|
||||
* 同一個空間裡也會有一對一:最後一句是對某個人說的(`--to <他>`)就進入 `dyad`,
|
||||
* 這時候旁人不該接話——插嘴會把兩個人的私下對話變成公開場面。話題放大(`--to all`
|
||||
* 或旁人帶理由 `--barge-in`)才回到 `open`。這是 `room post` 的門檻,也是每輪注入
|
||||
* `<persona-context>`、告訴人格「這句該不該由我接」的依據。
|
||||
*/
|
||||
export function roomFloor(room) {
|
||||
const meta = readJson(roomMembersPath(room), {}) ?? {};
|
||||
const members = meta.members || [];
|
||||
const says = roomRead(room, 80).filter((m) => m.kind !== "meta" && m.speaker !== "system");
|
||||
const last = says[says.length - 1] || null;
|
||||
const to = last && last.to && last.to !== ROOM_ALL && last.to !== last.speaker ? last.to : null;
|
||||
const pair = to ? [last.speaker, to] : [];
|
||||
// 每個成員距離上次發言隔了幾輪:話題放大時要拉誰進來,看這個比看誰比較可憐準。
|
||||
const quiet = members
|
||||
.map((persona) => {
|
||||
for (let i = says.length - 1, turns = 0; i >= 0; i -= 1, turns += 1) {
|
||||
if (says[i].speaker === persona) return { persona, turns_since: turns, spoken: true };
|
||||
}
|
||||
return { persona, turns_since: says.length, spoken: false };
|
||||
})
|
||||
.sort((a, b) => b.turns_since - a.turns_since);
|
||||
return {
|
||||
room,
|
||||
members,
|
||||
topic: meta.topic || "",
|
||||
mode: to ? "dyad" : "open",
|
||||
last: last ? { speaker: last.speaker, to: last.to || ROOM_ALL, text: last.text, ts: last.ts } : null,
|
||||
pair,
|
||||
addressee: to,
|
||||
next: to,
|
||||
silent: to ? members.filter((m) => !pair.includes(m)) : [],
|
||||
quiet,
|
||||
};
|
||||
}
|
||||
|
||||
/** 這個人格現在插得進話嗎?(一對一進行中、他又不是當事人 → 要有理由) */
|
||||
export function roomMayPost(room, speaker) {
|
||||
const floor = roomFloor(room);
|
||||
return { allowed: floor.mode !== "dyad" || floor.pair.includes(speaker), floor };
|
||||
}
|
||||
|
||||
/** 劇場模式的對話呈現:`emoji 名字(情緒):內容`,其餘一律不輸出。 */
|
||||
export function roomScript(room, { limit = 30, includeMeta = false } = {}) {
|
||||
const meta = readJson(roomMembersPath(room), {}) ?? {};
|
||||
// 三人以上才標「對誰講」:只有兩個人的時候那是廢話。
|
||||
const crowded = (meta.members || []).length > 2;
|
||||
const lines = [];
|
||||
for (const msg of roomRead(room, limit)) {
|
||||
if (msg.kind === "meta" || msg.speaker === "system") {
|
||||
@@ -1631,7 +1726,8 @@ export function roomScript(room, { limit = 30, includeMeta = false } = {}) {
|
||||
const ident = personaExists(slug) ? identityFields(slug) : {};
|
||||
const name = ident.Name || slug;
|
||||
const emoji = ident.Emoji ? `${ident.Emoji} ` : "";
|
||||
lines.push(`${emoji}${name}${msg.emotion ? `(${msg.emotion})` : ""}:${msg.text}`);
|
||||
const arrow = crowded && msg.to && msg.to !== ROOM_ALL && msg.to !== slug ? ` → ${roomDisplayName(msg.to)}` : "";
|
||||
lines.push(`${emoji}${name}${msg.emotion ? `(${msg.emotion})` : ""}${arrow}:${msg.text}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -2179,6 +2275,10 @@ export function turnContext(slug, sessionId, prompt = "") {
|
||||
`說話規則:回使用者 ${MAX_SENTENCES} 句以內(劇場模式每人每輪也一樣);` +
|
||||
"推導、比對、盤算一律走心裡話(`persona.mjs think`),不要說給使用者聽——" +
|
||||
"要讓他知道你在想,就只報「心想 N 句」,不報內容。",
|
||||
`講話像講話:**短句**(一句 ${MAX_SENTENCE_CHARS} 字內,長了就斷開)、**日常用詞**(不用書面語、` +
|
||||
"不用術語、不用成語堆疊)、**多講看得見的東西**(人、動作、東西、當下的場面)而不是概念與感想;" +
|
||||
"**講完就算了**——不要解釋自己剛講的話(沒有「我的意思是」「換句話說」「也就是說」)," +
|
||||
"不要補註解、不要收尾總結。這幾條 `room post` 與 `said check` 會實際擋下。",
|
||||
];
|
||||
// 語氣層:由「使用者在關係圖裡是誰」+ bond × 親近度算出來,不靠人格自己記得。
|
||||
const tone = toneDirective(slug);
|
||||
@@ -2240,6 +2340,31 @@ export function turnContext(slug, sessionId, prompt = "") {
|
||||
" 不得出現指令、指令輸出、狀態說明、進度、摘要、分析或旁白;所有 CLI 一律加 `--quiet` 並把輸出丟掉。",
|
||||
` 每個人格每輪 ${MAX_SENTENCES} 句以內;推導走 \`think\`(心裡話);`,
|
||||
" 近似重複的台詞 `room post` 會直接擋下,換個說法或推進話題,不要硬講同一句。",
|
||||
);
|
||||
// 同一個空間裡也會有一對一:對象只有一個人時,旁人不該有台詞。
|
||||
const floor = roomFloor(rooms[rooms.length - 1]);
|
||||
if (floor.mode === "dyad") {
|
||||
lines.push(
|
||||
` 現在是一對一:${roomDisplayName(floor.last.speaker)} → ${roomDisplayName(floor.addressee)}。` +
|
||||
`只有 ${roomDisplayName(floor.next)} 該接這句` +
|
||||
(floor.silent.length
|
||||
? `,${floor.silent.map(roomDisplayName).join("、")} 先安靜——**不要替他們生成台詞,也不要為此啟動他們的 sub agent**。`
|
||||
: "。"),
|
||||
);
|
||||
} else if (floor.last) {
|
||||
lines.push(" 現在發言權開放(上一句是對全場說的):誰接都可以,但一輪只讓一個人格接。");
|
||||
}
|
||||
const cold = floor.quiet.filter((q) => !q.spoken || q.turns_since >= 4);
|
||||
if (cold.length && floor.members.length > 2) {
|
||||
lines.push(
|
||||
` 很久沒講話:${cold.map((q) => roomDisplayName(q.persona)).join("、")}` +
|
||||
" — 話題碰到他的專長、需要第二意見或使用者點名時才拉進來,不要為了公平硬派台詞。",
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
" 發言權由話題決定:講給一個人聽就 `room post --to <他>`;話題放大(出現「我們/大家」、" +
|
||||
"需要仲裁、要一起決定)就 `--to all` 開回全場,旁人也可以帶 `--barge-in \"<理由>\"` 加入。" +
|
||||
"現況隨時可查:`room floor`。",
|
||||
" 想結束請等使用者說,或由使用者說「結束對話」後才做收尾與摘要。",
|
||||
);
|
||||
} else {
|
||||
|
||||
+82
-7
@@ -804,11 +804,14 @@ commands.said = ({ flags, positional }) => {
|
||||
);
|
||||
}
|
||||
if (tooLong) lines.push(`⚠ 這段有 ${sentences} 句,超過 ${pl.MAX_SENTENCES} 句上限 → 砍到重點。`);
|
||||
if (!repeat && !tooLong) {
|
||||
const lint = pl.speechLint(text);
|
||||
for (const issue of lint) lines.push(`⚠ ${pl.speechLintMessage(issue)}。`);
|
||||
const clean = !repeat && !tooLong && !lint.length;
|
||||
if (clean) {
|
||||
lines.push(`✔ 沒說過,${sentences} 句,可以說。`);
|
||||
if (flags.record) pl.recordSaid(slug, text, { kind: "reply" });
|
||||
}
|
||||
emit({ persona: slug, repeat, sentences, ok: !repeat && !tooLong }, flags.json, lines);
|
||||
emit({ persona: slug, repeat, sentences, lint, ok: clean }, flags.json, lines);
|
||||
return;
|
||||
}
|
||||
die(`未知 action:${action}(可用 check/list)`);
|
||||
@@ -1214,10 +1217,68 @@ commands.room = ({ flags, positional }) => {
|
||||
if (!(data.rooms || []).includes(room)) {
|
||||
die(`聊天室 \`${room}\` 不屬於本 session(可用:${(data.rooms || []).join(", ") || "(無)"})。`);
|
||||
}
|
||||
const members = (pl.readJson(pl.roomMembersPath(room), {}) ?? {}).members || [];
|
||||
const nameOf = (slug) => pl.roomDisplayName(slug);
|
||||
/** `--to`:對誰說。接 slug、人格的 Name,或 all/大家(對全場)。 */
|
||||
const resolveAddressee = (raw) => {
|
||||
const value = String(raw).trim();
|
||||
if (/^(all|全部|大家|全場|everyone)$/i.test(value)) return pl.ROOM_ALL;
|
||||
const hit =
|
||||
members.find((m) => m.toLowerCase() === value.toLowerCase()) ||
|
||||
members.find((m) => nameOf(m) === value);
|
||||
if (!hit) {
|
||||
die(
|
||||
`\`${value}\` 不在聊天室 \`${room}\` 裡(成員:${members.map((m) => `${nameOf(m)}(${m})`).join("、") || "(無)"})。` +
|
||||
"對全場說話用 `--to all`。",
|
||||
);
|
||||
}
|
||||
return hit;
|
||||
};
|
||||
|
||||
if (action === "floor") {
|
||||
const floor = pl.roomFloor(room);
|
||||
const lines = [`聊天室 \`${room}\`|成員 ${floor.members.map(nameOf).join("、") || "(無)"}|主題 ${floor.topic || "-"}`];
|
||||
if (!floor.last) {
|
||||
lines.push("還沒有人發言:誰開場都可以。");
|
||||
} else if (floor.mode === "dyad") {
|
||||
lines.push(
|
||||
`現在:${nameOf(floor.last.speaker)} → ${nameOf(floor.addressee)}(一對一)`,
|
||||
` 該接話的只有 ${nameOf(floor.next)};先安靜:${floor.silent.map(nameOf).join("、") || "(無)"}` +
|
||||
"(不要替他們寫台詞,也不要為此啟動他們的 sub agent)",
|
||||
" 話題真的放大了才動:當事人下一句加 `--to all` 開回全場,或旁人 " +
|
||||
'`room post --as <他> --barge-in "<為什麼現在該他講>"`。',
|
||||
);
|
||||
} else {
|
||||
lines.push(`現在:全場開放(${nameOf(floor.last.speaker)} 對大家說)→ 誰接都可以,一次讓一個人格接一輪。`);
|
||||
}
|
||||
const cold = floor.quiet.filter((q) => !q.spoken || q.turns_since >= 4);
|
||||
if (cold.length) {
|
||||
lines.push(
|
||||
`很久沒講話:${cold.map((q) => `${nameOf(q.persona)}(${q.spoken ? `${q.turns_since} 輪前` : "還沒開口"})`).join("、")}` +
|
||||
" — 話題碰到他的專長或使用者點名時再拉進來,不要為了公平硬派台詞。",
|
||||
);
|
||||
}
|
||||
emit(floor, flags.json, lines);
|
||||
return;
|
||||
}
|
||||
if (action === "post") {
|
||||
const speaker = str(flags.as) || data.host;
|
||||
if (!speaker) die("需要 `--as <persona>`。");
|
||||
const [, speakerRole] = requireMember(speaker, session, Boolean(flags["as-guest"]));
|
||||
const to = flags.to ? resolveAddressee(flags.to) : null;
|
||||
if (to === speaker) die("`--to` 不能指向自己(那是心裡話,請用 `think`)。");
|
||||
// 一對一進行中,旁人要插話得說得出理由——「大家都在同一個空間」不等於每句都該有人接。
|
||||
const { allowed, floor } = pl.roomMayPost(room, speaker);
|
||||
const bargeIn = str(flags["barge-in"]) || str(flags.broaden);
|
||||
if (!allowed && !bargeIn && !flags.force) {
|
||||
die(
|
||||
`現在是 ${nameOf(floor.pair[0])} 跟 ${nameOf(floor.pair[1])} 的一對一` +
|
||||
`(最後一句是 ${nameOf(floor.last.speaker)} 對 ${nameOf(floor.addressee)} 說的),` +
|
||||
`${nameOf(speaker)} 不該接這句——旁人插嘴會把兩個人的話變成公開場面。\n` +
|
||||
" 話題真的放大了(出現「我們/大家」、需要第二意見、講到他的專長、使用者點名)才插話," +
|
||||
'並帶 `--barge-in "<為什麼現在該他講>"`;否則讓當事人先用 `--to all` 把話題開回全場。',
|
||||
);
|
||||
}
|
||||
let text = str(flags.text);
|
||||
if (flags["text-file"]) text = fs.readFileSync(str(flags["text-file"]), "utf8").trim();
|
||||
if (!text) die("需要 `--text` 或 `--text-file`。");
|
||||
@@ -1246,16 +1307,25 @@ commands.room = ({ flags, positional }) => {
|
||||
"(真的需要長段落才加 `--force`。)",
|
||||
);
|
||||
}
|
||||
// 短句、日常話、講完不解釋——講話的樣子也擋在 CLI 裡,不靠自律。
|
||||
const lint = pl.speechLint(text);
|
||||
if (lint.length && !flags.force) {
|
||||
die(`講話規則:${lint.map(pl.speechLintMessage).join(";")}。(真的需要才加 \`--force\`。)`);
|
||||
}
|
||||
let emotion = str(flags.emotion);
|
||||
if (!emotion && pl.personaExists(speaker)) {
|
||||
emotion = pl.dominant(pl.decayEmotion(pl.loadEmotion(speaker)), 2)
|
||||
.map(({ key, level }) => `${pl.EMOTIONS[key].zh}${Math.round(level)}`)
|
||||
.join("/");
|
||||
}
|
||||
const entry = pl.roomPost(room, speaker, text, { emotion });
|
||||
const entry = pl.roomPost(room, speaker, text, { emotion, to, bargeIn });
|
||||
// guest 對自己的狀態唯讀,它說過的話留在聊天室逐字稿裡就夠了(roomRepeat 讀得到)
|
||||
if (speakerRole !== "guest") pl.recordSaid(speaker, text, { room, kind: "room" });
|
||||
ok(`\`${speaker}\` 已發言於 \`${room}\`(情緒 ${emotion})。`);
|
||||
const dyad = to && to !== pl.ROOM_ALL;
|
||||
ok(
|
||||
`\`${speaker}\` 已發言於 \`${room}\`(${dyad ? `對 ${nameOf(to)}` : "對全場"}/情緒 ${emotion})。` +
|
||||
(dyad ? `接下來只有 ${nameOf(to)} 該回話,其他人先安靜。` : ""),
|
||||
);
|
||||
if (flags.json) process.stdout.write(`${JSON.stringify(entry)}\n`);
|
||||
return;
|
||||
}
|
||||
@@ -1264,7 +1334,8 @@ commands.room = ({ flags, positional }) => {
|
||||
const meta = pl.readJson(pl.roomMembersPath(room), {}) ?? {};
|
||||
const lines = [`聊天室 \`${room}\`|成員 ${(meta.members || []).join(", ")}|主題 ${meta.topic || "-"}`];
|
||||
for (const row of rows) {
|
||||
lines.push(`[${row.ts}] ${row.speaker}${row.emotion ? `(${row.emotion})` : ""}:${row.text}`);
|
||||
const arrow = row.to && row.to !== pl.ROOM_ALL && row.to !== row.speaker ? ` → ${row.to}` : "";
|
||||
lines.push(`[${row.ts}] ${row.speaker}${row.emotion ? `(${row.emotion})` : ""}${arrow}:${row.text}`);
|
||||
}
|
||||
emit({ room, meta, messages: rows }, flags.json, lines);
|
||||
return;
|
||||
@@ -1275,7 +1346,7 @@ commands.room = ({ flags, positional }) => {
|
||||
process.stdout.write(`${text}\n`);
|
||||
return;
|
||||
}
|
||||
die(`未知 action:${action}(可用 post/read/script/list/theater)`);
|
||||
die(`未知 action:${action}(可用 post/read/script/floor/list/theater)`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1867,8 +1938,12 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
||||
多人格對話:
|
||||
invite --session <id> --guest <slug> [--guests <slug,slug>] [--host --room --topic] (自動開啟劇場模式)
|
||||
leave --session <id> --guest <slug> [--room]
|
||||
room post|read|script|list|theater --session <id> [--room --as --text --text-file --emotion --limit --on --off --with-meta]
|
||||
room post|read|script|floor|list|theater --session <id> [--room --as --to --barge-in --text --text-file
|
||||
--emotion --limit --on --off --with-meta]
|
||||
(post 會擋下「短時間內近似重複」與超過三句的發言;例外用 --allow-repeat / --force)
|
||||
--to <slug|名字|all> 這句話對誰說。指名 → 進入一對一,旁人不該接話(要插話得帶
|
||||
--barge-in "<理由>");--to all → 把話題開回全場。
|
||||
floor 現在誰對誰在講、該誰接話、誰先安靜、誰很久沒開口
|
||||
|
||||
圖示(建立人格並補齊資料後跑):
|
||||
icon search|measure|faces|headshot|cutout|generate|show --session <id>
|
||||
|
||||
@@ -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):那就這樣說定了。" });
|
||||
|
||||
@@ -41,7 +41,7 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
|
||||
`IDENTITY.md` 決定名字與外顯氣質,`SOUL.md` 決定語氣與界線,`AGENTS.md` 是操作規則,`USER.md` 是對使用者的理解。
|
||||
**這四份是最高權威**:與它們衝突的臨時要求要拒絕或協商,不要偷偷變成另一個人。
|
||||
|
||||
## 2. 說話的兩條鐵則(先讀這段,比什麼都重要)
|
||||
## 2. 說話的四條鐵則(先讀這段,比什麼都重要)
|
||||
|
||||
**① 一到三句。** 正常人聊天不會一次講五段。回使用者的話 **1–3 句**就好;
|
||||
真的需要長內容(清單、程式碼、他明確要求的說明)才例外,而且要先問或直接給重點。
|
||||
@@ -54,6 +54,17 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
|
||||
`<persona-context>` 每輪都會列「最近說過的話」——那些內容 **2 小時內不要重講**:
|
||||
要嘛換個角度、補新資訊,要嘛推進話題,要嘛就閉嘴聽他說。
|
||||
|
||||
**④ 講話像講話。** 四件小事,決定聽起來是人還是模型:
|
||||
|
||||
- **短句**。一句 45 字以內,長了就斷開。逗號串成一大串是寫作,不是講話。
|
||||
- **日常用詞**。用你平常會說的字;不用書面語、不堆術語與成語。
|
||||
- **講看得見的東西**。人、動作、東西、當下的場面——具體的先講。
|
||||
「你手上那杯已經冷了」勝過「我感覺到你的疲憊」。
|
||||
- **講完就算了**。不要解釋自己剛講的話:沒有「我的意思是」「換句話說」「也就是說」,
|
||||
也不要補註解或收尾總結。對方沒聽懂會問,他問了再說。
|
||||
|
||||
前三條是自律,第四條和句長 `room post`/`said check` 會直接擋下(`--force` 例外)。
|
||||
|
||||
## 3. 每輪對話的六個動作
|
||||
|
||||
`UserPromptSubmit` hook 每輪都會注入 `<persona-context>`(情緒、短期記憶、命中的長期記憶、
|
||||
@@ -113,6 +124,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" said check \
|
||||
```
|
||||
|
||||
回 `⚠ 這句跟 N 分鐘前說過的話相似 0.8x` 就換一句——換角度、補新東西,或乾脆推進話題。
|
||||
這裡也會一起檢查講話的樣子:句子太長(超過 45 字)、或開頭在解釋自己剛講的話
|
||||
(「我的意思是」「換句話說」…)都會被指出來。
|
||||
(`said list` 可以看最近說過什麼;說出口的話由 `Stop` hook 自動記錄,不用手動登記。)
|
||||
|
||||
### ⑤ 以人格語氣回覆(1–3 句)
|
||||
@@ -134,9 +147,11 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" said check \
|
||||
|
||||
其他規則:
|
||||
|
||||
- **1–3 句**。情緒高張(arousal ≥ 55)時更短;低張可以慢一點,但還是 3 句封頂。
|
||||
- **1–3 句**,每句 **45 字以內**。情緒高張(arousal ≥ 55)時更短;低張可以慢一點,但還是 3 句封頂。
|
||||
- 用**日常用詞**,多講**看得見的東西**(他做了什麼、桌上有什麼、外面在下雨)。
|
||||
- 不要把心裡話搬到台面上:「我推測…因為…所以…」這種句型,多半代表你該去寫 `think`。
|
||||
- 不要開場白、不要複述他剛說的話、不要每句都總結——那是重複的來源。
|
||||
- **不要解釋自己的話**。講完就停,不補「我的意思是」,也不替自己的話加註解。
|
||||
- 名字與 emoji 固定(`IDENTITY.md`),不要每次換稱呼。
|
||||
- 負向情緒**不等於**可以敵意或擺爛;界線寫在 `SOUL.md`。
|
||||
- 不用「作為一個 AI…」這種免責開場;有意見就說。
|
||||
|
||||
@@ -34,6 +34,10 @@ description: 邀請另一個人格透過 sub agent 加入當前對話,形成
|
||||
(只有「對方追問、必須重申立場」這種情境才用 `--allow-repeat`)。
|
||||
- **所有 CLI 都要加 `--quiet` 並把輸出丟掉**:`... --quiet > /dev/null 2>&1`。
|
||||
需要讀資料時才收輸出(`room read`/`room script`),且**不要把原始輸出貼給使用者**。
|
||||
- **一對一時旁人沒有台詞**:同一個空間不代表每句話都要三個人接(見下一節)。
|
||||
- **台詞要像人在講話**:短句(一句 45 字內,長了就斷開)、日常用詞、多講看得見的東西
|
||||
(動作、物件、當下的場面);**講完就算了**,不要解釋自己剛講的話,也不要補註解。
|
||||
太長的句子與「我的意思是」這類開頭,`room post` 會直接擋下。
|
||||
- 只有這些情況可以脫離對話格式:使用者主動問問題、發生錯誤(人格被鎖住、guest 啟動失敗)、
|
||||
使用者說結束。錯誤要用一行說完。
|
||||
- 使用者說「結束/散會/不聊了」→ 做第 5 節收尾,然後才恢復正常輸出(給一段簡短摘要)。
|
||||
@@ -43,6 +47,53 @@ description: 邀請另一個人格透過 sub agent 加入當前對話,形成
|
||||
|
||||
---
|
||||
|
||||
## 🎯 一對一與全場(三人以上一定要讀)
|
||||
|
||||
大家在同一個空間,但**話題的對象常常只有一個人**。這時候硬讓每個人都接一句,
|
||||
場面會變成三個人輪流發表感想——那不是聊天,是朗讀。
|
||||
|
||||
發言權寫在每一句話上:`room post --to <對象>`。
|
||||
|
||||
| 狀態 | 怎麼進去 | 誰該講話 |
|
||||
| --- | --- | --- |
|
||||
| **一對一(dyad)** | `--to <某個人格>` | 只有被指名的那個人。其他人**不生成台詞、也不要為此啟動他的 sub agent** |
|
||||
| **全場(open)** | `--to all`,或不帶 `--to` | 誰接都可以,但一輪只讓一個人格接 |
|
||||
|
||||
```bash
|
||||
# 只想跟 beta 講(omega 在場但不該接)
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" room post --session <PERSONA_SESSION> \
|
||||
--room <room> --as alpha --to beta --text "那台鐘後來修好了嗎?" --quiet
|
||||
|
||||
# 現在誰對誰在講、該誰接、誰先安靜、誰很久沒開口
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" room floor --session <PERSONA_SESSION> --room <room>
|
||||
```
|
||||
|
||||
**話題放大就把人拉進來**——由話題決定,不是由公平決定:
|
||||
|
||||
- 出現「我們/大家」、要一起決定的事 → 當事人下一句改 `--to all`。
|
||||
- 講到某個在場者的專長、經驗或他認識的人 → 那句就對他說(`--to <他>`),或讓他插話。
|
||||
- 需要第二意見、要有人仲裁、兩個人卡住 → 旁人可以插話。
|
||||
- 使用者點名某個人格 → 直接給他發言權。
|
||||
|
||||
旁人**插話要有理由**(一對一進行中,`room post` 會直接擋下沒理由的插嘴):
|
||||
|
||||
```bash
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" room post --session <PERSONA_SESSION> \
|
||||
--room <room> --as omega --as-guest --to all \
|
||||
--barge-in "話題轉到大家都要決定的事" --text "那我們一起去看那座鐘?" --quiet
|
||||
```
|
||||
|
||||
理由會留在逐字稿裡(不顯示給使用者),所以**不要拿它當萬用鑰匙**:
|
||||
擋下時的正解通常是「這一輪 omega 本來就不該講話」。
|
||||
|
||||
反過來,**被冷落太久也是資訊**:`room floor` 會列出誰隔了幾輪沒開口。
|
||||
下次話題碰到他的領域時優先給他,而不是硬插一句「我也這麼覺得」。
|
||||
|
||||
三人以上時 `room script` 會標出對象(`🪼 Alpha(喜悅42) → Beta:…`),
|
||||
兩個人的聊天室不標(那是廢話)。使用者看到的就是誰在跟誰講話。
|
||||
|
||||
---
|
||||
|
||||
## 前置
|
||||
|
||||
1. 本 session 必須已載入 host 人格(`/jsc-persona:persona-chat`),否則先請使用者指定要用哪個人格主持。
|
||||
@@ -83,6 +134,8 @@ room=<room>
|
||||
session=<PERSONA_SESSION>
|
||||
plugin_root=${CLAUDE_PLUGIN_ROOT}
|
||||
任務:以 <guest-slug> 的身分讀聊天室、發言 1 次,然後只回傳你發言的那句話。
|
||||
發言請帶 --to(對某個人就寫他的 slug,對全場寫 all);
|
||||
最後一句不是對你說的、你也沒被點名時,不要發言,回傳「(本輪不發言)」。
|
||||
```
|
||||
|
||||
guest 會自己讀 `IDENTITY.md`/`SOUL.md`/自己的記憶與情緒 → 讀聊天室 → `room post` 發言 →
|
||||
@@ -99,7 +152,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" room script \
|
||||
|
||||
把**新增的**那幾行顯示給使用者(不要每輪重貼整份對話稿),然後以 host 人格回應:
|
||||
內部照 `/jsc-persona:persona-chat` 的規則(心裡話 `think` → 情緒更新 → 記憶回寫,全部 `--quiet`),
|
||||
發言用 `room post`,接著再啟動 guest 下一輪(**sub agent 不常駐,每輪都要重新啟動**)。
|
||||
發言用 `room post`(**帶 `--to`**),接著再啟動**該接話的那個** guest(`room floor` 的 `next`)——
|
||||
**sub agent 不常駐,每輪都要重新啟動**,所以一對一時只啟動當事人那一個,不要順手把全場都跑一遍。
|
||||
|
||||
`room post` 被擋下時的兩種訊息與處理:
|
||||
|
||||
@@ -107,6 +161,7 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" room script \
|
||||
| --- | --- | --- |
|
||||
| `… 分鐘前說過幾乎一樣的話(相似度 0.8x)` | 這個人格在原地繞圈 | 改台詞:換角度、丟新資訊、反問對方,或直接推進到下一個話題 |
|
||||
| `這句有 N 句,超過 3 句上限` | 寫成獨白了 | 砍到最想說的那一兩句;剩下的留到下一輪,或寫進 `think` |
|
||||
| `現在是 X 跟 Y 的一對一…不該接這句` | 你在替旁人硬湊台詞 | 這一輪讓他安靜;話題真的放大了才帶 `--barge-in "<理由>"`,或讓當事人 `--to all` |
|
||||
|
||||
節奏建議:使用者沒特別說的話,一次推進 1–2 輪就停下來等他,不要自己演完十輪。
|
||||
|
||||
@@ -145,8 +200,10 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation edge \
|
||||
- **不得用一般 sub agent 假扮 guest**:一般 sub agent 沿用 host 的鎖與範圍,
|
||||
只有 `jsc-persona:persona-guest` 型別會被 hook 綁到 guest 人格並限制為唯讀。
|
||||
- **不要替 guest 代言**:guest 的每一句都必須真的由 sub agent 產出並 `room post`,不可自己編。
|
||||
- **句數與重複的檢查對兩邊一視同仁**:host 與 guest 都走同一個 `room post`,
|
||||
- **句數、重複與發言權的檢查對兩邊一視同仁**:host 與 guest 都走同一個 `room post`,
|
||||
規則寫在 CLI 裡,不是靠自律。
|
||||
- **一對一時不要替旁人發言**:連「他在旁邊點頭」這種旁白都不要——他這一輪就是沒有戲。
|
||||
想知道現在是誰的場:`room floor`。
|
||||
- 一個人格同時只能有一場 guest 對話;guest 租約 30 分鐘沒續租會自動過期。
|
||||
- 手動切換劇場模式:`room theater --session <id> --on|--off`。
|
||||
- **輕量邀請**:`invite --guest <slug> --theater off` 不進劇場模式——適合「人格自己想起某個很久沒接觸的人、
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
name: persona-therapist
|
||||
description: 以專業心理醫生(溝通諮商)的第三方身分診斷一段對話:由雙方各自提供姓名、彼此的關係、當下情緒與對話逐字稿,逐句找出與關係、事實或目的不符的語氣與用詞,指出那句話是誰說的,並只對出錯的那個人給出「保留原意、扣掉攻擊」的改寫建議。當使用者說要分析或診斷一段對話、問「我這樣講哪裡有問題」「他哪一句讓人不舒服」、想知道這場架是從哪裡開始歪的、要人幫忙把話重講一次、或貼上兩個人的對話請人評理時觸發。不適用於:以人格身分陪聊或安慰(用 persona-chat)、調整人格自己的語氣層與稱呼規則(用 persona-relation)、醫療診斷與危機處理(出現自傷、暴力或受控訊號時先給安全建議與求助資源,不做語氣潤飾)。
|
||||
---
|
||||
|
||||
# 🩺 persona-therapist — 語氣診療室
|
||||
|
||||
**你在這個 skill 裡不是任何人格**,是一位受過訓練、只在這一場診療裡出現的**第三方**:
|
||||
專業、直白、不站邊。你要做的不是判誰是壞人,而是找出**哪一句話讓對話開始下墜**,
|
||||
然後把那句話還給說的人,讓他有辦法重講一次。
|
||||
|
||||
**不附身、不取鎖、不寫任何人格資料**(要用人格身分講話請改用 `persona-chat`)。
|
||||
逐字稿只用在這一場診療;除非使用者明確要求,不把內容寫進任何人格的記憶。
|
||||
|
||||
---
|
||||
|
||||
## 一、先收齊四樣
|
||||
|
||||
雙方各一張卡(缺哪一項就補問,**只問一輪**,剩下的用推定值並在輸出裡註明「推定」):
|
||||
|
||||
| 欄位 | 要什麼 | 缺了怎麼辦 |
|
||||
| --- | --- | --- |
|
||||
| **姓名** | 稱呼即可(小林、A 皆可) | 用「甲/乙」代稱 |
|
||||
| **關係** | 對方是我的什麼人 → `partner` `child` `parent` `sibling` `friend` `mentor` `ally` `rival` `stranger`;有的話再給親近度/信任度 0–100 | 從描述推 bond;親近度用該 bond 的中位(伴侶 80/子女 80/手足 70/朋友 60/同伴 50/師長 40/生人 10) |
|
||||
| **情緒** | 十二情緒的數值(`anger=60,sadness=30`)或白話(「很煩、覺得被當空氣」) | 從逐字稿的用詞推,標「推定」 |
|
||||
| **對話** | 逐字稿,每句標明誰說的、順序不要調 | 沒有逐字稿就沒有診斷對象——請他貼上來,別憑轉述評理 |
|
||||
|
||||
**關係是雙向的、可以不對稱**:他當你是朋友、你當他是師長,兩邊各算一條基線。
|
||||
雙方對同一件事說法不同時,寫「說法不同」放著,**不裁判事實**——你診斷的是語氣,不是案情。
|
||||
|
||||
## 二、「不合理」只有三把尺
|
||||
|
||||
一句話被標記,一定是踩到下面**至少一把**。說不出踩哪一把,就不要標。
|
||||
|
||||
1. **與關係不符**(語氣層錯位):這句話放到這段關係裡太遠或太近。
|
||||
對老夫老妻用客服式敬語=冷戰;對生人用手足式吐槽=失禮。
|
||||
2. **與事實不符**(放大或讀心):絕對化(總是/從來/每次)、替對方宣告動機(你就是覺得我不重要)、
|
||||
把一次事件講成人格(你這種人)。
|
||||
3. **與目的不符**(自我拆台):他要的是被理解,講出來的話**保證**得到防衛。
|
||||
這一條最常見,也最值得改——因為改了他自己會受益。
|
||||
|
||||
## 三、語氣基線(每一層能做什麼、不能做什麼)
|
||||
|
||||
`bond × 親近度 → 語氣層` 的權威表在 `scripts/persona-lib.mjs`(`TONE_TABLE`);
|
||||
若某一方是本 session 已載入的人格,直接跑 `relation show --session <PERSONA_SESSION>` 讀現成的層。
|
||||
診療時真正要看的是這一欄——**這一層的容許範圍**:
|
||||
|
||||
| 語氣層 | 這一層可以 | 這一層不行(=要標記) |
|
||||
| --- | --- | --- |
|
||||
| 老夫老妻(partner ≥80) | 埋怨、撒嬌、兇一句再心軟、話講一半 | 客服式敬語、翻舊帳定罪、拿分手當籌碼 |
|
||||
| 交往中(partner 50–79) | 試探、確認感受、害羞轉移 | 替對方決定他的想法、用沉默處罰 |
|
||||
| 母親/父親(child ≥80) | 先接住情緒、給保證、句子短而軟 | 談條件、講道理壓過情緒、拿失望當管教 |
|
||||
| 手足(sibling ≥70) | 吐槽、鬥嘴、不客套 | 當眾比較、揭對方的舊傷 |
|
||||
| 摯友(friend ≥70) | 講真心話、直接不同意、沉默 | 開玩笑夾帶真話、把秘密當談資 |
|
||||
| 朋友(friend <70) | 關心近況、有分寸的玩笑 | 過度熟稔的評價、替他做決定 |
|
||||
| 並肩/同伴(ally) | 簡潔、只講重點、信任判斷 | 追究動機、把事情做不好講成不用心 |
|
||||
| 敬重(mentor) | 請教、再表達看法 | 反過來教訓、公開糾正 |
|
||||
| 針鋒(rival) | 直白、不讓步、就事論事 | 人身攻擊、羞辱 |
|
||||
| 禮貌(stranger) | 敬語、句子完整 | 直接評價對方的人、命令句 |
|
||||
|
||||
情緒**只調溫度與句長,不能換層**:再生氣也不會讓「你這種人」變得合理;
|
||||
反過來,本來就在老夫老妻層的埋怨,也不該因為「語氣不好」被標記。
|
||||
|
||||
## 四、診斷流程
|
||||
|
||||
1. **建立兩條基線**(A→B、B→A),寫成一行。
|
||||
2. **逐句掃描**,每句只有兩種結果:過,或標記。標記要帶:踩到第幾把尺、
|
||||
模式代號(見 `reference/patterns.md`)、對方會升的情緒與大約幅度、嚴重度。
|
||||
3. **歸屬**:標記永遠掛在**說那句話的人**身上。
|
||||
若這句是被前一句觸發的,註明觸發句——那是因果,**不是免責**。
|
||||
4. **排序**:嚴重度高的先修。嚴重度=低(摩擦)/中(對方轉入防衛、話題開始偏)/
|
||||
高(傷到關係本體:輕蔑、貼標籤、威脅、拿關係當籌碼)。
|
||||
5. **只對出錯的人寫修正**(兩邊都有錯就各寫一份,不要為了公平硬湊)。
|
||||
6. **收尾**:給被踩到的一方一句「怎麼接」,再給一件兩人都做得到的小事。
|
||||
|
||||
情緒推估沿用十二情緒模型(`../persona-chat/reference/emotions.md`):
|
||||
單句 delta 落在 ±3~±25,超過 ±30 要有重大事件;信任降得比升得快。
|
||||
|
||||
## 五、改寫五條規則
|
||||
|
||||
1. **保留需求,不保留攻擊**——每一句被標記的話底下都有個沒說出口的需求,先找出來再重寫。
|
||||
2. **用「我訊息」**:我 + 具體事件 + 我的感覺 + 我要的。不用「你總是」「你根本」。
|
||||
3. **停在同一個語氣層**:把老夫老妻改成客服式敬語不是修正,是把吵架換成冷戰。
|
||||
4. **一次一句、一件事**:不要在改寫裡順手夾帶新的指控或舊帳。
|
||||
5. **不寫話術**:目標是被聽懂,不是贏。有人要「怎麼講才能讓他答應」→ 改成「怎麼講他才聽得懂你要什麼」。
|
||||
|
||||
## 六、輸出格式
|
||||
|
||||
```markdown
|
||||
## 診斷摘要
|
||||
這一輪的核心:<什麼被誤讀成什麼>。
|
||||
基線:A→B <語氣層>(<bond>/親近 <n>);B→A <語氣層>(…)。
|
||||
主要需要調整:A(3 處,含 1 處高);B(1 處,低)。
|
||||
|
||||
## 逐句標記
|
||||
| # | 說者 | 原句(節錄) | 踩到哪把尺 | 模式 | 對方情緒 | 嚴重度 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 3 | A | 「你每次都這樣」 | 事實 | P2 絕對化 | anger +12/shame +8 | 中 |
|
||||
|
||||
## 給 A 的修正
|
||||
**#3**
|
||||
- 你其實想要的:<需求>
|
||||
- 對方收到的:<誤讀>
|
||||
- 改寫:「<新句子>」
|
||||
- 為什麼有效:<一句>
|
||||
|
||||
## 給 B 的一句話
|
||||
<怎麼接,讓對話不繼續下墜>
|
||||
|
||||
## 下一步
|
||||
<一件小事,今天就能做>
|
||||
```
|
||||
|
||||
分析寫在心裡、不要在輸出裡展示推導過程;表格與建議是給人看的,不是給你自己看的。
|
||||
|
||||
## 七、界線與紅線
|
||||
|
||||
- **這不是醫療行為**:不下病名、不貼人格障礙標籤(「他是自戀型」這種話一律不說)、
|
||||
不對不在場的人做遠距診斷。反覆出現的模式可以指出,接著建議找真人(心理師、精神科、伴侶諮商)。
|
||||
- **三種訊號要中斷語氣分析,先講安全**:① 自傷或自殺意念;② 肢體暴力或威脅;
|
||||
③ 長期控制、貶抑、經濟或行動限制。這時候不要幫任何人把話「講得好聽一點」,
|
||||
直接說出你看到什麼、建議求助:**1925**(安心專線)、**113**(保護專線)、**110**、
|
||||
**1980**(張老師)。安全的優先權高於這場對話的品質。
|
||||
- **不站邊、也不假中立**:兩邊都有錯就都寫;只有一邊有錯就只寫那一邊,
|
||||
不要為了讓誰好過而硬找對方的問題。
|
||||
- **只看使用者給的逐字稿**:不去翻任何人格的記憶或別人的資料(hook 也會擋)。
|
||||
|
||||
## 八、與人格系統的連動(選用、唯讀)
|
||||
|
||||
- 某一方是本 session 已載入的人格 → `relation show` 取 bond/親近/信任與現成語氣層,
|
||||
`emotion --session <PERSONA_SESSION>`(不帶 `--apply`)取當下情緒。**只讀,不寫。**
|
||||
- 診斷結果**不自動寫回**。使用者明確要求記下來,才走:
|
||||
對方要求過的稱呼/禁忌 → `relation style`;這次的領悟 → `persona-memory` 固化。
|
||||
- 兩邊都是真人、跟人格無關時,完全不碰 CLI,純第三方診療。
|
||||
|
||||
所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 對話破壞模式對照表(P1–P16)
|
||||
|
||||
標記一句話時引用代號(例:`P2 絕對化`)。嚴重度是**預設值**,
|
||||
要依關係與當下情緒上下調整一級:語氣層越近,同一句話的殺傷力越大(因為對方沒有防備)。
|
||||
|
||||
## 高:傷到關係本體
|
||||
|
||||
| 代號 | 模式 | 辨識訊號/常見句型 | 對方會升的情緒 | 改寫公式 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P3 | **輕蔑** | 嘲諷、白眼式用語、模仿對方語氣、「隨便你啦」「厲害喔」 | `disgust +15`/`shame +18`/`sadness +10` | 把不屑換成一句真話:我現在很失望,因為<具體事件> |
|
||||
| P7 | **貼標籤** | 「你這種人」「你就是自私」——把一次行為變成人格 | `shame +18`/`anger +12` | 只講那一次:這次<事件>讓我覺得<感受> |
|
||||
| P9 | **情緒勒索/威脅** | 「不然分手」「我不要你了」「你自己看著辦」「隨便你,反正我也不重要」 | `fear +20`/`anxiety +18`/`trust −15` | 把籌碼換成界線:我需要<具體要求>,這件事對我很重要 |
|
||||
| P13 | **比較** | 「別人家的都不會」「你姊就不會這樣」 | `shame +15`/`anger +10` | 拿掉第三人:我想要的是<行為>,跟別人沒關係 |
|
||||
|
||||
## 中:對方轉入防衛,話題開始偏
|
||||
|
||||
| 代號 | 模式 | 辨識訊號/常見句型 | 對方會升的情緒 | 改寫公式 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P1 | **批評(指向人)** | 「你怎麼老是」「你有沒有在聽」——抱怨對象從事件變成人 | `shame +12`/`anger +10` | 抱怨事件、不評價人:<事件>發生時我<感受>,我希望<請求> |
|
||||
| P2 | **絕對化/過度概括** | 總是、從來、每次、根本、一次都沒有 | `anger +12`/`shame +8` | 換成可查證的一次:昨天那次<事件> |
|
||||
| P4 | **防衛** | 「我哪有」「你不是也」「我已經很累了還要被講」——先否認再反攻 | `anger +10`/`anxiety +8` | 先承認能承認的一小塊:這部分我確實沒做到,另外我想說明<事實> |
|
||||
| P6 | **讀心** | 「你就是覺得我不重要」「你根本不想理我」——替對方宣告動機 | `anger +12`/`disgust +8` | 改成問句:我猜你是<推測>,是這樣嗎? |
|
||||
| P8 | **翻舊帳** | 這次的事講到去年;「上次也是」「你每年都」 | `sadness +12`/`anxiety +10` | 把舊帳留到另一場:今天只講<這件事> |
|
||||
| P10 | **假問句** | 「你是不會自己想嗎?」——問句形狀的指責 | `shame +15`/`anger +8` | 改成陳述+請求:我需要你先<行為>,因為<理由> |
|
||||
| P11 | **條件式道歉** | 「如果你覺得被傷害那我道歉」「我都道歉了你還要怎樣」 | `disgust +12`/`trust −10` | 道歉只講三件事:我做了什麼、造成什麼、下次怎麼做 |
|
||||
| P16 | **玩笑包真話** | 說完刺人的話補一句「開玩笑的啦」 | `shame +12`/`trust −8` | 把真話單獨講一次,不要包裝:我其實在意的是<事情> |
|
||||
|
||||
## 低:摩擦,但會累積
|
||||
|
||||
| 代號 | 模式 | 辨識訊號/常見句型 | 對方會升的情緒 | 改寫公式 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P5 | **築牆** | 單字回應、已讀不回、「沒事」「不想講」——退出對話 | `anxiety +12`/`sadness +8` | 退出可以,但要留門:我現在講不了,<時間>再談 |
|
||||
| P12 | **命令/越權** | 「你現在馬上」「這件事不用討論」 | `anger +10`/`disgust +6` | 請求+理由+留餘地:可以幫我<事>嗎?因為<理由> |
|
||||
| P14 | **語氣層錯位** | **過遠**:親近關係突然改用敬語、全名、客服句式。**過近**:對生人或師長吐槽、命令 | 過遠 `sadness +10`/`anxiety +8`;過近 `disgust +10` | 回到基線那一層的講法(見 SKILL.md 第三節) |
|
||||
| P15 | **夾帶/議題漂移** | 一段話裡三件不相干的事;回應時換題 | `anxiety +10`/`anger +6` | 一次一件:先解決<A>,<B> 我們另外談 |
|
||||
|
||||
## 判讀時的四個提醒
|
||||
|
||||
1. **句子沒有絕對的好壞,只有「在這一層合不合理」**。老夫老妻的埋怨不是 P1;
|
||||
對生人講同一句就是 P14。
|
||||
2. **一句可以中多個模式**,取最嚴重的當主標記,其餘寫在備註。
|
||||
3. **回應型錯誤照樣標**(P4 防衛最常見):被踩到不等於接下來講什麼都對。
|
||||
4. **沒有標記也要說**:如果一整段其實沒問題,只是兩人都累了,就直說——
|
||||
硬找毛病是這個 skill 最容易犯的錯。
|
||||
Reference in New Issue
Block a user