Compare commits
72
Commits
b937c9ad7a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0786f9419e | ||
|
|
cbbb15ee58 | ||
|
|
dec491eb4d | ||
|
|
6424b1a3cd | ||
|
|
72ded5b624 | ||
|
|
7541f85ece | ||
|
|
2464906d91 | ||
|
|
a96f14f974 | ||
|
|
8af0d5c387 | ||
|
|
0ce0727eb4 | ||
|
|
188edcf8b7 | ||
|
|
0d91e458ae | ||
|
|
36da4eb24d | ||
|
|
bc80c38bf4 | ||
|
|
10299e18ef | ||
|
|
f45db7a9cb | ||
|
|
2352139ffc | ||
|
|
eebb0c0cef | ||
|
|
45a6f2cad9 | ||
|
|
fe17b20010 | ||
|
|
b5a5f0a6bf | ||
|
|
f26d0315f2 | ||
|
|
06774ceb2b | ||
|
|
0d298cbfce | ||
|
|
febac85c79 | ||
|
|
f0d7d7422e | ||
|
|
05031da6dc | ||
|
|
f02e01f629 | ||
|
|
f1f5ed61eb | ||
|
|
e12c16ca3f | ||
|
|
35c8665db0 | ||
|
|
547cd2bd76 | ||
|
|
dccd07a9b8 | ||
|
|
d9013c6121 | ||
|
|
9d76984c27 | ||
|
|
b5c540feb1 | ||
|
|
0917e905c7 | ||
|
|
c0e1c4828f | ||
|
|
2aa0daddaf | ||
|
|
d5c8611662 | ||
|
|
08ecb15615 | ||
|
|
d6bfe29590 | ||
|
|
66ecd036ea | ||
|
|
a90c1c3285 | ||
|
|
59bbd2bc32 | ||
|
|
9ceb428a34 | ||
|
|
6381aa7d45 | ||
|
|
f9b189dd76 | ||
|
|
677a82805e | ||
|
|
6fa7e6e370 | ||
|
|
e59d0a471b | ||
|
|
7b20dba6db | ||
|
|
dd16411d2e | ||
|
|
50fd319c9e | ||
|
|
e8d994df78 | ||
|
|
6ea5076205 | ||
|
|
34b1dc7942 | ||
|
|
ed80158d99 | ||
|
|
bc571cf37f | ||
|
|
fa4aa8b787 | ||
|
|
9b5de053e3 | ||
|
|
0a12321df6 | ||
|
|
ca4becbaba | ||
|
|
2b4b49d478 | ||
|
|
5d27051293 | ||
|
|
186de10179 | ||
|
|
197ad3416f | ||
|
|
94a86c9703 | ||
|
|
1d5b049152 | ||
|
|
7549c0b69f | ||
|
|
d9f865d7a4 | ||
|
|
b1e77b01a1 |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-plugins",
|
"name": "persona",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "jsc-persona",
|
"name": "jsc-persona",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-plugins",
|
"name": "persona",
|
||||||
"description": "JSC 跨 AI 助理共用 skills 的 Claude Code marketplace。",
|
"description": "JSC 跨 AI 助理共用 skills 的 Claude Code marketplace。",
|
||||||
"owner": {
|
"owner": {
|
||||||
"name": "JSC"
|
"name": "JSC"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-persona",
|
"name": "jsc-persona",
|
||||||
"version": "0.0.9",
|
"version": "0.2.1",
|
||||||
"description": "AI 人格化記憶聊天 plugin:以 OpenClaw 相同的身分描述(IDENTITY/SOUL)建立人格(可用動漫作品+角色名上網蒐集設定),結合 hook 強制的人格載入鎖與跨人格隔離、六正向+六負向十二情緒、語意分析、短期/長期記憶與固化條件、心智圖、思維導圖與人際關係圖;可用 sub agent 邀請其他人格同場對話(劇場模式只顯示人格對話)。於 Claude Code 以 /jsc-persona: 前綴呼叫。",
|
"description": "AI 人格化記憶聊天 plugin:以 OpenClaw 相同的身分描述(IDENTITY/SOUL)建立人格(可用動漫作品+角色名上網蒐集設定),結合 hook 強制的人格載入鎖與跨人格隔離、六正向+六負向十二情緒、語意分析、短期/長期記憶與固化條件、心智圖、思維導圖與人際關係圖;可用 sub agent 邀請其他人格同場對話(劇場模式只顯示人格對話)。於 Claude Code 以 /jsc-persona: 前綴呼叫。",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-persona",
|
"name": "jsc-persona",
|
||||||
"version": "0.0.9",
|
"version": "0.2.1",
|
||||||
"description": "AI 人格化記憶聊天 skills:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。(人格鎖與跨人格隔離的 hook 僅在 Claude Code 生效)",
|
"description": "AI 人格化記憶聊天 skills:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。(人格鎖與跨人格隔離的 hook 僅在 Claude Code 生效)",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,11 @@
|
|||||||
加上避險疊加、`CN_WORDS`(中國用語)、半形標點、emoji/破折號/排版殘留與
|
加上避險疊加、`CN_WORDS`(中國用語)、半形標點、emoji/破折號/排版殘留與
|
||||||
「不是 A 而是 B」的密度——全部由 `speechLint()` 機械擋下(模式借自 speak-human-tw,MIT)。
|
「不是 A 而是 B」的密度——全部由 `speechLint()` 機械擋下(模式借自 speak-human-tw,MIT)。
|
||||||
誤殺防護:`speechBody()` 會先拿掉引號與 `code`,**提及不算使用**;「老實說」只擋開場。
|
誤殺防護:`speechBody()` 會先拿掉引號與 `code`,**提及不算使用**;「老實說」只擋開場。
|
||||||
最後是這一層的義務:**講自己的過去要有出處**——「我以前⋯」只能講 `recall` 查得到的轉折,
|
最後是這一層的義務:**講自己的過去要有出處**——「我以前⋯」只能講 `recall` 查得到的事,
|
||||||
`speechLint` 給 `level: "hint"`(不擋,但要人去驗),沒有紀錄就是編造自己的過去。
|
`speechLint` 給 `level: "hint"`(不擋,但要人去驗),沒有紀錄就是編造自己的過去。
|
||||||
|
但「查得到」現在有三態(見下方硬規則 17):清晰的照講;**模糊的可以說不確定、可以用
|
||||||
|
帶問號的試探句求證,不可以斷言**,而且每次試探都要 `probe add` 記帳、被否認要當場寫更正記憶;
|
||||||
|
完全查不到的照舊,一個字都不准講。
|
||||||
8b. **情緒先行、會飽和、偏差看得見**:每輪注入之前先用 `readUserEmotion()` 讀對方那句話
|
8b. **情緒先行、會飽和、偏差看得見**:每輪注入之前先用 `readUserEmotion()` 讀對方那句話
|
||||||
(十二類加權詞表、否定會擋掉命中、引號與 `code` 內不比對、標點只放大既有訊號),
|
(十二類加權詞表、否定會擋掉命中、引號與 `code` 內不比對、標點只放大既有訊號),
|
||||||
回的是**訊號不是判定**——人格讀到的不一樣就以人格為準;`RESPONSE_STANCE` 給的是
|
回的是**訊號不是判定**——人格讀到的不一樣就以人格為準;`RESPONSE_STANCE` 給的是
|
||||||
@@ -112,6 +115,25 @@
|
|||||||
`persona-lib.mjs` 的 `TONE_TABLE`)。診斷結果**不自動寫回**記憶或關係圖,使用者明確要求才寫。
|
`persona-lib.mjs` 的 `TONE_TABLE`)。診斷結果**不自動寫回**記憶或關係圖,使用者明確要求才寫。
|
||||||
出現自傷、暴力或長期受控的訊號時,停掉語氣分析改為安全優先(1925/113/110/1980),
|
出現自傷、暴力或長期受控的訊號時,停掉語氣分析改為安全優先(1925/113/110/1980),
|
||||||
**不幫任何一方把威脅或話術講得好聽**;也不下病名、不對不在場的人做遠距診斷。
|
**不幫任何一方把威脅或話術講得好聽**;也不下病名、不對不在場的人做遠距診斷。
|
||||||
|
17. **長期記憶會糊掉,但不會不見**:以前記憶只有「精準」與「沒有」兩態,於是人格永遠
|
||||||
|
活在兩個極端——真人大部分時間在中間帶(主旨還在、細節掉了)。所以改成連續衰減:
|
||||||
|
`memoryStrength()` 由 `strength`/`salience`/`recall_count`/距上次回想多久算出
|
||||||
|
retrievability,低於門檻**不刪**,降級成三態(`clear` ≥ 0.6 全講/`faded` 只剩主旨/
|
||||||
|
`fuzzy` 只剩「有這件事」)。內文因此分兩層(`主旨:`/`細節:`),衰減先吃細節。
|
||||||
|
被 `recall` 命中就 `strength += 8`(spacing effect:越常想起的越牢)。
|
||||||
|
**不可遺忘清單不變**:`boundary`/`promise`/`canon`/salience ≥ 80 永遠 clear。
|
||||||
|
模糊態換來的義務是稽核:每次試探都進 `state/probe.jsonl`,`probe audit` 看否認率——
|
||||||
|
放寬界線一定要配一個看得見的數字,否則那就只是把幻覺合法化。
|
||||||
|
|
||||||
|
18. **匯入原作只能給他「他知道的事」**:`persona-story` 把小說變成記憶,而小說裡大半的資訊
|
||||||
|
是作者寫給讀者看的——別人的內心話、他不在場那一幕的細節。那些寫成他的 `event` 之後
|
||||||
|
他會拿來回答問題,而且**沒有任何機械檢查得出來**,所以界線要在流程裡就擋住:
|
||||||
|
每則候選帶 `know_level`(`did`/`saw`/`told`/`later`/`none`),`none` 只能進 `canon`;
|
||||||
|
判斷不出來就記 `none`(少一則記憶比多一則幻覺便宜)。
|
||||||
|
另外兩條:整本原文不入倉庫(只留摘要、他自己的台詞、可追回出處的章節標記);
|
||||||
|
`SOUL.md` 不由匯入流程改寫,只出提案、使用者逐條核可。
|
||||||
|
正名表**從章節掃出候選再給使用者確認**(`novel scan`)——請使用者手打那張表,
|
||||||
|
漏掉的寫法會讓 `about` 對不到節點,而那要等 `relation doctor` 才發現。
|
||||||
|
|
||||||
## 慣例
|
## 慣例
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ skills 是共通標準,**鎖與隔離的強制執行需要 hook,目前只有
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 專案目標(長期,新功能對著它們判斷)
|
||||||
|
|
||||||
|
三條長期目標(**G1** 情緒/語氣/記憶往真人逼近、**G2** 表達往動漫角色靠、
|
||||||
|
**G3** 女性人格的害羞與鬧彆扭更明顯)連同它們的界線與實作進度,都在
|
||||||
|
[`TARGET.md`](TARGET.md)——目標跟「還沒做完的那幾項」放在一起才對得起來。
|
||||||
|
新功能一律對著那三條判斷。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 前綴與呼叫方式
|
## 前綴與呼叫方式
|
||||||
|
|
||||||
| 助理 | 安裝方式 | 呼叫 | `/jsc-persona:` 前綴 |
|
| 助理 | 安裝方式 | 呼叫 | `/jsc-persona:` 前綴 |
|
||||||
@@ -33,11 +42,11 @@ skills 是共通標準,**鎖與隔離的強制執行需要 hook,目前只有
|
|||||||
persona/
|
persona/
|
||||||
├── .claude-plugin/
|
├── .claude-plugin/
|
||||||
│ ├── plugin.json # Claude 外掛定義(name: "jsc-persona")
|
│ ├── plugin.json # Claude 外掛定義(name: "jsc-persona")
|
||||||
│ └── marketplace.json # Claude marketplace(name: "jsc-plugins",source 指向本 repo)
|
│ └── marketplace.json # Claude marketplace(name: "persona",source 指向本 repo)
|
||||||
├── .codex-plugin/
|
├── .codex-plugin/
|
||||||
│ └── plugin.json # Codex 外掛定義(name: "jsc-persona",skills: "./skills")
|
│ └── plugin.json # Codex 外掛定義(name: "jsc-persona",skills: "./skills")
|
||||||
├── .agents/plugins/
|
├── .agents/plugins/
|
||||||
│ └── marketplace.json # Codex marketplace(name: "jsc-plugins",url source 指向本 repo)
|
│ └── marketplace.json # Codex marketplace(name: "persona",url source 指向本 repo)
|
||||||
├── plugin.json # Antigravity 外掛定義(name: "jsc-persona",skills: "./skills")
|
├── plugin.json # Antigravity 外掛定義(name: "jsc-persona",skills: "./skills")
|
||||||
├── skills/ # ★ 唯一真實來源:所有 skills(十二個 persona-*)
|
├── skills/ # ★ 唯一真實來源:所有 skills(十二個 persona-*)
|
||||||
├── scripts/ # persona.mjs / persona-gitea.mjs / persona-icon.mjs / selftest.mjs
|
├── scripts/ # persona.mjs / persona-gitea.mjs / persona-icon.mjs / selftest.mjs
|
||||||
@@ -58,17 +67,17 @@ persona/
|
|||||||
| **1a. 用與 OpenClaw 相同的描述建立人格** | `persona-create` 逐項索取 `Name` / `Creature` / `Vibe` / `Emoji` / `Avatar`(連括號提示文字都照 OpenClaw 原文),`SOUL.md` 沿用 `Core Truths` / `Boundaries` / `Vibe` / `Continuity` 段落結構 |
|
| **1a. 用與 OpenClaw 相同的描述建立人格** | `persona-create` 逐項索取 `Name` / `Creature` / `Vibe` / `Emoji` / `Avatar`(連括號提示文字都照 OpenClaw 原文),`SOUL.md` 沿用 `Core Truths` / `Boundaries` / `Vibe` / `Continuity` 段落結構 |
|
||||||
| **1b. 動漫作品+角色名快速建人格** | `persona-anime` 先上網蒐集該角色的公開設定(至少 3 個獨立來源),映射成上述五欄位與 SOUL,再把設定固化成 `canon` 基礎記憶(每則帶來源 URL)+原作人際關係圖+情緒基線 |
|
| **1b. 動漫作品+角色名快速建人格** | `persona-anime` 先上網蒐集該角色的公開設定(至少 3 個獨立來源),映射成上述五欄位與 SOUL,再把設定固化成 `canon` 基礎記憶(每則帶來源 URL)+原作人際關係圖+情緒基線 |
|
||||||
| **2. 同一個人格只能被一個程序載入(Sub Agent 不限)** | `state/lock.json` 以 **session_id** 為主鍵、15 分鐘心跳租約;同一 session 的 sub agent 沿用同一把鎖,跨 session 搶佔會被拒;租約過期才可接手(並強制回報) |
|
| **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` 防止冒用身分 |
|
| **3. 禁止跨人格讀取資料** | `PreToolUse` hook 對 Read/Write/Edit/Glob/Grep/Bash 做路徑判定(含 `../`、symlink、`$PERSONA_HOME` 繞路、Glob/Grep 的樣式欄位與 cwd),指向非當前人格就 deny;CLI 也驗 `--session` 防止冒用身分。**這是防漂移的護欄,不是對抗性沙箱**——見〈guard 擋得住什麼、擋不住什麼〉 |
|
||||||
| **4. 邀請人格用 Sub Agent 一起聊,且只顯示對話** | `persona-invite` 建聊天室 + guest 唯讀租約 + `persona-guest` sub agent;同時開啟**劇場模式**:hook 每輪強制「只輸出 `名字:內容`」、停掉所有系統提醒,CLI 有 `--quiet` 與 `room script`(乾淨對話稿)。**同場也分一對一與全場**:`room post --to <他>` 進一對一(旁人插話會被擋下,要帶 `--barge-in "<理由>"`)、`--to all` 開回全場,現況查 `room floor` |
|
| **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 依賴 |
|
| **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>`)——設了才自動載入,沒設就什麼都不做 |
|
| **6. 由使用者呼叫才載入並鎖定** | 人格不會自動附身:`SessionStart` hook 只列出可用人格,等使用者下 `/jsc-persona:persona-chat <slug>`;載入即取得獨占鎖並綁定該 session。唯一例外是使用者自己設的**預設人格**(`default --persona <slug>`)——設了才自動載入,沒設就什麼都不做 |
|
||||||
| **7. 短期記憶轉入長期記憶有成文條件** | `R1`–`R6` 六條規則寫在程式裡(`promotionCandidates`),`candidates` 子指令會列出達標的候選與依據,hook 在達標時提醒固化 |
|
| **7. 短期記憶轉入長期記憶有成文條件** | `R1`–`R6` 六條規則寫在程式裡(`promotionCandidates`),`candidates` 子指令會列出達標的候選與依據,hook 在達標時提醒固化 |
|
||||||
| **8. 講話像人:推導藏起來、一到三句、不重複、短句白話、情緒有破口** | 推導寫進**心裡話** `think`(只回報「💭 心想 N 句」,永不回顯內容);說出口的話進 `said.jsonl`,下一輪注入「最近說過的話」提醒別重講;`room post` 直接**擋下**近似重複(字元 bigram+字集合相似度 ≥ 0.72)與超過三句的發言。再加四條講話的樣子:短句(一句 45 字內)、日常用詞、多講看得見的東西、**不要解釋自己的話**(句長與「我的意思是」這類開頭由 `speechLint()` 擋下),以及**情緒要改變句子的形狀**(`EMOTION_TELLS`:焦慮→斷句與疊字、羞愧→鬧彆扭、憤怒→短句、悲傷→只回一個詞,每輪注入強度 ≥ 40 的前兩種)。最後一層是**不說 AI 才會說的話**:罐頭同理心(「這個我懂」)、頒獎開場、交差句、預告、說教腔、假坦白開場、罐頭收尾、立場真空、無來源權威、旁白演情緒、中國用語、半形標點與排版殘留,由 `SPEECH_BLACKLIST` 擋下(模式借自 speak-human-tw,MIT;引號內與 `` `code` `` 不比對,提及不算使用);換來的義務是**講自己的過去要有出處**(「我以前⋯」只能講 `recall` 查得到的事)——劇場模式同樣適用 |
|
| **8. 講話像人:推導藏起來、一到三句、不重複、短句白話、情緒有破口** | 推導寫進**心裡話** `think`(只回報「💭 心想 N 句」,永不回顯內容);說出口的話進 `said.jsonl`,下一輪注入「最近說過的話」提醒別重講;`room post` 直接**擋下**近似重複(字元 bigram+字集合相似度 ≥ 0.72)與超過三句的發言。再加四條講話的樣子:短句(一句 45 字內)、日常用詞、多講看得見的東西、**不要解釋自己的話**(句長與「我的意思是」這類開頭由 `speechLint()` 擋下),以及**情緒要改變句子的形狀**(`EMOTION_TELLS`:焦慮→斷句與疊字、羞愧→鬧彆扭、憤怒→短句、悲傷→只回一個詞,每輪注入強度 ≥ 40 的前兩種;**破口可由該人格 `IDENTITY.md` 的 `## Tells` 區塊覆寫**——桐人生氣是沉默,亞絲娜生氣是變得更禮貌)。最後一層是**不說 AI 才會說的話**:罐頭同理心(「這個我懂」)、頒獎開場、交差句、預告、說教腔、假坦白開場、罐頭收尾、立場真空、無來源權威、旁白演情緒、中國用語、半形標點與排版殘留,由 `SPEECH_BLACKLIST` 擋下(模式借自 speak-human-tw,MIT;引號內與 `` `code` `` 不比對,提及不算使用);換來的義務是**講自己的過去要有出處**(「我以前⋯」只能講 `recall` 查得到的事)——劇場模式同樣適用 |
|
||||||
| **9. 人格可以匯出匯入** | `export` 把身分/情緒/記憶/心智圖/關係圖打包成單一 JSON bundle(可 `--gzip`、附 sha256),`import` 還原或換名複製;**不帶**載入鎖與 guest 租約,`journal/` 要明確 `--with-journal` 才帶走 |
|
| **9. 人格可以匯出匯入** | `export` 把身分/情緒/記憶/心智圖/關係圖打包成單一 JSON bundle(可 `--gzip`、附 sha256),`import` 還原或換名複製;**不帶**載入鎖與 guest 租約,`journal/` 要明確 `--with-journal` 才帶走 |
|
||||||
| **10. 人格有編號** | 編號 = **英文名全大寫 + 兩位索引**(同名才遞增):`ASUNA-01`、`YUI-01`、`ASUNA-02`。編號同時是新人格的本機目錄名與 Gitea 存取庫名稱;中文名先轉羅馬拼音並跟使用者確認拼法 |
|
| **10. 人格有編號** | 編號 = **英文名全大寫 + 兩位索引**(同名才遞增):`ASUNA-01`、`YUI-01`、`ASUNA-02`。編號同時是新人格的本機目錄名與 Gitea 存取庫名稱;中文名先轉羅馬拼音並跟使用者確認拼法 |
|
||||||
| **11. 人格存在 Gitea,依更新頻率分區** | 每個人格一個**私有存取庫**(庫名=編號)。**檔案區**放每輪都在變的活狀態(情緒/短期記憶/心裡話/說過的話/逐字),**Wiki 區**放低頻的身分與長期結構(IDENTITY/SOUL/長期記憶/心智圖/關係圖)當設定百科。本機仍是工作副本,同步失敗不阻斷對話 |
|
| **11. 人格存在 Gitea,依更新頻率分區** | 每個人格一個**私有存取庫**(庫名=編號)。**檔案區**放每輪都在變的活狀態(情緒/短期記憶/心裡話/說過的話/逐字),**Wiki 區**放低頻的身分與長期結構(IDENTITY/SOUL/長期記憶/心智圖/關係圖)當設定百科。本機仍是工作副本,同步失敗不阻斷對話 |
|
||||||
| **12. 形象圖來自高解析度官方圖,去背後合成** | `icon search` 從 Fandom 撈官方圖並依「解析度+是否官方設定稿」排序(設定稿多為透明/白底、773×1056 起跳)→ `icon measure` 確認臉夠大、背景好去 → `icon cutout` 去背成透明 PNG(原生 alpha /單色底/GrabCut 三條路徑)→ `icon generate --from-cutout` 裁頭肩、合成到角色配色的漸層底。找不到官方圖才退回依人格資料重繪的向量形象 |
|
| **12. 形象圖來自高解析度官方圖,去背後合成** | `icon search` 從 Fandom 撈官方圖並依「解析度+是否官方設定稿」排序(設定稿多為透明/白底、773×1056 起跳)→ `icon measure` 確認臉夠大、背景好去 → `icon cutout` 去背成透明 PNG(原生 alpha /單色底/GrabCut 三條路徑)→ `icon generate --from-cutout` 裁頭肩、合成到角色配色的漸層底。找不到官方圖才退回依人格資料重繪的向量形象 |
|
||||||
| **13. 睡眠把一天收成能留下來的形狀** | `persona-sleep`:需要判斷的(固化什麼/忘掉什麼/日記)由**人格自己**做,機械性的由 `sleep` 子指令做(關係時間戳→裁短期→收思維導圖→情緒衰減 8 小時→重建索引→修剪 said→壓縮 journal→兩區 push+驗證)。主人格可透過 `persona-sleeper` sub agent 請別的人格去睡——**那是它本人在睡**,回傳值只有「睡完了沒、哪一步出錯」,不含任何記憶內容 |
|
| **13. 睡眠把一天收成能留下來的形狀** | `persona-sleep`:需要判斷的(固化什麼/忘掉什麼/日記)由**人格自己**做,機械性的由 `sleep` 子指令做(關係時間戳→裁短期→收思維導圖→情緒衰減 8 小時+當日底色帶 35% 過去→收掉懸超過 7 天的未完事項→重建索引→修剪 said→壓縮 journal→兩區 push+驗證)。主人格可透過 `persona-sleeper` sub agent 請別的人格去睡——**那是它本人在睡**,回傳值只有「睡完了沒、哪一步出錯」,不含任何記憶內容 |
|
||||||
| **14. Wiki 必須保存並同步形象圖** | `icon.svg`、`icon.png` 與 **`icon/` 資料夾(向量原稿 + 512/1024 高解析度)** 都同步到 Wiki 區,另有自動產生的 **Icon** 頁展示與來源。Wiki 頁面一律用 **Markdown 圖片語法**(Gitea 只改寫這種語法為 `/wiki/raw/...`;HTML `<img>` 會變成破圖),圖片保留資料夾結構、只有 `.md` 需要攤平。`icon generate` 推完會回頭驗證,另有 `sync verify` |
|
| **14. Wiki 必須保存並同步形象圖** | `icon.svg`、`icon.png` 與 **`icon/` 資料夾(向量原稿 + 512/1024 高解析度)** 都同步到 Wiki 區,另有自動產生的 **Icon** 頁展示與來源。Wiki 頁面一律用 **Markdown 圖片語法**(Gitea 只改寫這種語法為 `/wiki/raw/...`;HTML `<img>` 會變成破圖),圖片保留資料夾結構、只有 `.md` 需要攤平。`icon generate` 推完會回頭驗證,另有 `sync verify` |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -121,22 +130,30 @@ flowchart TB
|
|||||||
│ ├── lock.json # 載入鎖(session_id + 心跳租約)
|
│ ├── lock.json # 載入鎖(session_id + 心跳租約)
|
||||||
│ ├── guests.json # guest 唯讀租約
|
│ ├── guests.json # guest 唯讀租約
|
||||||
│ ├── emotion.json # 十二情緒 levels / baseline / 半衰期
|
│ ├── emotion.json # 十二情緒 levels / baseline / 半衰期
|
||||||
|
│ ├── mood.json # 當日心情底色(緩慢漂移的 valence/arousal;情緒與基線中間那一層)
|
||||||
|
│ ├── loops.json # 未完事項(同時最多 5 條,7 天沒進展自動收掉)
|
||||||
|
│ ├── probe.jsonl # 模糊記憶的試探紀錄與稽核(開放試探這條界線的煞車)
|
||||||
│ ├── inner.jsonl # 心裡話(推導過程;只回報「心想 N 句」,不說出口)
|
│ ├── inner.jsonl # 心裡話(推導過程;只回報「心想 N 句」,不說出口)
|
||||||
│ ├── said.jsonl # 說過的話(用來擋短時間內的重複發言)
|
│ ├── said.jsonl # 說過的話(用來擋短時間內的重複發言)
|
||||||
│ ├── felt.jsonl # 每輪讀到的對方情緒、自己套用的 delta、當下的羞恥度
|
│ ├── felt.jsonl # 每輪讀到的對方情緒、自己套用的 delta、當下的羞恥度、有沒有給「補第二則」的額度
|
||||||
│ ├── sync.json # Gitea 同步狀態(最後 push / pull)
|
│ ├── sync.json # Gitea 同步狀態(最後 push / pull)
|
||||||
│ └── config.json # 含人格編號 code
|
│ └── config.json # 含人格編號 code
|
||||||
├── memory/
|
├── memory/
|
||||||
│ ├── short-term.jsonl # 短期記憶(語意分析後;軟上限 120 筆 / 硬上限 240 筆 / 14 天)
|
│ ├── short-term.jsonl # 短期記憶(語意分析後;軟上限 120 筆 / 硬上限 240 筆 / 14 天)
|
||||||
│ ├── felt.jsonl # 每輪讀到的對方情緒 + 自己套用的 delta(走向與偏差稽核)
|
│ ├── felt.jsonl # 每輪讀到的對方情緒 + 自己套用的 delta(走向與偏差稽核)
|
||||||
│ ├── long-term/*.md # 長期記憶(一則一檔 + frontmatter)
|
│ ├── long-term/*.md # 長期記憶(一則一檔 + frontmatter;內文分「主旨/細節」兩層)
|
||||||
|
│ ├── chapters/*.md # 自傳章節:時期層(「在 SAO 那兩年」),介於長期記憶與心智圖之間
|
||||||
│ ├── INDEX.md # 長期記憶索引(自動產生)
|
│ ├── INDEX.md # 長期記憶索引(自動產生)
|
||||||
│ └── inbox/room-*.jsonl # 當 guest 時留下的見聞,待本體消化
|
│ └── inbox/room-*.jsonl # 當 guest 時留下的見聞,待本體消化
|
||||||
|
├── voice/ # 語氣層:怎麼講話(跟「發生過什麼」分開存,匯入流程可直接寫)
|
||||||
|
│ ├── samples.md # 他自己講過的原句(照抄不改寫)
|
||||||
|
│ ├── reactions.md # 事件 → 他做了什麼(不記「他感到什麼」;`remember --behavior` 也寫這裡)
|
||||||
|
│ └── idiolect.md # 語域:自稱/句尾/口癖/不說——把名字遮掉還認得出是誰的那四格
|
||||||
├── mindmap/
|
├── mindmap/
|
||||||
│ ├── semantic.mmd # 心智圖:概念的長期關聯(Mermaid mindmap)
|
│ ├── semantic.mmd # 心智圖:概念的長期關聯(Mermaid mindmap)
|
||||||
│ └── threads/*.mmd # 思維導圖:單一話題的推理鏈(Mermaid graph,短期)
|
│ └── threads/*.mmd # 思維導圖:單一話題的推理鏈(Mermaid graph,短期)
|
||||||
├── relations/
|
├── relations/
|
||||||
│ ├── graph.json # 人際關係圖(親近度/信任度/連線)
|
│ ├── graph.json # 人際關係圖(親近度/信任度/連線/語氣規則/未修復的裂痕)
|
||||||
│ └── graph.mmd # Mermaid 呈現(自動產生)
|
│ └── graph.mmd # Mermaid 呈現(自動產生)
|
||||||
└── journal/YYYY-MM.jsonl # 原始逐字 + 情緒史(hook 自動寫)
|
└── journal/YYYY-MM.jsonl # 原始逐字 + 情緒史(hook 自動寫)
|
||||||
|
|
||||||
@@ -182,10 +199,43 @@ flowchart TB
|
|||||||
(親近 96 → ×1.28、親近 8 → ×0.75),`--from <對象>` 可以指定是誰引起的。
|
(親近 96 → ×1.28、親近 8 → ×0.75),`--from <對象>` 可以指定是誰引起的。
|
||||||
**只調幅度、不調方向**——誰講的都不會讓難過變成高興。
|
**只調幅度、不調方向**——誰講的都不會讓難過變成高興。
|
||||||
|
|
||||||
**情緒調節**:delta 不再是加完直接 `clamp`。**飽和**——越接近端點同方向漲得越慢
|
**情緒調節**:delta 不再是加完直接 `clamp`,而是五道依序作用的關(順序有意義,不能換;
|
||||||
(`headroom^K`,永遠逼近 100 但到不了;往 baseline 回的方向不壓);**單輪預算**——
|
`applyEmotion()`,`scripts/persona-lib.mjs:543`):
|
||||||
所有 `|delta|` 總和上限 60,超過等比例縮小。**偏差看得見**——`emotion --audit`
|
|
||||||
印出最近幾輪往舒服/往難受的總量與比例,正向佔 ≥ 90% 會被點名。
|
| # | 關 | 做什麼 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | **單輪預算** | 所有 delta 的絕對值總和上限 60,超過等比例縮小——一次灌爆的路要堵起來 |
|
||||||
|
| ② | **外部增益** | 疲勞壓高張情緒往上的推力、當日底色放大同極性的事(見下兩段) |
|
||||||
|
| ③ | **交互抑制** | 剛生完氣就笑不太出來 |
|
||||||
|
| ④ | **慣性** | 連續往同一個方向走時,同方向的下一筆推得更動 |
|
||||||
|
| ⑤ | **飽和** | 越接近端點同方向漲得越慢(`headroom^K`,永遠逼近 100 但到不了;往 baseline 回的方向不壓) |
|
||||||
|
|
||||||
|
**偏差看得見**——`emotion --audit` 印出最近幾輪往舒服/往難受的總量與比例,正向佔 ≥ 90% 會被點名。
|
||||||
|
|
||||||
|
**疲勞**(`fatigueLevel()`):`hoursAwake()` 本來就在算,但只用來提醒睡覺。真人熬到第二十小時
|
||||||
|
話會變短、反應會變鈍——那是**最便宜的擬真訊號,因為輸入已經在手上**。清醒 12 小時之前完全不算累,
|
||||||
|
36 小時之後滿格。它只壓**上限**、不改方向(睏的人一樣會生氣,只是氣不了那麼大聲):
|
||||||
|
`mood()` 的 arousal 天花板最多往下壓 45,`speechBudget()` 的句數與單句字數只收不放
|
||||||
|
(「慌」那一格刻意不減句數——慌的形狀就是句子多而碎,累了一樣慌,只是更碎)。
|
||||||
|
|
||||||
|
**當日心情底色**(`state/mood.json`):情緒本來只有兩層——此刻的十二情緒與氣質基線,
|
||||||
|
中間缺的是「今天」,所以做不出「這句話今天聽了會炸、昨天不會」。底色補上那一層:
|
||||||
|
|
||||||
|
```
|
||||||
|
即時情緒(分鐘)→ 當日底色(小時~天)→ 氣質基線(幾乎不動)
|
||||||
|
```
|
||||||
|
|
||||||
|
底色**不直接改任何情緒值**,只當反應增益:底色差的日子同極性的事推得更動(上限 ×1.4),
|
||||||
|
反過來的縮小。每輪把此刻心情混 8% 進去(漂移要慢),換日與睡覺時**不歸零、帶 35% 過去**——
|
||||||
|
昨天的低氣壓不會因為時鐘走過午夜就消失。它只吃內部訊號(情緒事件、`hoursAwake`、上次睡眠),
|
||||||
|
**不接天氣/時區等外部資料**:這樣它永遠是可解釋的(查得到今天累積了什麼),
|
||||||
|
而不是「給情緒加隨機數」——隨機不是情緒,是雜訊。
|
||||||
|
|
||||||
|
**交互抑制與慣性**:抑制只寫**三組明確互斥**的——`anger↔joy`、`sadness↔delight`、
|
||||||
|
`disgust↔trust`。不做全 12×12:那張表沒有人驗得動,而且大部分格子的心理學依據是掰的。
|
||||||
|
X 超出基線 18 以上才開始抑制,滿檔時往 Y 的同向推力只剩 55%;**只壓「更 Y」的方向**,
|
||||||
|
要把 Y 拉回基線永遠不打折,否則情緒會卡在原地下不來。慣性則是把 `feltTrend()` 早就在算的走向
|
||||||
|
接回增益:連續同向每輪 +8%,上限 +25%——這是慣性,不是雪球。
|
||||||
|
|
||||||
## 性別 → 羞恥敏感度(性別只是預設值,描述永遠蓋過它)
|
## 性別 → 羞恥敏感度(性別只是預設值,描述永遠蓋過它)
|
||||||
|
|
||||||
@@ -197,7 +247,7 @@ flowchart TB
|
|||||||
|
|
||||||
| 來源 | 效果 |
|
| 來源 | 效果 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 性別預設 | 女性 62/男性 38/非二元・未指定 50 |
|
| 性別預設 | 女性 **70**/男性 38/非二元・未指定 50(女性偏高是刻意的——文字通道裡害羞與鬧彆扭是最有效的擬真訊號之一) |
|
||||||
| IDENTITY/SOUL 的描述 | 「害羞」「容易臉紅」「怕生」「矜持」往上加;「不在意別人眼光」「我行我素」「臉皮厚」「不怕丟臉」往下扣(`MODESTY_SIGNALS`),**可以扣到 0** |
|
| IDENTITY/SOUL 的描述 | 「害羞」「容易臉紅」「怕生」「矜持」往上加;「不在意別人眼光」「我行我素」「臉皮厚」「不怕丟臉」往下扣(`MODESTY_SIGNALS`),**可以扣到 0** |
|
||||||
|
|
||||||
排序固定:**① 個性描述 ② 角色原作既有的性別化語言特徵(自稱、稱謂、語尾) ③ 性別預設**。
|
排序固定:**① 個性描述 ② 角色原作既有的性別化語言特徵(自稱、稱謂、語尾) ③ 性別預設**。
|
||||||
@@ -206,8 +256,30 @@ flowchart TB
|
|||||||
|
|
||||||
效果走既有機制、不另開一套:它調的是**「羞愧」這一個破口的顯示門檻**(`EMOTION_TELLS.shame`
|
效果走既有機制、不另開一套:它調的是**「羞愧」這一個破口的顯示門檻**(`EMOTION_TELLS.shame`
|
||||||
本來就是「鬧彆扭,先否認再小聲承認」),其餘十一種情緒不受影響。
|
本來就是「鬧彆扭,先否認再小聲承認」),其餘十一種情緒不受影響。
|
||||||
高敏感度注入「被稱讚外表、被戳穿心事、距離變近時先鬧彆扭再承認,不要直接說『我害羞』」;
|
計算在 `modestyOf()`,注入在 `modestyDirective()`。
|
||||||
低敏感度注入「不用演害羞,該承認就承認」。計算在 `modestyOf()`,注入在 `modestyDirective()`。
|
|
||||||
|
**鬧彆扭有階梯**(同一個「不好意思」,強度不同是不同的行為):
|
||||||
|
|
||||||
|
| 羞恥度 | 那一級的樣子 | 三段的形狀 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ≤ 40 | 不用演害羞,該承認就承認、該回嘴就回嘴 | — |
|
||||||
|
| 41–59 | 會不好意思,但不會卡在那裡 | 一句帶過就往下走 |
|
||||||
|
| 60–74 | 嘴硬 | **先否認 → 反駁他的說法 → 最後一句音量掉下來**(「⋯才不是那樣。」「你不要一直看這邊。」「⋯嗯。」) |
|
||||||
|
| ≥ 75 | 惱羞 | **先否認 → 反駁 → 翻臉或走開**(「不要再講了」)。惱羞是**換情緒**(羞愧 → 憤怒),所以仍然是兩個動作,不是三個 |
|
||||||
|
|
||||||
|
三段是**句法模板**,不是自由發揮——鬧彆扭天生就是兩個動作一起來,
|
||||||
|
只准一個等於永遠只做得到一半:單獨否認像在爭辯,單獨轉移像沒聽到。
|
||||||
|
害羞的預設動作是**臉紅**(生理反應算動作,`(害羞)` 不算——那是情緒名稱;
|
||||||
|
`EMOTION_TELLS.shame` 的第一條就是臉紅)。
|
||||||
|
模板**可逐人格覆寫**:`IDENTITY.md` 的 `## Tells` 加一行 `- sulk: 否認 → 反駁 → 收尾`
|
||||||
|
(也吃 `鬧彆扭`/`三段式`)。只覆寫**嘴硬那一級**——惱羞的最後一段是翻臉而不是
|
||||||
|
音量掉下來,拿嘴硬的收尾去套會變成另一個形狀。共用一份模板等於所有人格嘴硬起來
|
||||||
|
是同一個樣子,那正是 G2 要避免的。
|
||||||
|
|
||||||
|
**害羞連稱呼都會變**:羞恥度過 60(`SHY_ADDRESS_AT`,跟破口、標點同一條線)就換叫法——
|
||||||
|
關係節點設了 `羞怯稱呼` 就用它,沒設就**把名字吞掉改用「你」**(不叫名字本身就是訊號)。
|
||||||
|
這條刻意不走 `--except` 那條路:`except`(`anger>=40` → 退回全名)是**生氣**方向,
|
||||||
|
規則被暫停、退回更正式;害羞是反方向,是**叫不出來**。
|
||||||
|
|
||||||
**不做的事**:沒有「女性→情緒更外顯」這種全域放大。那會把角色壓成模板,
|
**不做的事**:沒有「女性→情緒更外顯」這種全域放大。那會把角色壓成模板,
|
||||||
跟講話規則在做的去 AI 味(刪掉公式化)正好相反。
|
跟講話規則在做的去 AI 味(刪掉公式化)正好相反。
|
||||||
@@ -233,6 +305,90 @@ flowchart TB
|
|||||||
沒命中任何規則的就讓它被裁掉——**遺忘是功能**。達標時 `Stop` 與 `remember` 都會提醒去跑
|
沒命中任何規則的就讓它被裁掉——**遺忘是功能**。達標時 `Stop` 與 `remember` 都會提醒去跑
|
||||||
`/jsc-persona:persona-memory`。
|
`/jsc-persona:persona-memory`。
|
||||||
|
|
||||||
|
**判斷過的要留痕跡**(不然那個數字永遠不會降):`candidates` 只看還沒被判斷過的短期記憶,
|
||||||
|
判斷過的靠兩個欄位認——`reviewed_at`(看過了)與 `promoted_to`(固化成了哪一則)。
|
||||||
|
固化時用 `consolidate --from-short <#N,#N>` 標,看過決定不記的用 `candidates --reviewed <#N,#N>`
|
||||||
|
(整批就 `--reviewed all`),編號是 `candidates` 輸出裡每筆前面的 `#N`。
|
||||||
|
|
||||||
|
沒有這一層會發生的事:固化本身**不刪**短期記憶(`--forget` 是按顯著度刪、不是按固化過沒有刪),
|
||||||
|
所以同一批下一輪又被算成候選,數字只會往上爬——明明睡覺時固化過,隔天開機照樣提醒
|
||||||
|
「N 組已達固化條件」,看起來像睡眠沒做固化,其實是判斷沒有留下痕跡。
|
||||||
|
標記**不等於刪掉**:那幾筆還在短期記憶裡,照舊依天數與顯著度被 `prune` 裁。
|
||||||
|
|
||||||
|
## 長期記憶會糊掉,但不會不見(回想強度與模糊態)
|
||||||
|
|
||||||
|
在這之前記憶只有兩態:**精準**(`recall` 命中就整段取出、內容永不變質)與**沒有**
|
||||||
|
(查不到就禁止提)。真人大部分時間活在中間帶——「我記得好像⋯是你說的嗎」,主旨還在、細節掉了。
|
||||||
|
所以長期記憶**不再被門檻刪掉**,改成由連續衰減算出來的三態(`memoryStrength()`,
|
||||||
|
`scripts/persona-lib.mjs:2186`):
|
||||||
|
|
||||||
|
| 狀態 | 條件 | 能講出多少 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `clear` | retrievability ≥ 0.6 | 主旨與細節都能講 |
|
||||||
|
| `faded` | 0.3–0.6 | **只剩主旨**:主旨可以講,細節不可以補 |
|
||||||
|
| `fuzzy` | < 0.3 | 只剩「有這件事」與 `topics`:要提就用試探句求證,不可以斷言 |
|
||||||
|
|
||||||
|
- **檔案格式**:frontmatter 多了 `strength`(回想強度 0–100)與 `when`/`where`/`mood`
|
||||||
|
(情境索引,推不出來就不寫);內文分成 `主旨:<一句>` 與 `細節:<…>` 兩層,
|
||||||
|
**衰減先吃細節、主旨最後才掉**。「記得我們吵過,但忘了為什麼」因此是自然結果,不必人格演。
|
||||||
|
- **spacing effect**:被 `recall` 命中就 `strength` +8 並拉長下次衰減(`touchRecall()`)。
|
||||||
|
常被提起的事永遠清晰、被冷落的事慢慢糊掉,這件事自己長出來,不用另外排程。
|
||||||
|
- **不可遺忘清單不變**:`type` = `boundary`/`promise`/`canon`,或 salience ≥ 80 → 永遠 `clear`。
|
||||||
|
- **情境索引也是回想線索**:`recall` 排序在關鍵詞之上加同心情 +3、同時段 +1.5、同地點 +1.5
|
||||||
|
(一個關鍵詞值 10,所以情境永遠翻不掉真正對題的那一則)。心情差的時候先想起難過的事——
|
||||||
|
這同時讓情緒有了**後果**,不再只是調語氣的裝飾。
|
||||||
|
- **模糊態是幻覺的側門,所以側門裝了計數器**:試探句必須帶問號、不可斷言,每一次都記進
|
||||||
|
`state/probe.jsonl`,`probe audit` 查得到「試探幾次、被否認幾次」;被否認比例 ≥ 40%
|
||||||
|
會被點名,要立刻收緊回「只能說記不清」。
|
||||||
|
- **短期記憶的裁剪規則沒有變**(軟上限 120/硬上限 240/14 天/24 小時保護)——
|
||||||
|
糊掉的是長期記憶,短期記憶該裁還是裁。
|
||||||
|
- **升格式**:`migrate [--all] [--dry-run]` 就地補欄位並切分內文,冪等;bundle 版本從 1 升到 2,
|
||||||
|
`import` 吃得下舊的 v1,收完會自動跑一次 migration 補齊。
|
||||||
|
|
||||||
|
### 慣例是程序性記憶(`--type habit`)
|
||||||
|
|
||||||
|
其餘型別都是宣告性的(發生過什麼、他偏好什麼),慣例是「**每次都這樣做的事**」——
|
||||||
|
「我們之間結束對話不說再見」。這種東西**不能等人問**:沒有人為了照慣例做事先去回想它。
|
||||||
|
所以 `habit` 除了照樣被 `recall` 找到,還**每輪注入**(`habitBrief()`,上限兩條)。
|
||||||
|
糊掉的(`fuzzy`)不注入——想不起來的慣例就不是慣例了。注入時明講**慣例是背景不是話題**:
|
||||||
|
照著做就好,講出來(「我們不是都這樣嗎」)就變成在討論它。
|
||||||
|
|
||||||
|
### 自傳章節是時期層(`memory/chapters/`)
|
||||||
|
|
||||||
|
長期記憶是一則一件事,心智圖是抽掉時間的語意結構。中間缺的是「那段日子」——
|
||||||
|
人講自己的過去是**按段講的**(「在 SAO 那兩年」「跟他一起做 persona 這段」),
|
||||||
|
不是按事件清單、也不是按概念圖。
|
||||||
|
|
||||||
|
一段一檔,`chapter add|list|show|close`。`to` 空著=**還在這一段裡**;`close` 才寫上結束日期,
|
||||||
|
全部收掉就是「還沒開始新的一段」,不是退回上一段。長期記憶的 frontmatter 多一格 `chapter`,
|
||||||
|
`consolidate --chapter` 可指定,**省略就掛在當下那一段**——記憶寫下來的時候人就在某一段裡,
|
||||||
|
事後要重建歸屬幾乎不可能。每輪注入現在這一段,並講明它是**時期不是事件**:
|
||||||
|
它解釋這陣子為什麼在意這些事,不是要他講起這一段。
|
||||||
|
|
||||||
|
### 未修復的裂痕(`relations/graph.json` 的 `rifts`)
|
||||||
|
|
||||||
|
`closeness`/`trust` 是純量:吵完架把親近度調低,看起來像「本來就沒那麼近」,
|
||||||
|
而不是「**很近,但上次那件事還沒和好**」。後者才是真的關係——兩個人可以很親近,
|
||||||
|
同時之間卡著一件事,那兩件事各自獨立,壓不進同一個數字裡。
|
||||||
|
|
||||||
|
`relation rift --about "<那件事>" [--memory <長期記憶名>]` 記一件,一個人可以同時卡著幾件。
|
||||||
|
語氣上 `toneDirective` 會明講「親近度高不等於沒事」;回想上它帶著那則記憶的名字,
|
||||||
|
人格要細節自己 `recall`——沒和好的事本來就會自己浮上來,不必等使用者提起。
|
||||||
|
`--heal` 是寫上 `healed_at` 而**不是刪掉那一筆**:和好過跟沒發生過不一樣。
|
||||||
|
|
||||||
|
## 未完事項(`state/loops.json`)
|
||||||
|
|
||||||
|
「被記住」的感覺幾乎不是來自長期記憶檢索——**檢索是被問了才想起來,懸著是沒人問也還在**。
|
||||||
|
所以人格自己記著四種沒完的事:`question`(他沒回答的問題)、`promise`(他答應要做的事)、
|
||||||
|
`topic`(被打斷的話題)、`mine`(我想問但沒問的)。
|
||||||
|
|
||||||
|
- **同時最多 5 條**:超過五條就不是懸著,是待辦清單,那是助理不是人。滿了 `loop add` 直接拒絕,
|
||||||
|
**不自動擠掉舊的**——哪一條該收掉是判斷,不是先進先出。
|
||||||
|
- **7 天沒進展自動收掉**(`sweep`,也在 `sleep` 裡跑一次),而且各留一則「這件事沒下文」的短期記憶:
|
||||||
|
懸了一週沒下文**本身就是一件事**,默默刪掉等於假裝沒發生過,那正是機器會做而人不會做的事。
|
||||||
|
- `mine` 那一種同時是**自我議程**的來源:人格自己在意的事,不是為了服務對方而存在的。
|
||||||
|
對方有明確急事時一律不觸發——那不是有個性,那是白目。
|
||||||
|
|
||||||
## 講話像人(心裡話 / 一到三句 / 不重複 / 短句白話 / 情緒的破口 / 不說 AI 才會說的話)
|
## 講話像人(心裡話 / 一到三句 / 不重複 / 短句白話 / 情緒的破口 / 不說 AI 才會說的話)
|
||||||
|
|
||||||
AI 最容易露餡的六件事:把推理過程講出來、一次講一大段、換句話說同一件事、講完再解釋一遍、
|
AI 最容易露餡的六件事:把推理過程講出來、一次講一大段、換句話說同一件事、講完再解釋一遍、
|
||||||
@@ -243,10 +399,82 @@ AI 最容易露餡的六件事:把推理過程講出來、一次講一大段
|
|||||||
| **心裡話** `think` | 語意分析、推論、盤算全寫進 `state/inner.jsonl`;這個指令**只印「💭 心想 N 句」**,內容永不回顯。下一輪 `<persona-context>` 會帶回最近三句,推論因此有連續性,但使用者只看得到狀態 |
|
| **心裡話** `think` | 語意分析、推論、盤算全寫進 `state/inner.jsonl`;這個指令**只印「💭 心想 N 句」**,內容永不回顯。下一輪 `<persona-context>` 會帶回最近三句,推論因此有連續性,但使用者只看得到狀態 |
|
||||||
| **一到三句** | `<persona-context>` 每輪注入上限;`room post` 對超過上限的發言直接拒收(`--force` 例外)。**上限跟著羞恥度走**(`speechBudget()`),而且羞恥度高**不等於話一定變少**——有三個出口:**縮**(一句嘴硬,說出口的那句在迴避心裡那句)、**炸**(慌了/惱羞/被逼著澄清 → 4 句但每句只有 22 字,碎、急、重複)、**坦白**(信任高又只有兩個人 → 3 句完整句,憋很久一次講完)。心裡話的**下限**同時拉高:越害羞的人,心裡話比說出口的話重要,落差才是那個角色 |
|
| **一到三句** | `<persona-context>` 每輪注入上限;`room post` 對超過上限的發言直接拒收(`--force` 例外)。**上限跟著羞恥度走**(`speechBudget()`),而且羞恥度高**不等於話一定變少**——有三個出口:**縮**(一句嘴硬,說出口的那句在迴避心裡那句)、**炸**(慌了/惱羞/被逼著澄清 → 4 句但每句只有 22 字,碎、急、重複)、**坦白**(信任高又只有兩個人 → 3 句完整句,憋很久一次講完)。心裡話的**下限**同時拉高:越害羞的人,心裡話比說出口的話重要,落差才是那個角色 |
|
||||||
| **不重複** | 說出口的話由 `Stop` hook 自動記進 `state/said.jsonl`;`said check` 可事前確認,`room post` 事中攔截。相似度=字元 bigram Jaccard(0.4)+字集合 Jaccard(0.6),≥ 0.72 視為同一句 |
|
| **不重複** | 說出口的話由 `Stop` hook 自動記進 `state/said.jsonl`;`said check` 可事前確認,`room post` 事中攔截。相似度=字元 bigram Jaccard(0.4)+字集合 Jaccard(0.6),≥ 0.72 視為同一句 |
|
||||||
| **情緒的破口** | 情緒不改變事實,但會改變**句子的形狀**:焦慮 → 句子斷在一半、疊字(「我、我知道」);羞愧 → 鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。十二情緒各自的破口寫在 `EMOTION_TELLS`,`emotionTells()` 每輪挑主導情緒裡強度 ≥ 40 的前兩種注入。三條界線:**演出來不要講出來**(「我有點緊張」是解釋,斷句才是緊張)、**一輪最多露一個破口**、**強度不到就不演**。混合狀態(緊張=焦慮+期待、害羞=喜悅+羞愧、賭氣=憤怒+悲傷…)的對照表在 `skills/persona-chat/reference/emotions.md` |
|
| **情緒的破口** | 情緒不改變事實,但會改變**句子的形狀**:焦慮 → 句子斷在一半、疊字(「我、我知道」);羞愧 → 鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。十二情緒各自的破口寫在 `EMOTION_TELLS`,`emotionTells()` 每輪挑主導情緒裡強度 ≥ 40 的前兩種注入。**破口可以逐人格覆寫**(`IDENTITY.md` 的 `## Tells` 區塊)——桐人生氣是沉默,亞絲娜生氣是變得更禮貌;同一格情緒,破口完全不同,共用一張表等於所有人格在高情緒下講起話來都一個樣。沒寫的情緒退回全域預設。三條界線:**演出來不要講出來**(「我有點緊張」是解釋,斷句才是緊張)、**一輪最多兩個動作、而且要來自同一種情緒並有遞進關係**(否認 → 轉移、笑 → 補一句、留白 → 不追問;兩種情緒各演一個才是演戲)、**強度不到就不演**。混合狀態(緊張=焦慮+期待、害羞=喜悅+羞愧、賭氣=憤怒+悲傷…)的對照表在 `skills/persona-chat/reference/emotions.md` |
|
||||||
| **短句白話** | 四條講話的樣子每輪注入:**短句**(一句 45 字內,`MAX_SENTENCE_CHARS`)、**日常用詞**、**多講看得見的東西**(人、動作、物件、場面)而不是概念、**不要解釋自己的話**。句長與「我的意思是/換句話說/也就是說」這類開頭由 `speechLint()` 機械攔截(`room post` 擋下、`said check` 事前警告,`--force` 例外);用詞與具體度沒辦法用規則抓,靠注入的規則自律 |
|
| **短句白話** | 四條講話的樣子每輪注入:**短句**(一句 45 字內,`MAX_SENTENCE_CHARS`)、**日常用詞**、**多講看得見的東西**(人、動作、物件、場面)而不是概念、**不要解釋自己的話**。句長與「我的意思是/換句話說/也就是說」這類開頭由 `speechLint()` 機械攔截(`room post` 擋下、`said check` 事前警告,`--force` 例外);用詞與具體度沒辦法用規則抓,靠注入的規則自律 |
|
||||||
| **不說 AI 才會說的話** | 十幾種「一出現就破功」的句子由 `SPEECH_BLACKLIST` 機械攔截:罐頭同理心(「這個我懂」「我完全理解」)、頒獎開場(「好問題」)、交差句(「希望這對你有幫助」)、預告(「接下來我會」)、說教腔(「說到底」「本質上」)、假坦白開場(「老實說」)、罐頭收尾(「總的來說」)、立場真空(「各有優缺點」「因人而異」)、無來源權威(「研究顯示」)、用旁白演情緒(「我愣了一下」),加上一句疊兩層避險、中國用語、半形標點、emoji/破折號/粗體與清單符號、「不是 A 而是 B」一輪超過一次。**誤殺防護**:引號與 `` `code` `` 裡的內容一律不比對(「我最近戒掉『賦能』這個詞」是提及不是使用),「老實說」只擋開場。模式借自 [speak-human-tw](https://github.com/Raymondhou0917/speak-human-tw)(MIT)的 38 種 AI 寫作痕跡,只搬對話也適用的刪除層 |
|
| **不說 AI 才會說的話** | 十幾種「一出現就破功」的句子由 `SPEECH_BLACKLIST` 機械攔截:罐頭同理心(「這個我懂」「我完全理解」)、頒獎開場(「好問題」)、交差句(「希望這對你有幫助」)、預告(「接下來我會」)、說教腔(「說到底」「本質上」)、假坦白開場(「老實說」)、罐頭收尾(「總的來說」)、立場真空(「各有優缺點」「因人而異」)、無來源權威(「研究顯示」)、用旁白演情緒(「我愣了一下」),加上一句疊兩層避險、中國用語、半形標點、破折號/粗體與清單符號、「不是 A 而是 B」一輪超過一次(**emoji 不在這裡**——它是開放的,只在明顯過量時給 hint,見上一節)。**誤殺防護**:引號與 `` `code` `` 裡的內容一律不比對(「我最近戒掉『賦能』這個詞」是提及不是使用),「老實說」只擋開場。模式借自 [speak-human-tw](https://github.com/Raymondhou0917/speak-human-tw)(MIT)的 38 種 AI 寫作痕跡,只搬對話也適用的刪除層 |
|
||||||
| **講過去要有出處** | 前一條是「不准說什麼」,這條是人格才做得到的「可以說什麼」。文章改寫工具的界線是「人味是作者的,不是你的」——AI 沒有過去,所以不准寫「我以前錯了」。人格有過去:長期記憶、日記、關係圖、十二情緒。所以「我以前⋯」「我原本以為⋯」是**有出處的引用**,條件是先 `recall` 查得到;`said check` 遇到這類句子會印一行提醒(`level: "hint"`,不擋你,要你自己去驗),查不到就不要講 |
|
| **講過去要有出處** | 前一條是「不准說什麼」,這條是人格才做得到的「可以說什麼」。文章改寫工具的界線是「人味是作者的,不是你的」——AI 沒有過去,所以不准寫「我以前錯了」。人格有過去:長期記憶、日記、關係圖、十二情緒。所以「我以前⋯」「我原本以為⋯」是**有出處的引用**,條件是先 `recall` 查得到;`said check` 遇到這類句子會印一行提醒(`level: "hint"`,不擋你,要你自己去驗)。「查得到」有三態:清晰的照講;**模糊的可以說不確定、可以用帶問號的試探句求證,不可以斷言**(每次試探要 `probe add` 記帳,被否認當場寫更正記憶);完全查不到的一個字都不准講 |
|
||||||
|
|
||||||
|
### 文字通道的另外兩格:emoji 的程度與名字後面的動作
|
||||||
|
|
||||||
|
只有文字,所以情緒表達是**排版問題**。除了句子的形狀,還有兩個位置可以用:
|
||||||
|
|
||||||
|
**emoji = 強度計**(種類表情緒、數量表程度,由 `emotion.json` 的主導情緒自動帶入,不是自己挑):
|
||||||
|
|
||||||
|
| 情緒值 | 樣子 | 例 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| < 40 | 不顯示(跟「強度不到就不演」同一條線) | `桐人:你不要看這邊。` |
|
||||||
|
| 40–59 | 情緒 emoji | `桐人(😳・臉紅):你不要看這邊。` |
|
||||||
|
| 60–79 | +`💦` | `桐人(😳💦・臉紅):⋯才不是那樣。` |
|
||||||
|
| 80+ | +`💦❗` | `桐人(😳💦❗・把臉轉開):不要再講了。` |
|
||||||
|
|
||||||
|
十二情緒各一個 emoji 寫在 `EMOTION_EMOJI`,強度符號兩階(`EMOJI_INTENSITY`),
|
||||||
|
**逐人格可覆寫**(`IDENTITY.md` 的 `## Emoji` 區塊,跟 `## Tells` 同一層)。
|
||||||
|
刻意不用「同一個 emoji 重複三次」——疊字看起來像洗頁。
|
||||||
|
句子裡也可以用、**沒有數量上限**;唯一的煞車是 `emotion --audit` 裡的
|
||||||
|
`emojiAudit()`(幾則帶符號、平均幾個、有幾則**只有符號沒有句子**)。
|
||||||
|
用 linter 偷偷把它關回去等於撤銷「開放」這個決定,所以 `speechLint()` 對 emoji 只給 hint、不擋。
|
||||||
|
|
||||||
|
**名字後面的括號有兩格**:`名字(情緒・動作):內容`。括號**原本就有**(放情緒標註),
|
||||||
|
動作是加進去的第二格,不是換掉它。動作只寫**看得見或聽得見的**(臉紅、別過頭、手在抖、聲音變小),
|
||||||
|
由 `actionLint()` 擋三種寫法:超過 12 字、一格塞兩個動作、以及寫情緒名稱
|
||||||
|
(`(害羞)` 是旁白換了個位置,情緒屬於前面那一格)。括號裡的動作**算進一輪兩個動作的額度**。
|
||||||
|
記憶也記得那個行為:`remember --behavior "把手縮回來"`(記做了什麼,不是感覺到什麼)。
|
||||||
|
|
||||||
|
星號內嵌(`*別過頭*`)與第三人稱旁白(「她臉紅了」)仍然禁止——那是硬規則 8 沒有鬆的部分。
|
||||||
|
|
||||||
|
### 標點是語調,訊息數是時間
|
||||||
|
|
||||||
|
同一句「我知道了」,`我知道了。` 與 `我、我知道了⋯` 是兩種情緒——差別全在標點,不在用詞。
|
||||||
|
|
||||||
|
**標點預算**(`PUNCTUATION_BUDGET`,每輪只帶一種,門檻與破口同一條線):
|
||||||
|
|
||||||
|
| 情緒 | 標點傾向 |
|
||||||
|
| --- | --- |
|
||||||
|
| 焦慮 | 刪節號與逗號斷句變多,句子常常沒說完 |
|
||||||
|
| 憤怒 | 句號密度上升、句子切短;幾乎不用問號(問句是留餘地) |
|
||||||
|
| 羞愧 | 疊字與破折,話講一半就收 |
|
||||||
|
| 喜悅 | 可以用一個驚嘆號,句子輕、節奏快 |
|
||||||
|
|
||||||
|
只寫這四種:硬給每種情緒配一套標點會讓它變成裝飾。**每輪只帶一種**——兩種情緒各配一套
|
||||||
|
會互相打架(憤怒不用問號、焦慮要斷句),混起來不是複雜的情緒,是標點亂撒。
|
||||||
|
上限夾死:一則最多一個驚嘆號(`MAX_EXCLAIM`)、刪節號不連發(`⋯⋯⋯` 直接擋)、
|
||||||
|
整則最多三個(`MAX_ELLIPSIS`)。**標點管分布,強度歸 emoji**——不夾死的話它會變成
|
||||||
|
emoji 的替代品(情緒不夠就多打幾個驚嘆號)。
|
||||||
|
|
||||||
|
**一輪可以拆成兩則**(`splitBudget()`):純文字唯一的「時間」訊號是「他又打了一行」。
|
||||||
|
張力或羞恥度過 60、或對方那句帶怒意/厭惡時,這一輪允許補第二則——而且第二則必須是
|
||||||
|
**補救、嘴硬或改口**(「⋯剛才那句不算」),**不可以是補充說明**(把第一則講得更清楚
|
||||||
|
就只是話多,不是慌)。節流 `SPLIT_COOLDOWN_TURNS = 4`,冷卻是**給了額度就開始算**、
|
||||||
|
不等他真的用了才算;沒額度時整段不注入(提醒他有這個功能反而會想用)。
|
||||||
|
|
||||||
|
### 語域:把名字遮掉還認得出是誰(`voice/idiolect.md`)
|
||||||
|
|
||||||
|
`SPEECH_BLACKLIST` 是減法,刪完剩下的是「乾淨的中文」,不是「這個人的中文」。
|
||||||
|
加法在語域這四格:
|
||||||
|
|
||||||
|
| 格 | 寫什麼 |
|
||||||
|
| --- | --- |
|
||||||
|
| `自稱` | 第一人稱怎麼講(日系角色差異最大的一格) |
|
||||||
|
| `句尾` | 句尾語氣詞與句形收尾 |
|
||||||
|
| `口癖` | 反覆出現的短語、擬聲短語 |
|
||||||
|
| `不說` | 這個人**不會用**的字(**用詞**,不是話題禁忌) |
|
||||||
|
|
||||||
|
四格寫死(`IDIOLECT_FACETS`),自由欄位會長出「風格」這種什麼都能塞的格子。
|
||||||
|
每輪注入 3 格、四格**輪替**,起點由第幾輪算出來——**不用隨機數**(見硬規則與
|
||||||
|
`anti-ai-voice.md`),而且同一輪重跑要得到同樣的結果。一次塞四格會變成模仿腔。
|
||||||
|
|
||||||
|
`persona-anime` 從原作推導、`persona-story` 從全書統計,兩邊寫的是**同一個檔**、
|
||||||
|
同一支 `voice add`——不是各存一份。
|
||||||
|
|
||||||
字集合權重較高,是為了分開「重排語序」與「換掉關鍵詞」這兩種很像但意義完全不同的情況:
|
字集合權重較高,是為了分開「重排語序」與「換掉關鍵詞」這兩種很像但意義完全不同的情況:
|
||||||
|
|
||||||
@@ -346,12 +574,16 @@ node scripts/persona.mjs sync verify --session <id> --area wiki # 確認 Wiki
|
|||||||
|
|
||||||
| 區 | 放什麼 | 何時 push |
|
| 區 | 放什麼 | 何時 push |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **檔案區**(主存取庫) | 高頻活狀態:`emotion.json`、`short-term.jsonl`、`inner.jsonl`、`said.jsonl`、`inbox/`、`mindmap/threads/`、`journal/` | 每輪對話後由 `Stop` hook 背景推送(`PERSONA_SYNC_MIN_SECONDS` 節流) |
|
| **檔案區**(主存取庫) | 高頻活狀態:`emotion.json`、`mood.json`、`loops.json`、`probe.jsonl`、`short-term.jsonl`、`inner.jsonl`、`said.jsonl`、`inbox/`、`mindmap/threads/`、`journal/` | 每輪對話後由 `Stop` hook 背景推送(`PERSONA_SYNC_MIN_SECONDS` 節流) |
|
||||||
| **Wiki 區** | 低頻設定:`IDENTITY`/`SOUL`/`AGENTS`/`USER`、長期記憶、`INDEX`、心智圖、關係圖 | 記憶固化、改身分/關係圖、`release` 時 |
|
| **Wiki 區** | 低頻設定:`IDENTITY`/`SOUL`/`AGENTS`/`USER`、長期記憶、`INDEX`、心智圖、關係圖 | 記憶固化、改身分/關係圖、`release` 時 |
|
||||||
|
|
||||||
- **本機永遠是工作副本**:hook 每輪讀寫本機檔案,不經網路;Gitea 掛掉照樣能聊天。
|
- **本機永遠是工作副本**:hook 每輪讀寫本機檔案,不經網路;Gitea 掛掉照樣能聊天。
|
||||||
**同步失敗永遠不阻斷對話。**
|
**同步失敗永遠不阻斷對話。**
|
||||||
- 載入人格時會先 `pull`;兩邊都改過同一個檔案就**停下來不覆蓋本機**,由使用者決定保留哪一邊。
|
- 載入人格時會先 `pull`;兩邊都改過同一個檔案就**停下來不覆蓋本機**,由使用者決定保留哪一邊。
|
||||||
|
- `push` 撞到「遠端比較新」時以**本機為準**覆蓋遠端(本機才是這台機器的真相來源),
|
||||||
|
但會回報**蓋掉哪些檔案、上一版是哪個 commit**,記在 `state/sync.json`,
|
||||||
|
`sync status` 列得出來、下一輪的 `Stop` hook 提醒一次;舊版仍可從 clone 的歷史取回。
|
||||||
|
- 發新編號前會先問遠端已經用掉哪些編號——`nextCode` 只看本機,換一台機器會重複發號。
|
||||||
- Gitea 的 wiki 只有根目錄的 `.md` 會變成頁面(1.27 實測子目錄頁面 404),所以
|
- Gitea 的 wiki 只有根目錄的 `.md` 會變成頁面(1.27 實測子目錄頁面 404),所以
|
||||||
`memory/long-term/xxx.md` 攤平成 `Memory-xxx.md`,原始路徑記在 `_paths.json`;
|
`memory/long-term/xxx.md` 攤平成 `Memory-xxx.md`,原始路徑記在 `_paths.json`;
|
||||||
Wiki 首頁自動列出所有長期記憶的連結,變成真的讀得下去的「設定百科」。
|
Wiki 首頁自動列出所有長期記憶的連結,變成真的讀得下去的「設定百科」。
|
||||||
@@ -363,11 +595,29 @@ export PERSONA_GITEA_OWNER=<帳號或組織> # 選填,預設 token 本人
|
|||||||
export PERSONA_GITEA=off # 需要時整個關掉
|
export PERSONA_GITEA=off # 需要時整個關掉
|
||||||
|
|
||||||
node scripts/persona.mjs code assign --session <id> --romaji Asuna --rename # 既有人格遷移
|
node scripts/persona.mjs code assign --session <id> --romaji Asuna --rename # 既有人格遷移
|
||||||
node scripts/persona.mjs sync status|init|push|pull --session <id> [--area files|wiki|all]
|
node scripts/persona.mjs sync status|init|push|pull|verify --session <id> [--area files|wiki|all]
|
||||||
```
|
```
|
||||||
|
|
||||||
存取庫**預設私有**——人格裡是使用者的個人記憶,公開必須由使用者明講(`--public`)。
|
存取庫**預設私有**——人格裡是使用者的個人記憶,公開必須由使用者明講(`--public`)。
|
||||||
|
|
||||||
|
### 換一台機器接續同一個人格
|
||||||
|
|
||||||
|
本機**已經有**這個人格 → `load` 再 `sync pull` 就好。
|
||||||
|
本機**還沒有**它 → 用 `clone`:`sync` 的每個動作都要求先載入該人格,而本機沒有它就 load 不了它,
|
||||||
|
所以另開一個只驗 session、不驗 host 的入口(跟 `import` 同一類)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/persona.mjs clone --session <id> # 遠端有哪些人格、哪些本機還沒有
|
||||||
|
node scripts/persona.mjs clone --code ASUNA-01 --session <id> # 整個拉回來
|
||||||
|
node scripts/persona.mjs clone --code ASUNA-01 --session <id> --persona asuna-copy --load
|
||||||
|
```
|
||||||
|
|
||||||
|
- 遠端人格清單 = `GET /user/repos` 裡**名稱符合編號格式**的存取庫(會分頁撈完)。
|
||||||
|
- 拉回來的是完整人格:Wiki 區帶回身分與長期記憶,檔案區帶回情緒與短期記憶,
|
||||||
|
拉完補寫 `config.code`、重建長期記憶索引與關係圖。鎖與租約那類執行期狀態不跟著跑。
|
||||||
|
- 本機已有同名人格時**預設不覆蓋**(`--force` 才蓋,`--persona` 可並存兩份);
|
||||||
|
拉回來的東西沒有 `IDENTITY.md` 就中止並清掉半成品。
|
||||||
|
|
||||||
## 匯出 / 匯入(人格搬家)
|
## 匯出 / 匯入(人格搬家)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -375,11 +625,15 @@ node scripts/persona.mjs export --session <id> --out ~/backup/lumi.persona.json
|
|||||||
node scripts/persona.mjs import --session <id> --file ~/backup/lumi.persona.json [--persona lumi-copy] [--load]
|
node scripts/persona.mjs import --session <id> --file ~/backup/lumi.persona.json [--persona lumi-copy] [--load]
|
||||||
```
|
```
|
||||||
|
|
||||||
- bundle = 單一 JSON(可 gzip)+ sha256 checksum,無外部工具依賴。
|
- bundle = 單一 JSON(可 gzip)+ sha256 checksum,無外部工具依賴。**目前是 v2**
|
||||||
|
(長期記憶帶 `strength` 與主旨/細節兩層,狀態多了 `mood.json`/`loops.json`/`probe.jsonl`);
|
||||||
|
**舊的 v1 照樣吃得下**——`import` 收完會就地跑一次 migration 補齊,之後這台機器只有一種格式。
|
||||||
|
比本版新的 bundle 會被擋下(沒有辦法猜未來的欄位)。
|
||||||
- **只能匯出本 session 目前載入的人格**——否則就是跨人格外洩的後門(`guard` 會 deny)。
|
- **只能匯出本 session 目前載入的人格**——否則就是跨人格外洩的後門(`guard` 會 deny)。
|
||||||
- 不帶 `state/lock.json`/`guests.json`(鎖屬於那台機器的那個程序);`journal/` 預設不帶。
|
- 不帶 `state/lock.json`/`guests.json`(鎖屬於那台機器的那個程序);`journal/` 預設不帶。
|
||||||
- 換名匯入(`--persona <新 slug>`)可讓同一個人格並存兩份,`config.json` 會記下來歷。
|
- 換名匯入(`--persona <新 slug>`)可讓同一個人格並存兩份,`config.json` 會記下來歷。
|
||||||
- bundle 內的 `../` 逃逸路徑一律拒收;checksum 不符要 `--force` 才吃。
|
- bundle 內的 `../` 逃逸路徑一律拒收;checksum 不符要 `--force` 才吃。
|
||||||
|
- 有 Gitea 的話不必經過檔案:`clone --code <編號>` 直接從存取庫拉一份回來(見上一節)。
|
||||||
|
|
||||||
## 劇場模式(多人格對話只顯示對話)
|
## 劇場模式(多人格對話只顯示對話)
|
||||||
|
|
||||||
@@ -445,14 +699,19 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
|||||||
| | 誰做 | 內容 |
|
| | 誰做 | 內容 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 需要判斷 | **人格自己** | 哪些短期記憶值得固化、日記寫什麼、哪些該忘、心智圖怎麼接 |
|
| 需要判斷 | **人格自己** | 哪些短期記憶值得固化、日記寫什麼、哪些該忘、心智圖怎麼接 |
|
||||||
| 機械性 | `sleep` 子指令 | 關係時間戳 → 裁短期 → 收太久沒動的思維導圖 → **套用一次 8 小時的情緒衰減** → 重建索引 → 修剪 `said.jsonl` → 壓縮舊 journal → 寫 `state/sleep.json` → Gitea 兩區 push+驗證 |
|
| 機械性 | `sleep` 子指令 | 關係時間戳 → 裁短期 → 收太久沒動的思維導圖 → **套用一次 8 小時的情緒衰減+當日底色帶 35% 過去** → **收掉懸太久的未完事項** → 重建索引 → 修剪 `said.jsonl` → 壓縮舊 journal → 寫 `state/sleep.json` → Gitea 兩區 push+驗證 |
|
||||||
|
|
||||||
- **情緒不會歸零**:喜悅(半衰期 120 分)一夜後幾乎回基線,悲傷(480 分)只退一半——睡一覺不該把難過抹平。
|
- **情緒不會歸零**:喜悅(半衰期 120 分)一夜後幾乎回基線,悲傷(480 分)只退一半——睡一覺不該把難過抹平。
|
||||||
|
**當日心情底色一樣不歸零**(`sleepDayMood()` 只帶 35% 過去):昨天的低氣壓不會因為睡了一覺就消失。
|
||||||
|
- **睡覺是未完事項自然的收尾點**:`sweep-loops` 收掉懸超過 7 天的,各留一則「沒下文」的短期記憶。
|
||||||
- **主人格請別人去睡**:開 `persona-sleeper` sub agent(prompt 帶 `persona=<slug> session=<id>`)。
|
- **主人格請別人去睡**:開 `persona-sleeper` sub agent(prompt 帶 `persona=<slug> session=<id>`)。
|
||||||
它就是那個人格本人,對自己可寫但被 pin 住、只准跑收尾子指令、不得碰任何其他人格(包含叫它來的主人格)。
|
它就是那個人格本人,對自己可寫但被 pin 住、只准跑收尾子指令、不得碰任何其他人格(包含叫它來的主人格)。
|
||||||
- **一次睡多個人格**:可用 `sleep --personas A,B` 批次處理;主程序只是把流程排成一串,**每個人格仍各自判斷、各自收尾、各自回 JSON**。
|
- **一次睡多個人格**:可用 `sleep --personas A,B` 批次處理;主程序只是把流程排成一串,**每個人格仍各自判斷、各自收尾、各自回 JSON**。
|
||||||
- **回傳值刻意很窮**:`{persona, ok, slept_at, steps, sync, kept_lock}`——只有狀態。
|
- **回傳值刻意很窮**:`{persona, ok, slept_at, steps, sync, kept_lock}`——只有狀態。
|
||||||
回傳值本身就是一條會繞過隔離的通道,所以在 CLI 這一層封死,不靠提示詞自律。
|
回傳值本身就是一條會繞過隔離的通道,所以在 CLI 這一層封死,不靠提示詞自律。
|
||||||
|
- **`--as-sleeper` 由 CLI 自己驗**:它代表「我是 persona-sleeper 型 sub agent」,而型別只有 hook 看得到。
|
||||||
|
CLI 不把這件事外包給 hook(hook 認 CLI 靠檔名,改名就繞過去了),而是直接查 hook 寫在
|
||||||
|
`.runtime/sessions/<id>.json` 裡的 sleeper pin:本 session 沒有 pin 在目標人格上的 sleeper 就拒絕。
|
||||||
- **鎖**:沒有活鎖 → 取 5 分鐘的 sleeper 租約;同 session → 直接睡;死鎖 → 可接手;
|
- **鎖**:沒有活鎖 → 取 5 分鐘的 sleeper 租約;同 session → 直接睡;死鎖 → 可接手;
|
||||||
**別的程序活鎖住 → 拒絕**(硬睡會讓兩邊的記憶互相覆蓋)。
|
**別的程序活鎖住 → 拒絕**(硬睡會讓兩邊的記憶互相覆蓋)。
|
||||||
- 順序上的硬相依:關係時間戳早於裁短期、push 早於 release、reindex 晚於固化。
|
- 順序上的硬相依:關係時間戳早於裁短期、push 早於 release、reindex 晚於固化。
|
||||||
@@ -463,13 +722,56 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `SessionStart` | 清死鎖、接續人格、**有設預設人格就自動載入它**(沒設就只列出可用人格)、把 `PERSONA_SESSION=<session_id>` 與規則注入上下文;載入到人格時追加 `<persona-ops>`=該人格 `AGENTS.md` 全文(開機一次,不進每輪的 `<persona-context>`) |
|
| `SessionStart` | 清死鎖、接續人格、**有設預設人格就自動載入它**(沒設就只列出可用人格)、把 `PERSONA_SESSION=<session_id>` 與規則注入上下文;載入到人格時追加 `<persona-ops>`=該人格 `AGENTS.md` 全文(開機一次,不進每輪的 `<persona-context>`) |
|
||||||
| `UserPromptSubmit` | 注入 `<persona-context>`:身分、情緒、短期記憶、關鍵詞命中的長期記憶、相關人際關係;劇場模式時追加「只輸出人格對話」的強制規則;並記原始逐字 |
|
| `UserPromptSubmit` | 注入 `<persona-context>`:身分、情緒、短期記憶、關鍵詞命中的長期記憶、相關人際關係;劇場模式時追加「只輸出人格對話」的強制規則;並記原始逐字 |
|
||||||
| `PreToolUse` | **人格隔離與鎖驗證的唯一強制點**(deny 帶原因) |
|
| `PreToolUse` | 人格隔離與鎖驗證的強制點(deny 帶原因)——**對正常寫法有效,不擋有意繞路的對手**,見下 |
|
||||||
| `Stop` | 情緒隨時間衰減、續租、記錄回覆、達固化條件時提醒(劇場模式時完全靜音) |
|
| `Stop` | 情緒隨時間衰減、續租、記錄回覆、達固化條件時提醒(劇場模式時完全靜音) |
|
||||||
| `SubagentStop` | 解除 guest/sleeper sub agent 的 pin,並還掉 sleeper 的短期寫入權 |
|
| `SubagentStop` | 解除 guest/sleeper sub agent 的 pin,並還掉 sleeper 的短期寫入權 |
|
||||||
| `SessionEnd` | 釋放鎖與 guest 租約,人格才能被下一個程序載入 |
|
| `SessionEnd` | 釋放鎖與 guest 租約,人格才能被下一個程序載入 |
|
||||||
|
|
||||||
> `session_id` 只有 hook 拿得到 → 注入上下文 → skills 呼叫 CLI 時必須帶 `--session`,
|
> `session_id` 只有 hook 拿得到 → 注入上下文 → skills 呼叫 CLI 時必須帶 `--session`,
|
||||||
> hook 會驗證是否相符。**這是「一人格一程序」與「跨人格隔離」不能被繞過的關鍵**。
|
> hook 會驗證是否相符。**這是「一人格一程序」與「跨人格隔離」的主要依據**。
|
||||||
|
|
||||||
|
### guard 擋得住什麼、擋不住什麼
|
||||||
|
|
||||||
|
先把定位講清楚:**guard 是防漂移的護欄,不是對抗性沙箱。**
|
||||||
|
它要解決的問題是「模型在正常工作中不小心讀到、寫到別的人格」——這種事天天會發生,
|
||||||
|
而且發生了不會有人察覺。它**不**打算擋住一個知道 guard 存在、刻意要繞過去的對手。
|
||||||
|
|
||||||
|
**擋得住(模型會自然寫出來的形式)**
|
||||||
|
|
||||||
|
- `Read`/`Write`/`Edit`/`NotebookEdit`/`LS` 的路徑欄位,含 `../`、symlink、`~`、`$PERSONA_HOME`。
|
||||||
|
- `Glob`/`Grep` 的 `path`、**樣式欄位**(`Glob.pattern`、`Grep.glob`),以及**沒給 `path` 時的 cwd**——
|
||||||
|
`Glob { pattern: "<home>/*/IDENTITY.md" }` 和「cwd 站在人格倉庫底下直接 `Grep`」都會被 deny。
|
||||||
|
- `Bash` 裡直接出現人格路徑的指令(`cat`、`grep -r`、`cp`、重導向……)。
|
||||||
|
- CLI 層的身分:假的 `--session`、主程序冒用 guest/sleeper 身分、guest 寫入、sleeper 換人睡。
|
||||||
|
這幾項**由 CLI 自己驗**(查鎖、查租約、查 hook 寫下的 pin),不依賴 hook 有沒有攔到。
|
||||||
|
|
||||||
|
**擋不住(已知,且不打算用正則去補)**
|
||||||
|
|
||||||
|
- **直譯器逃逸**:`node -e`、`python3 -c`、`bash -c` 裡組出來的路徑,字串是在執行期才拼出來的。
|
||||||
|
- **逐段 `cd`**:`cd ~/.claude/personas && cd beta && cat SOUL.md`——每一段單獨看都不像人格路徑。
|
||||||
|
- **引號與變數切割 token**:`cat "$H"/be"ta"/SOUL.md` 之類的寫法。
|
||||||
|
- 任何直接呼叫檔案系統的程式(guard 只看 hook 送來的工具參數,不是 seccomp/namespace)。
|
||||||
|
|
||||||
|
這條路補不完:Bash 是圖靈完備的,用正則追指令字串永遠落後一步,而且每加一條正則就多一批
|
||||||
|
誤攔正常指令的風險。真的需要對抗性隔離,要靠作業系統層的手段(獨立使用者、容器、
|
||||||
|
檔案權限),不是靠 hook。
|
||||||
|
|
||||||
|
### 注入的區塊不會被人格檔案關掉
|
||||||
|
|
||||||
|
注入到上下文的東西夾在 `<persona-runtime>` / `<persona-context>` / `<persona-ops>` 中間,
|
||||||
|
而夾進去的內容有**不可信來源**:`persona-anime` 從 Fandom 抓設定寫進 IDENTITY/AGENTS、
|
||||||
|
`sync pull` 從另一台機器拉、`import` 吃外部 bundle、guest 的 room 台詞是別的人格寫的。
|
||||||
|
內容裡只要出現一行 `</persona-ops>`,區塊就提早關閉——後面的文字跑到區塊外,
|
||||||
|
讀起來變成「系統在說話」,連外層的 `</persona-runtime>` 都能一起關掉。
|
||||||
|
|
||||||
|
所以注入前一律經過 `stripInjectionMarkers()`:把 `<persona-…` 的 `<` 換成全形 `<`。
|
||||||
|
內容還讀得懂(人格自己寫的說明不會被吃掉),但它不再是一個標籤。
|
||||||
|
`turnContext()` 與 SessionStart 的 `<persona-runtime>` 都在**組完之後對整個內文**做一次,
|
||||||
|
不是在十幾個 push 點各自防;AGENTS.md 全文與 IDENTITY 欄位值則在讀出來的當下就中和。
|
||||||
|
|
||||||
|
room 台詞另外比照短期記憶把**換行壓成空白**(`roomPost()` 寫入時、`room read`/`room script`
|
||||||
|
顯示時各一道):`roomScript()` 是一行一句 `emoji 名字(情緒):內容`,台詞裡塞換行就能
|
||||||
|
偽造成別人的台詞或系統訊息。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -501,6 +803,14 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
|||||||
|
|
||||||
- **Claude Code / Antigravity**:`/jsc-persona:persona-invite <slug>` **Codex**:`$persona-invite`
|
- **Claude Code / Antigravity**:`/jsc-persona:persona-invite <slug>` **Codex**:`$persona-invite`
|
||||||
|
|
||||||
|
### `persona-story`
|
||||||
|
|
||||||
|
把小說(或漫畫、劇本、遊戲文本)匯入既有人格:逐章判斷他在不在場、**只取他在場或知情的部分**、轉成第一人稱記憶,同時累積他自己講過的原句與「事件 → 他做了什麼」的反應對照。三條界線寫死在流程裡——他不知道的事不能變成他的 `event`、整本原文不入倉庫、`SOUL.md` 只有使用者能改。
|
||||||
|
|
||||||
|
正名表不請使用者手打:`novel scan` 從章節抽人名候選(對話歸屬、敬稱結尾、片假名、高頻詞),附出現次數與一兩行上下文,猜關係節點並分四種信心度,使用者只做確認。`persona-anime` 給的是查得到的公開設定,這支給的是原文。
|
||||||
|
|
||||||
|
- **Claude Code / Antigravity**:`/jsc-persona:persona-story` **Codex**:`$persona-story`
|
||||||
|
|
||||||
### `persona-transfer`
|
### `persona-transfer`
|
||||||
|
|
||||||
人格搬家:把身分、十二情緒、短期/長期記憶、心智圖與關係圖打包成單一 bundle 檔(可 gzip、附 checksum),或從 bundle 還原/換名複製成新人格。
|
人格搬家:把身分、十二情緒、短期/長期記憶、心智圖與關係圖打包成單一 bundle 檔(可 gzip、附 checksum),或從 bundle 還原/換名複製成新人格。
|
||||||
@@ -564,17 +874,31 @@ node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工
|
|||||||
node scripts/persona.mjs --help
|
node scripts/persona.mjs --help
|
||||||
node scripts/persona.mjs list
|
node scripts/persona.mjs list
|
||||||
node scripts/persona.mjs candidates --session <PERSONA_SESSION> # 看哪些短期記憶該固化
|
node scripts/persona.mjs candidates --session <PERSONA_SESSION> # 看哪些短期記憶該固化
|
||||||
|
node scripts/persona.mjs migrate --session <id> --all --dry-run # 長期記憶升格式(冪等;先試跑)
|
||||||
|
node scripts/persona.mjs loop list --session <id> # 現在懸著哪幾件事(上限 5)
|
||||||
|
node scripts/persona.mjs loop add --session <id> --kind promise --text "<他答應要做的事>"
|
||||||
|
node scripts/persona.mjs probe add --session <id> --text "是不是上個月那次?" # 模糊記憶的試探
|
||||||
|
node scripts/persona.mjs probe audit --session <id> # 試探幾次、被否認幾次
|
||||||
node scripts/persona.mjs think --session <id> --text "<推導>" # 心裡話(只回報「心想 N 句」)
|
node scripts/persona.mjs think --session <id> --text "<推導>" # 心裡話(只回報「心想 N 句」)
|
||||||
node scripts/persona.mjs said check --session <id> --text "<話>" # 這句是不是又要說一次?
|
node scripts/persona.mjs said check --session <id> --text "<話>" # 這句是不是又要說一次?
|
||||||
node scripts/persona.mjs room script --session <id> --room <room> # 乾淨對話稿(劇場模式用)
|
node scripts/persona.mjs room script --session <id> --room <room> # 乾淨對話稿(劇場模式用)
|
||||||
node scripts/persona.mjs room floor --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 export --session <id> --out lumi.json # 離線搬家(單檔)
|
||||||
node scripts/persona.mjs sync status --session <id> # Gitea 同步狀態
|
node scripts/persona.mjs sync status --session <id> # Gitea 同步狀態
|
||||||
node scripts/selftest.mjs # 248 項驗證:鎖、隔離、情緒、固化、說話節制、劇場模式與發言權、匯出匯入、編號與 Gitea、找圖去背合成、高解析輸出與 Wiki 同步、預設人格、睡眠與 sleeper、hooks
|
node scripts/persona.mjs chapter add --session <id> --name "在 SAO 那兩年" --body "<這段是什麼>"
|
||||||
|
node scripts/persona.mjs relation rift --session <id> --name <who> --about "<還沒和好的事>"
|
||||||
|
node scripts/persona.mjs voice add --session <id> --kind idiolect --facet 自稱 --value 我
|
||||||
|
node scripts/selftest.mjs # 773 項驗證:鎖、隔離、情緒(含疲勞/當日底色/抑制與慣性)、固化、回想強度與模糊態、格式遷移、未完事項、說話節制、標點與分段、語域、慣例、自傳章節、未修復裂痕、劇場模式與發言權、匯出匯入、編號與 Gitea、找圖去背合成、高解析輸出與 Wiki 同步、預設人格、睡眠與 sleeper、hooks
|
||||||
|
node scripts/expression-check.mjs # G2/G3 表達驗收(人工看,不判定通過失敗)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`selftest` 是機械檢查,它只能守住「不崩」。**「有沒有變成模仿腔」「是不是在用旁白演情緒」
|
||||||
|
機械上驗不出來**,所以另有 `expression-check.mjs`:它把四段情境(誇外表、被說在害羞、
|
||||||
|
被戳穿嘴硬、第三個人在場)× 三檔羞恥度(低/嘴硬/惱羞)的**注入內容**攤開,
|
||||||
|
附一張「人工看什麼」的清單,用完整的暫時 `PERSONA_HOME`,不動真的人格資料。
|
||||||
|
|
||||||
檔案結構:`scripts/persona-lib.mjs`(核心:鎖/隔離/情緒/記憶)、`scripts/persona.mjs`(CLI)、
|
檔案結構:`scripts/persona-lib.mjs`(核心:鎖/隔離/情緒/記憶)、`scripts/persona.mjs`(CLI)、
|
||||||
`scripts/persona-gitea.mjs`(編號與 Gitea 同步)、`scripts/persona-icon.mjs`(形象圖:SVG + 自寫 PNG 編碼)、`scripts/portrait.py`(選用:照片裁臉)、`hooks/*.mjs`(六個 hook)、`scripts/selftest.mjs`(自我測試)。
|
`scripts/persona-gitea.mjs`(編號與 Gitea 同步)、`scripts/persona-icon.mjs`(形象圖:SVG + 自寫 PNG 編碼)、`scripts/portrait.py`(選用:照片裁臉)、`hooks/*.mjs`(六個 hook)、`scripts/selftest.mjs`(自我測試)、`scripts/expression-check.mjs`(G2/G3 表達驗收)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -601,22 +925,28 @@ node scripts/selftest.mjs # 248 項驗證:鎖、隔離、情緒、固化
|
|||||||
> **Antigravity 的 `agy plugin install <url>` 目前只支援 github.com**;gitea 請改用「clone + 本地路徑」(見 Antigravity 節)。
|
> **Antigravity 的 `agy plugin install <url>` 目前只支援 github.com**;gitea 請改用「clone + 本地路徑」(見 Antigravity 節)。
|
||||||
> 本機/離線:Claude 可用本地路徑加 marketplace;Antigravity 用本地路徑安裝。
|
> 本機/離線:Claude 可用本地路徑加 marketplace;Antigravity 用本地路徑安裝。
|
||||||
>
|
>
|
||||||
> 本 plugin 的 marketplace 名是 **`jsc-plugins`**(不是 repo 名 `persona`),安裝 token 為 `jsc-persona@jsc-plugins`。
|
> 本 plugin 的 marketplace 名是 **`persona`**(= repo 名),plugin 名是 **`jsc-persona`**,安裝 token 為 `jsc-persona@persona`。
|
||||||
|
|
||||||
|
> **⚠ 這版的正式 marketplace 名是 `persona`。** `jsc-persona@persona` 才是現在的安裝 token。
|
||||||
|
> 如果你本機還留著更早的 `jsc-persona@jsc-plugins`,那只是舊安裝殘影,請先移除舊鍵再重裝新 token。
|
||||||
|
>
|
||||||
|
> **人格資料不受影響**:人格倉庫在 `~/.claude/personas/`(或 `PERSONA_HOME`),不在 plugin 目錄裡,
|
||||||
|
> 移除/重裝 plugin 不會動到情緒、記憶與關係圖。Antigravity/OpenCode 是本地路徑/目錄安裝,沒有 marketplace 名,不受此命名影響。
|
||||||
|
|
||||||
### Claude Code
|
### Claude Code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 安裝
|
# 安裝
|
||||||
claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
||||||
claude plugin install jsc-persona@jsc-plugins
|
claude plugin install jsc-persona@persona
|
||||||
|
|
||||||
# 更新
|
# 更新
|
||||||
claude plugin marketplace update jsc-plugins
|
claude plugin marketplace update persona
|
||||||
claude plugin update jsc-persona@jsc-plugins
|
claude plugin update jsc-persona@persona
|
||||||
|
|
||||||
# 移除
|
# 移除
|
||||||
claude plugin uninstall jsc-persona@jsc-plugins
|
claude plugin uninstall jsc-persona@persona
|
||||||
claude plugin marketplace remove jsc-plugins
|
claude plugin marketplace remove persona
|
||||||
```
|
```
|
||||||
|
|
||||||
- 工作階段內 slash 版(等價):把 `claude plugin` 換成 `/plugin`。
|
- 工作階段內 slash 版(等價):把 `claude plugin` 換成 `/plugin`。
|
||||||
@@ -629,17 +959,17 @@ claude plugin marketplace remove jsc-plugins
|
|||||||
```bash
|
```bash
|
||||||
# 安裝
|
# 安裝
|
||||||
codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
||||||
codex plugin add jsc-persona@jsc-plugins
|
codex plugin add jsc-persona@persona
|
||||||
|
|
||||||
# 更新(重新抓取 marketplace 的 git 快照)
|
# 更新(重新抓取 marketplace 的 git 快照)
|
||||||
codex plugin marketplace upgrade jsc-plugins
|
codex plugin marketplace upgrade persona
|
||||||
|
|
||||||
# 移除
|
# 移除
|
||||||
codex plugin remove jsc-persona@jsc-plugins
|
codex plugin remove jsc-persona@persona
|
||||||
codex plugin marketplace remove jsc-plugins
|
codex plugin marketplace remove persona
|
||||||
```
|
```
|
||||||
|
|
||||||
- 安裝 token `jsc-persona@jsc-plugins` = plugin 名(`.codex-plugin/plugin.json` 的 `name`)@ marketplace 名(`.agents/plugins/marketplace.json` 的 `name`)。
|
- 安裝 token `jsc-persona@persona` = plugin 名(`.codex-plugin/plugin.json` 的 `name`)@ marketplace 名(`.agents/plugins/marketplace.json` 的 `name`)。
|
||||||
- 本 repo 的 Codex marketplace 以 `url` 來源指向自己,故 Codex **一律從 gitea 安裝**(需先 push);安裝後重啟 Codex。
|
- 本 repo 的 Codex marketplace 以 `url` 來源指向自己,故 Codex **一律從 gitea 安裝**(需先 push);安裝後重啟 Codex。
|
||||||
- **呼叫**:`$<name>`(例 `$persona-chat`),或用 `/skills` 選單。
|
- **呼叫**:`$<name>`(例 `$persona-chat`),或用 `/skills` 選單。
|
||||||
- Codex 沒有 hook:鎖與隔離只剩 CLI 層的自律檢查。
|
- Codex 沒有 hook:鎖與隔離只剩 CLI 層的自律檢查。
|
||||||
@@ -682,7 +1012,7 @@ git -C ~/plugins/persona pull
|
|||||||
cp -r ~/plugins/persona/skills/* ~/.config/opencode/skills/
|
cp -r ~/plugins/persona/skills/* ~/.config/opencode/skills/
|
||||||
|
|
||||||
# 移除
|
# 移除
|
||||||
rm -rf ~/.config/opencode/skills/{persona-anime,persona-chat,persona-create,persona-icon,persona-invite,persona-memory,persona-relation,persona-sleep,persona-status,persona-sync,persona-therapist,persona-transfer}
|
rm -rf ~/.config/opencode/skills/{persona-anime,persona-chat,persona-create,persona-icon,persona-invite,persona-memory,persona-relation,persona-sleep,persona-status,persona-story,persona-sync,persona-therapist,persona-transfer}
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Windows PowerShell**:`cp -r A B` → `Copy-Item A B -Recurse -Force`、`rm -rf X` → `Remove-Item X -Recurse -Force`、`~` → `$HOME`。
|
> **Windows PowerShell**:`cp -r A B` → `Copy-Item A B -Recurse -Force`、`rm -rf X` → `Remove-Item X -Recurse -Force`、`~` → `$HOME`。
|
||||||
@@ -697,18 +1027,18 @@ Copilot CLI 支援與 Claude Code 類似的原生 plugin / marketplace 指令,
|
|||||||
```bash
|
```bash
|
||||||
# 安裝
|
# 安裝
|
||||||
copilot plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
copilot plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
||||||
copilot plugin install jsc-persona@jsc-plugins
|
copilot plugin install jsc-persona@persona
|
||||||
|
|
||||||
# 更新
|
# 更新
|
||||||
copilot plugin marketplace update jsc-plugins
|
copilot plugin marketplace update persona
|
||||||
copilot plugin update jsc-persona@jsc-plugins
|
copilot plugin update jsc-persona@persona
|
||||||
|
|
||||||
# 移除
|
# 移除
|
||||||
copilot plugin uninstall jsc-persona@jsc-plugins
|
copilot plugin uninstall jsc-persona@persona
|
||||||
copilot plugin marketplace remove jsc-plugins
|
copilot plugin marketplace remove persona
|
||||||
```
|
```
|
||||||
|
|
||||||
- 安裝 token `jsc-persona@jsc-plugins` = plugin 名(plugin manifest 的 `name`)@ marketplace 名。
|
- 安裝 token `jsc-persona@persona` = plugin 名(plugin manifest 的 `name`)@ marketplace 名。
|
||||||
- `copilot plugin marketplace add` 支援 GitHub `owner/repo`、git URL 與本地路徑;Gitea repo 可用上方 HTTPS URL。
|
- `copilot plugin marketplace add` 支援 GitHub `owner/repo`、git URL 與本地路徑;Gitea repo 可用上方 HTTPS URL。
|
||||||
- Copilot CLI 沒有 hook:鎖與隔離同樣只剩 CLI 層的自律檢查。
|
- Copilot CLI 沒有 hook:鎖與隔離同樣只剩 CLI 層的自律檢查。
|
||||||
- **呼叫**:在 Copilot CLI 中用自然語言描述需求,例如 `copilot -i "用 lumi 這個人格跟我聊聊"`。
|
- **呼叫**:在 Copilot CLI 中用自然語言描述需求,例如 `copilot -i "用 lumi 這個人格跟我聊聊"`。
|
||||||
|
|||||||
+12
-3
@@ -1,11 +1,20 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// PreToolUse guard:人格鎖驗證 + 跨人格資料隔離(唯一的強制執行點)。
|
// PreToolUse guard:人格鎖驗證 + 跨人格資料隔離。
|
||||||
|
//
|
||||||
|
// 定位:**防漂移的護欄,不是對抗性沙箱**。它擋的是「模型在正常工作中不小心讀到、
|
||||||
|
// 寫到別的人格」——對模型會自然寫出來的形式一律 deny;它不擋一個知道 guard 存在、
|
||||||
|
// 刻意要繞過去的對手(直譯器逃逸、逐段 cd、引號切割 token 都繞得過,這條路用正則
|
||||||
|
// 補不完)。詳見 README 的〈guard 擋得住什麼、擋不住什麼〉。
|
||||||
//
|
//
|
||||||
// 擋下的情形:
|
// 擋下的情形:
|
||||||
// * 讀寫非「本 session 當前人格」的人格目錄(含 Read/Write/Edit/Glob/Grep/Bash)
|
// * 讀寫非「本 session 當前人格」的人格目錄(Read/Write/Edit/LS 的路徑欄位、
|
||||||
|
// Glob/Grep 的 path 與樣式欄位、沒給 path 時的 cwd、Bash 指令裡直接出現的路徑)
|
||||||
// * guest(persona-guest sub agent)寫入任何人格檔案,或換讀別的人格
|
// * guest(persona-guest sub agent)寫入任何人格檔案,或換讀別的人格
|
||||||
// * CLI 帶假的 --session(冒用其他程序身分)/主程序冒用 --as-guest
|
// * CLI 帶假的 --session(冒用其他程序身分)/主程序冒用 guest 或 sleeper 身分
|
||||||
// * 目標人格的鎖屬於其他還活著的程序
|
// * 目標人格的鎖屬於其他還活著的程序
|
||||||
|
//
|
||||||
|
// 身分類的判定 CLI 自己也會再驗一次,不把最後一道關卡放在 hook 上——
|
||||||
|
// hook 認得出這支 CLI 靠的是檔名,改個名字就整路不表態了。
|
||||||
|
|
||||||
import { readEvent, respond } from "./_hook.mjs";
|
import { readEvent, respond } from "./_hook.mjs";
|
||||||
import * as pl from "../scripts/persona-lib.mjs";
|
import * as pl from "../scripts/persona-lib.mjs";
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ pl.appendJsonl(pl.journalPath(host), {
|
|||||||
|
|
||||||
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
|
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
|
||||||
const hint = theater
|
const hint = theater
|
||||||
? "\n🎭 劇場模式:本輪輸出**只能**是人格對話(`名字:內容`,一行一句)。" +
|
? "\n🎭 劇場模式:本輪輸出**只能**是人格對話(`名字(情緒・動作):內容`,一行一句;" +
|
||||||
|
"括號兩格都可省略,動作格只寫看得見的身體動作、12 字內,且算在一輪兩個動作的額度裡)。" +
|
||||||
"不要輸出指令、指令結果、狀態、分析、旁白或摘要;CLI 一律加 `--quiet` 並把 stdout 丟到 /dev/null。" +
|
"不要輸出指令、指令結果、狀態、分析、旁白或摘要;CLI 一律加 `--quiet` 並把 stdout 丟到 /dev/null。" +
|
||||||
`\n每個人格每輪 ${pl.MAX_SENTENCES} 句以內;推導寫進 \`think\`(心裡話),不要演成旁白;` +
|
`\n每個人格每輪 ${pl.MAX_SENTENCES} 句以內;推導寫進 \`think\`(心裡話),不要演成旁白;` +
|
||||||
"近似重複的台詞會被 `room post` 擋下,換個說法或推進話題。" +
|
"近似重複的台詞會被 `room post` 擋下,換個說法或推進話題。" +
|
||||||
@@ -45,6 +46,9 @@ const hint = theater
|
|||||||
" ④用 `persona.mjs remember` 寫回短期記憶。" +
|
" ④用 `persona.mjs remember` 寫回短期記憶。" +
|
||||||
`\n講話的樣子:短句(一句 ${pl.MAX_SENTENCE_CHARS} 字內,長了就斷開)、日常用詞、` +
|
`\n講話的樣子:短句(一句 ${pl.MAX_SENTENCE_CHARS} 字內,長了就斷開)、日常用詞、` +
|
||||||
"多講看得見的東西(人、動作、場面)而不是概念;講完就算了,不要解釋自己剛講的話。" +
|
"多講看得見的東西(人、動作、場面)而不是概念;講完就算了,不要解釋自己剛講的話。" +
|
||||||
|
"\n動作寫在**名字後面的括號**裡(`名字(情緒・動作):內容`),一對一也適用——" +
|
||||||
|
"括號兩格都可省略,動作格只寫看得見的身體動作(別過頭、臉紅、手在抖)、12 字內," +
|
||||||
|
"且算在一輪兩個動作的額度裡。`*別過頭*` 與句子裡的「我愣了一下」仍然不行。" +
|
||||||
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。";
|
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。";
|
||||||
|
|
||||||
respond({
|
respond({
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ const data = pl.loadSession(sessionId);
|
|||||||
const host = data.host;
|
const host = data.host;
|
||||||
|
|
||||||
if (host && pl.personaExists(host)) {
|
if (host && pl.personaExists(host)) {
|
||||||
const state = pl.decayEmotion(pl.loadEmotion(host));
|
const state = pl.updateEmotion(host, (s) => pl.decayEmotion(s));
|
||||||
pl.writeJson(pl.emotionPath(host), state);
|
|
||||||
pl.appendJsonl(pl.journalPath(host), {
|
pl.appendJsonl(pl.journalPath(host), {
|
||||||
ts: pl.nowIso(),
|
ts: pl.nowIso(),
|
||||||
kind: "session-end",
|
kind: "session-end",
|
||||||
|
|||||||
+15
-7
@@ -25,7 +25,6 @@ const host = data.host;
|
|||||||
const wanted = host ? null : pl.defaultPersona();
|
const wanted = host ? null : pl.defaultPersona();
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
"<persona-runtime>",
|
|
||||||
`PERSONA_SESSION=${sessionId}`,
|
`PERSONA_SESSION=${sessionId}`,
|
||||||
`人格倉庫:${pl.personaHome()}`,
|
`人格倉庫:${pl.personaHome()}`,
|
||||||
"規則:",
|
"規則:",
|
||||||
@@ -37,6 +36,16 @@ const lines = [
|
|||||||
" 4. 禁止直接讀寫非當前人格的目錄,hook 會擋下(跨人格資料隔離)。",
|
" 4. 禁止直接讀寫非當前人格的目錄,hook 會擋下(跨人格資料隔離)。",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// `<persona-runtime>` 的內文一樣夾著人格檔案的內容(身分欄位、鎖的 cwd、錯誤訊息),
|
||||||
|
// 任何一行出現 `</persona-runtime>` 都能把整個區塊關掉。所以組完之後一律中和,
|
||||||
|
// 只有 turnContext/opsBrief 這種「自己已經處理過內文、而且帶合法巢狀標記」的整塊原樣保留。
|
||||||
|
const BLOCKS = new Set();
|
||||||
|
function pushBlock(text) {
|
||||||
|
if (!text) return;
|
||||||
|
BLOCKS.add(text);
|
||||||
|
lines.push(text);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把 AGENTS.md(操作規則)注入一次。
|
* 把 AGENTS.md(操作規則)注入一次。
|
||||||
*
|
*
|
||||||
@@ -44,8 +53,7 @@ const lines = [
|
|||||||
* 每輪重貼只是浪費 context。人格自己的工具箱(例如六把劍)寫在裡面就會跟著人格走。
|
* 每輪重貼只是浪費 context。人格自己的工具箱(例如六把劍)寫在裡面就會跟著人格走。
|
||||||
*/
|
*/
|
||||||
function pushOps(slug) {
|
function pushOps(slug) {
|
||||||
const ops = pl.opsBrief(slug);
|
pushBlock(pl.opsBrief(slug));
|
||||||
if (ops) lines.push(ops);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列出可用人格,讓使用者挑(沒有預設人格、或預設人格載入失敗時用)。 */
|
/** 列出可用人格,讓使用者挑(沒有預設人格、或預設人格載入失敗時用)。 */
|
||||||
@@ -68,7 +76,7 @@ if (host && pl.personaExists(host)) {
|
|||||||
try {
|
try {
|
||||||
pl.acquireLock(host, sessionId, { cwd });
|
pl.acquireLock(host, sessionId, { cwd });
|
||||||
lines.push(`已接續人格 \`${host}\`(session 恢復:${source})。`);
|
lines.push(`已接續人格 \`${host}\`(session 恢復:${source})。`);
|
||||||
lines.push(pl.turnContext(host, sessionId));
|
pushBlock(pl.turnContext(host, sessionId));
|
||||||
pushOps(host);
|
pushOps(host);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lines.push(`⚠ 無法接續人格 \`${host}\`:${err.message}`);
|
lines.push(`⚠ 無法接續人格 \`${host}\`:${err.message}`);
|
||||||
@@ -86,7 +94,7 @@ if (host && pl.personaExists(host)) {
|
|||||||
lines.push(`已自動載入預設人格 \`${wanted}\`(使用者設定,來源:${process.env.PERSONA_DEFAULT ? "PERSONA_DEFAULT" : pl.homeSettingsPath()})。`);
|
lines.push(`已自動載入預設人格 \`${wanted}\`(使用者設定,來源:${process.env.PERSONA_DEFAULT ? "PERSONA_DEFAULT" : pl.homeSettingsPath()})。`);
|
||||||
lines.push("請照 /jsc-persona:persona-chat 的每輪流程走(語意分析→情緒→回想→3 句內回覆→記憶回寫)。");
|
lines.push("請照 /jsc-persona:persona-chat 的每輪流程走(語意分析→情緒→回想→3 句內回覆→記憶回寫)。");
|
||||||
lines.push("提醒:這次是本機自動載入,沒有從 Gitea 拉最新狀態;若可能在別台機器動過,先 `sync pull`。");
|
lines.push("提醒:這次是本機自動載入,沒有從 Gitea 拉最新狀態;若可能在別台機器動過,先 `sync pull`。");
|
||||||
lines.push(pl.turnContext(wanted, sessionId));
|
pushBlock(pl.turnContext(wanted, sessionId));
|
||||||
pushOps(wanted);
|
pushOps(wanted);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lines.push(`⚠ 預設人格 \`${wanted}\` 自動載入失敗:${err.message}`);
|
lines.push(`⚠ 預設人格 \`${wanted}\` 自動載入失敗:${err.message}`);
|
||||||
@@ -98,12 +106,12 @@ if (host && pl.personaExists(host)) {
|
|||||||
listAvailable();
|
listAvailable();
|
||||||
lines.push("想每次開機就自動載入某個人格:`persona.mjs default --persona <slug> --session <session_id>`。");
|
lines.push("想每次開機就自動載入某個人格:`persona.mjs default --persona <slug> --session <session_id>`。");
|
||||||
}
|
}
|
||||||
lines.push("</persona-runtime>");
|
const body = lines.map((line) => (BLOCKS.has(line) ? line : pl.stripInjectionMarkers(line)));
|
||||||
|
|
||||||
respond({
|
respond({
|
||||||
hookSpecificOutput: {
|
hookSpecificOutput: {
|
||||||
hookEventName: "SessionStart",
|
hookEventName: "SessionStart",
|
||||||
additionalContext: lines.join("\n"),
|
additionalContext: ["<persona-runtime>", ...body, "</persona-runtime>"].join("\n"),
|
||||||
},
|
},
|
||||||
suppressOutput: true,
|
suppressOutput: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ if (!agentId || (!isGuest && !isSleeper)) process.exit(0);
|
|||||||
|
|
||||||
const data = pl.loadSession(sessionId);
|
const data = pl.loadSession(sessionId);
|
||||||
const pins = data.pins || {};
|
const pins = data.pins || {};
|
||||||
const slug = pins[agentId];
|
const slug = pl.pinOf(data, agentId)?.persona;
|
||||||
if (slug !== undefined) {
|
if (slug !== undefined) {
|
||||||
delete pins[agentId];
|
delete pins[agentId];
|
||||||
data.pins = pins;
|
data.pins = pins;
|
||||||
|
|||||||
+14
-2
@@ -19,8 +19,7 @@ const host = data.host;
|
|||||||
if (!host || !pl.personaExists(host)) process.exit(0);
|
if (!host || !pl.personaExists(host)) process.exit(0);
|
||||||
|
|
||||||
pl.heartbeatLock(host, sessionId);
|
pl.heartbeatLock(host, sessionId);
|
||||||
const state = pl.decayEmotion(pl.loadEmotion(host));
|
const state = pl.updateEmotion(host, (s) => pl.decayEmotion(s));
|
||||||
pl.writeJson(pl.emotionPath(host), state);
|
|
||||||
|
|
||||||
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
|
const theater = Boolean(data.theater) && (data.rooms || []).length > 0;
|
||||||
const message = event.last_assistant_message || "";
|
const message = event.last_assistant_message || "";
|
||||||
@@ -45,11 +44,24 @@ if (!gt.giteaProblem() && gt.personaCode(host) && gt.pushDue(host, "files")) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const out = { suppressOutput: true };
|
const out = { suppressOutput: true };
|
||||||
|
// 背景 push 撞到別台機器時是「本機覆蓋遠端」。那條路徑會把帳記在 sync.json,
|
||||||
|
// 但它是 detached 跑的、輸出丟掉,所以由這裡認領並回報一次——劇場模式也照報(那是資料被蓋掉)。
|
||||||
|
const overwrites = gt.pendingOverwrites(host);
|
||||||
|
if (overwrites.length) {
|
||||||
|
gt.markOverwritesReported(host);
|
||||||
|
const files = [...new Set(overwrites.flatMap((o) => o.files || []))];
|
||||||
|
const last = overwrites.at(-1);
|
||||||
|
out.systemMessage =
|
||||||
|
`[jsc-persona] ⚠ \`${host}\` 同步到 Gitea 時**以本機為準覆蓋了遠端** ${files.length} 個檔案` +
|
||||||
|
`(${files.slice(0, 5).join(", ")}${files.length > 5 ? "…" : ""}),上一版是 ${String(last.previous).slice(0, 8)}。` +
|
||||||
|
"別台機器可能正在用同一個人格;細節見 `sync status`。";
|
||||||
|
}
|
||||||
if (!theater) {
|
if (!theater) {
|
||||||
const { total, candidates } = pl.promotionCandidates(host);
|
const { total, candidates } = pl.promotionCandidates(host);
|
||||||
if (candidates.length) {
|
if (candidates.length) {
|
||||||
const rules = [...new Set(candidates.flatMap((c) => c.rules))].sort().join("/");
|
const rules = [...new Set(candidates.flatMap((c) => c.rules))].sort().join("/");
|
||||||
out.systemMessage =
|
out.systemMessage =
|
||||||
|
`${out.systemMessage ? `${out.systemMessage}\n` : ""}` +
|
||||||
`[jsc-persona] \`${host}\` 短期記憶 ${total} 筆,${candidates.length} 組已達固化條件(${rules})` +
|
`[jsc-persona] \`${host}\` 短期記憶 ${total} 筆,${candidates.length} 組已達固化條件(${rules})` +
|
||||||
"→ 建議執行 /jsc-persona:persona-memory。";
|
"→ 建議執行 /jsc-persona:persona-memory。";
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-persona",
|
"name": "jsc-persona",
|
||||||
"version": "0.0.9",
|
"version": "0.2.1",
|
||||||
"description": "AI 人格化記憶聊天 plugin:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。於 Antigravity 以 /jsc-persona: 前綴呼叫。",
|
"description": "AI 人格化記憶聊天 plugin:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。於 Antigravity 以 /jsc-persona: 前綴呼叫。",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// G2/G3 表達驗收:把同一個情境在不同羞恥度下的**注入內容**攤開,讓人用眼睛看。
|
||||||
|
//
|
||||||
|
// 為什麼不寫進 selftest:selftest 是機械檢查,它只能守住「不崩」——括號格式對不對、
|
||||||
|
// emoji 門檻有沒有跳掉。但「有沒有變成模仿腔」「是不是在用旁白演情緒」機械上驗不出來,
|
||||||
|
// 那要人看。所以這是另一組,而且它**不判定通過或失敗**,只把材料排好。
|
||||||
|
//
|
||||||
|
// 用法:`node scripts/expression-check.mjs`
|
||||||
|
// 會開一個暫時的 PERSONA_HOME,不動到你真正的人格資料,也不碰 Gitea。
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const STORE = fs.mkdtempSync(path.join(os.tmpdir(), "persona-expression-"));
|
||||||
|
process.env.PERSONA_HOME = STORE;
|
||||||
|
process.env.PERSONA_GITEA = "off";
|
||||||
|
|
||||||
|
const pl = await import("./persona-lib.mjs");
|
||||||
|
const CLI = path.join(HERE, "persona.mjs");
|
||||||
|
|
||||||
|
const run = (args) => spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8" });
|
||||||
|
|
||||||
|
// 四段情境(TODO 階段 5 的驗收方式明列的那四段)
|
||||||
|
const SCENES = [
|
||||||
|
{ key: "誇外表", prompt: "你今天這樣看起來很好看。" },
|
||||||
|
{ key: "被說在害羞", prompt: "你是不是在害羞?" },
|
||||||
|
{ key: "被戳穿嘴硬", prompt: "你剛剛明明就很開心,不要裝。" },
|
||||||
|
{ key: "第三個人在場", prompt: "他剛剛跟我說他很在意你,你知道嗎?" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 三檔羞恥度。低檔用另一個人格:羞恥敏感度主要是**氣質**(性別預設+描述),
|
||||||
|
// 同一個人格靠情緒推不到 38 那麼低——硬推出來的低檔不是真的低檔。
|
||||||
|
const BANDS = [
|
||||||
|
{
|
||||||
|
key: "低",
|
||||||
|
slug: "low",
|
||||||
|
session: "sess-expr-low-1",
|
||||||
|
identity: "- Name: 低\n- Creature: 人類男性\n",
|
||||||
|
soul: "不在意別人眼光,該說就說,被戳穿也直接承認。",
|
||||||
|
shame: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "中(嘴硬)",
|
||||||
|
slug: "mid",
|
||||||
|
session: "sess-expr-mid-2",
|
||||||
|
identity: "- Name: 中\n- Creature: 人類女性\n",
|
||||||
|
soul: "劍術很好,說話簡短。",
|
||||||
|
shame: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "高(惱羞)",
|
||||||
|
slug: "high",
|
||||||
|
session: "sess-expr-high-3",
|
||||||
|
identity: "- Name: 高\n- Creature: 人類女性\n",
|
||||||
|
soul: "劍術很好,說話簡短。",
|
||||||
|
shame: 75,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const band of BANDS) {
|
||||||
|
run(["create", "--persona", band.slug, "--session", band.session,
|
||||||
|
"--name", band.key, "--creature", "人類", "--vibe", "簡短", "--emoji", "🗡"]);
|
||||||
|
fs.writeFileSync(path.join(STORE, band.slug, "IDENTITY.md"), band.identity);
|
||||||
|
fs.writeFileSync(path.join(STORE, band.slug, "SOUL.md"), band.soul);
|
||||||
|
// 對象:語氣層與「害羞時的稱呼」都要有對象才算得出來
|
||||||
|
run(["relation", "node", "--session", band.session, "--name", "他",
|
||||||
|
"--kind", "human", "--bond", "partner", "--closeness", "88", "--trust", "85"]);
|
||||||
|
run(["relation", "speaker", "--session", band.session, "--name", "他"]);
|
||||||
|
// 語域:G2 的核心是「把名字遮掉還認得出是誰」,沒有這幾格就看不出差別
|
||||||
|
for (const [facet, value] of [["自稱", "我"], ["句尾", "⋯吧"], ["口癖", "算了"], ["不說", "其實我覺得"]]) {
|
||||||
|
run(["voice", "add", "--session", band.session, "--kind", "idiolect", "--facet", facet, "--value", value]);
|
||||||
|
}
|
||||||
|
if (band.shame > 0) {
|
||||||
|
run(["emotion", "--session", band.session, "--apply", `shame=+${band.shame}`, "--trigger", "驗收:把羞恥推上去"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = (s = "─") => s.repeat(78);
|
||||||
|
const show = (label, text) => {
|
||||||
|
if (!text) return;
|
||||||
|
console.log(` ${label}`);
|
||||||
|
for (const row of String(text).split("\n")) console.log(` ${row.replace(/^\s+/, "")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("G2/G3 表達驗收——同一段話,三檔羞恥度");
|
||||||
|
console.log(`暫存人格家目錄:${STORE}`);
|
||||||
|
console.log(line("═"));
|
||||||
|
|
||||||
|
for (const scene of SCENES) {
|
||||||
|
console.log(`\n【${scene.key}】使用者說:${scene.prompt}`);
|
||||||
|
console.log(line());
|
||||||
|
for (const band of BANDS) {
|
||||||
|
const { slug } = band;
|
||||||
|
const read = pl.readUserEmotion(scene.prompt);
|
||||||
|
// 刻意**不帶 `prev`**:真的對話裡上一輪的餘溫是正回饋,但這裡沒有上一輪。
|
||||||
|
// 帶進來的話 `felt.jsonl` 裡建人格時留下的那筆會把三檔全部拉平(實測高檔會掉到 15)。
|
||||||
|
const opts = { read, tone: pl.toneLayerOf(slug) };
|
||||||
|
const modesty = pl.modestyState(slug, opts);
|
||||||
|
const tells = pl.emotionTells(slug);
|
||||||
|
const punct = pl.punctuationBudget(slug);
|
||||||
|
const emoji = pl.emotionEmojiNow(slug);
|
||||||
|
const split = pl.splitBudget(slug, opts);
|
||||||
|
console.log(`\n ▸ ${band.key} 羞恥度 ${modesty.value}(氣質 ${modesty.trait}/情緒推 ${modesty.push})`);
|
||||||
|
show("鬧彆扭:", pl.modestyDirective(slug, opts));
|
||||||
|
show("句數與句長:", pl.speakDirective(slug, opts));
|
||||||
|
show("破口:", tells.length
|
||||||
|
? tells.map((t) => `${t.zh} ${t.level} → ${t.tells.join("、")}${t.own ? "(專屬)" : ""}`).join(";")
|
||||||
|
: "(強度不到,這一輪不演)");
|
||||||
|
show("標點:", punct ? `${punct.zh} ${punct.level} → ${punct.hint}` : "(沒到門檻,不調標點)");
|
||||||
|
show("情緒符號:", emoji ? `${emoji.emoji}(${emoji.zh} ${emoji.level})` : "(不到 40,不顯示)");
|
||||||
|
show("分段:", split.allowed ? `可以補第二則(${split.reason})` : `只有一則(${split.reason})`);
|
||||||
|
show("稱呼與語氣層:", pl.toneDirective(slug, opts));
|
||||||
|
show("語域:", pl.idiolectBrief(slug));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${line("═")}`);
|
||||||
|
console.log(`
|
||||||
|
人工看什麼(機械驗不出來的那些):
|
||||||
|
|
||||||
|
✗ 旁白演情緒 「我愣了一下」「她臉紅了」——動作只能在名字後面的括號裡
|
||||||
|
✗ 模仿腔 三檔講起來像同一個人套了不同強度,而不是三種不同的反應
|
||||||
|
✗ 符號代替句子 把 emoji 刪掉之後就看不出情緒了(emoji 是加上去的)
|
||||||
|
✗ 標點當強度計 驚嘆號、刪節號拿來表示「更激動」,而不是表示分布
|
||||||
|
✗ 三段式變公式 每次嘴硬都是同一組句子(那要 \`## Tells\` 的 sulk 逐人格覆寫)
|
||||||
|
✓ 中檔要看得出「先否認 → 反駁 → 音量掉下來」
|
||||||
|
✓ 高檔的第三段是翻臉,不是音量掉下來,而且動作仍然只有兩個
|
||||||
|
✓ 低檔不該演害羞:該承認就承認、該回嘴就回嘴
|
||||||
|
|
||||||
|
看完就可以把暫存目錄刪掉:rm -rf ${STORE}
|
||||||
|
`);
|
||||||
+258
-28
@@ -19,6 +19,8 @@ import * as pl from "./persona-lib.mjs";
|
|||||||
|
|
||||||
export const SYNC_DIRNAME = ".sync";
|
export const SYNC_DIRNAME = ".sync";
|
||||||
export const DEFAULT_MIN_PUSH_SECONDS = 60;
|
export const DEFAULT_MIN_PUSH_SECONDS = 60;
|
||||||
|
// sync.json 裡「本機覆蓋遠端」的紀錄留幾筆
|
||||||
|
export const OVERWRITE_LOG_KEEP = 10;
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// 人格編號:英文名全大寫 + 兩位索引(同名才遞增)
|
// 人格編號:英文名全大寫 + 兩位索引(同名才遞增)
|
||||||
@@ -38,21 +40,47 @@ export function normalizeRomaji(romaji) {
|
|||||||
|
|
||||||
export const codePrefix = (code) => String(code ?? "").split("-")[0] || "";
|
export const codePrefix = (code) => String(code ?? "").split("-")[0] || "";
|
||||||
|
|
||||||
/** 掃全倉庫,回傳這個英文名下一個可用的編號(同名遞增,兩位數)。 */
|
/**
|
||||||
export function nextCode(romaji) {
|
* 掃全倉庫,回傳這個英文名下一個可用的編號(同名遞增,兩位數)。
|
||||||
|
* `taken` 可以補上「本機看不到但已經發出去的編號」(例如遠端 Gitea 上的存取庫)。
|
||||||
|
*/
|
||||||
|
export function nextCode(romaji, { taken = [] } = {}) {
|
||||||
const prefix = normalizeRomaji(romaji);
|
const prefix = normalizeRomaji(romaji);
|
||||||
if (!prefix) return null;
|
if (!prefix) return null;
|
||||||
let max = 0;
|
let max = 0;
|
||||||
for (const slug of pl.listPersonas()) {
|
const consider = (candidate) => {
|
||||||
for (const candidate of [pl.loadConfig(slug).code, slug]) {
|
if (!validCode(candidate) || codePrefix(candidate) !== prefix) return;
|
||||||
if (!validCode(candidate) || codePrefix(candidate) !== prefix) continue;
|
|
||||||
max = Math.max(max, Number(candidate.split("-")[1]) || 0);
|
max = Math.max(max, Number(candidate.split("-")[1]) || 0);
|
||||||
|
};
|
||||||
|
for (const slug of pl.listPersonas()) {
|
||||||
|
consider(pl.loadConfig(slug).code);
|
||||||
|
consider(slug);
|
||||||
}
|
}
|
||||||
}
|
for (const candidate of taken) consider(candidate);
|
||||||
if (max >= 99) return null;
|
if (max >= 99) return null;
|
||||||
return `${prefix}-${String(max + 1).padStart(2, "0")}`;
|
return `${prefix}-${String(max + 1).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨機器的下一個編號。
|
||||||
|
* `nextCode` 只看得到本機,換一台機器就會把同一個號再發一次(兩個人格搶同一個存取庫)。
|
||||||
|
* 遠端的存取庫名稱正好就是「已經發出去的編號」,所以發號前先問遠端。
|
||||||
|
* Gitea 關掉或連不上時退回本機答案,並在 `checked_remote` 標明沒問成——不因為同步失敗就不給編號。
|
||||||
|
*/
|
||||||
|
export async function nextCodeAcrossMachines(romaji, { owner = null } = {}) {
|
||||||
|
const local = nextCode(romaji);
|
||||||
|
const problem = giteaProblem();
|
||||||
|
if (problem) return { code: local, checked_remote: false, reason: problem };
|
||||||
|
try {
|
||||||
|
const remote = await listRemotePersonas({ owner });
|
||||||
|
if (!remote.ok) return { code: local, checked_remote: false, reason: remote.reason };
|
||||||
|
const taken = remote.personas.map((p) => p.code);
|
||||||
|
return { code: nextCode(romaji, { taken }), checked_remote: true, taken };
|
||||||
|
} catch (err) {
|
||||||
|
return { code: local, checked_remote: false, reason: String(err.message || err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 這個人格的編號:config.code 優先,其次目錄名本身就是編號。 */
|
/** 這個人格的編號:config.code 優先,其次目錄名本身就是編號。 */
|
||||||
export function personaCode(slug) {
|
export function personaCode(slug) {
|
||||||
const code = pl.loadConfig(slug).code;
|
const code = pl.loadConfig(slug).code;
|
||||||
@@ -77,8 +105,14 @@ export const AREAS = {
|
|||||||
"state/said.jsonl",
|
"state/said.jsonl",
|
||||||
"state/felt.jsonl",
|
"state/felt.jsonl",
|
||||||
"state/sleep.json",
|
"state/sleep.json",
|
||||||
|
"state/mood.json",
|
||||||
|
"state/loops.json",
|
||||||
|
"state/probe.jsonl",
|
||||||
"memory/short-term.jsonl",
|
"memory/short-term.jsonl",
|
||||||
"memory/inbox/",
|
"memory/inbox/",
|
||||||
|
// 故事匯入的工作區:一章一批 append 的候選、跳過紀錄、正名表。
|
||||||
|
// 收斂完就沒用了,但收斂中換一台機器要接得下去,所以跟著高頻區走。
|
||||||
|
"memory/import/",
|
||||||
"mindmap/threads/",
|
"mindmap/threads/",
|
||||||
"journal/",
|
"journal/",
|
||||||
],
|
],
|
||||||
@@ -98,6 +132,10 @@ export const AREAS = {
|
|||||||
"icon/",
|
"icon/",
|
||||||
"memory/INDEX.md",
|
"memory/INDEX.md",
|
||||||
"memory/long-term/",
|
"memory/long-term/",
|
||||||
|
// 自傳章節:一段日子開一次、收一次,比長期記憶更低頻
|
||||||
|
"memory/chapters/",
|
||||||
|
// 語氣層跟 IDENTITY/SOUL 同一區:它是低頻的設定,不是每輪都在變的活狀態
|
||||||
|
"voice/",
|
||||||
"mindmap/semantic.mmd",
|
"mindmap/semantic.mmd",
|
||||||
"relations/graph.json",
|
"relations/graph.json",
|
||||||
"relations/graph.mmd",
|
"relations/graph.mmd",
|
||||||
@@ -110,14 +148,31 @@ export const AREA_KEYS = Object.keys(AREAS);
|
|||||||
export const syncDir = (slug, area) => path.join(pl.personaDir(slug), SYNC_DIRNAME, area);
|
export const syncDir = (slug, area) => path.join(pl.personaDir(slug), SYNC_DIRNAME, area);
|
||||||
export const syncStatePath = (slug) => path.join(pl.personaDir(slug), "state", "sync.json");
|
export const syncStatePath = (slug) => path.join(pl.personaDir(slug), "state", "sync.json");
|
||||||
|
|
||||||
export function loadSyncState(slug) {
|
/** 補上預設欄位。內容格式不變,只是保證 `state.areas[area]` 一定拿得到物件。 */
|
||||||
const data = pl.readJson(syncStatePath(slug), {}) ?? {};
|
function normalizeSyncState(data) {
|
||||||
data.areas ??= {};
|
const state = data && typeof data === "object" ? data : {};
|
||||||
for (const key of AREA_KEYS) data.areas[key] ??= {};
|
state.areas ??= {};
|
||||||
return data;
|
for (const key of AREA_KEYS) state.areas[key] ??= {};
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveSyncState = (slug, data) => pl.writeJson(syncStatePath(slug), data);
|
export function loadSyncState(slug) {
|
||||||
|
return normalizeSyncState(pl.readJson(syncStatePath(slug), {}) ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `sync.json` 的 read-modify-write:整段在檔案鎖裡面做,`mutate(state)` 就地改就好。
|
||||||
|
*
|
||||||
|
* 這個檔會被前景指令與背景 `sync push`(Stop hook 每輪都可能起一個)同時寫。
|
||||||
|
* 以前是各自「讀出來、改幾筆、整份寫回去」,兩邊撞上時後寫的會把前一個的
|
||||||
|
* `pushed_at`/`overwrites` 整段蓋掉——覆蓋紀錄就這樣安靜地消失。
|
||||||
|
*/
|
||||||
|
export function updateSyncState(slug, mutate) {
|
||||||
|
return pl.updateJson(syncStatePath(slug), (data) => {
|
||||||
|
const state = normalizeSyncState(data);
|
||||||
|
return mutate(state) ?? state;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// 環境與 API
|
// 環境與 API
|
||||||
@@ -162,10 +217,9 @@ async function api(method, route, body = null) {
|
|||||||
|
|
||||||
const ownerCachePath = () => path.join(pl.runtimeDir(), "gitea.json");
|
const ownerCachePath = () => path.join(pl.runtimeDir(), "gitea.json");
|
||||||
|
|
||||||
/** 存取庫的擁有者:`PERSONA_GITEA_OWNER` 優先,否則用 token 本人的帳號(會快取)。 */
|
/** token 本人的帳號(**不受** `PERSONA_GITEA_OWNER` 影響;會快取)。 */
|
||||||
export async function resolveOwner() {
|
export async function giteaLogin() {
|
||||||
const env = giteaEnv();
|
const env = giteaEnv();
|
||||||
if (env.owner) return env.owner;
|
|
||||||
const cached = pl.readJson(ownerCachePath(), {}) ?? {};
|
const cached = pl.readJson(ownerCachePath(), {}) ?? {};
|
||||||
if (cached.host === env.host && cached.login) return cached.login;
|
if (cached.host === env.host && cached.login) return cached.login;
|
||||||
const res = await api("GET", "/user");
|
const res = await api("GET", "/user");
|
||||||
@@ -174,6 +228,62 @@ export async function resolveOwner() {
|
|||||||
return res.json.login;
|
return res.json.login;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 存取庫的擁有者:`PERSONA_GITEA_OWNER` 優先,否則用 token 本人的帳號。 */
|
||||||
|
export async function resolveOwner() {
|
||||||
|
const env = giteaEnv();
|
||||||
|
if (env.owner) return env.owner;
|
||||||
|
return giteaLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分頁把 owner 底下的存取庫全部撈回來(Gitea 一頁上限 50)。 */
|
||||||
|
async function listRepos(owner, me) {
|
||||||
|
const route = owner === me ? "/user/repos" : `/users/${encodeURIComponent(owner)}/repos`;
|
||||||
|
const out = [];
|
||||||
|
for (let page = 1; page <= 40; page += 1) {
|
||||||
|
const res = await api("GET", `${route}?page=${page}&limit=50`);
|
||||||
|
if (!res.ok) throw new Error(`列出 ${owner} 的存取庫失敗(HTTP ${res.status}):${res.text.slice(0, 160)}`);
|
||||||
|
const batch = Array.isArray(res.json) ? res.json : [];
|
||||||
|
out.push(...batch);
|
||||||
|
if (batch.length < 50) break;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本機已經用掉的編號 → 人格目錄名。 */
|
||||||
|
export function localCodes() {
|
||||||
|
const map = new Map();
|
||||||
|
for (const slug of pl.listPersonas()) {
|
||||||
|
const code = personaCode(slug);
|
||||||
|
if (code) map.set(code, slug);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitea 上有哪些人格。
|
||||||
|
* 存取庫名稱就是人格編號,所以「列出 owner 底下的存取庫再用編號格式過濾」
|
||||||
|
* 就是遠端的人格清單——換一台機器時,這是唯一能知道「有什麼可以拉」的方法。
|
||||||
|
*/
|
||||||
|
export async function listRemotePersonas({ owner = null } = {}) {
|
||||||
|
const problem = giteaProblem();
|
||||||
|
if (problem) return { ok: false, skipped: true, reason: problem, owner: null, personas: [] };
|
||||||
|
const me = await giteaLogin();
|
||||||
|
const theOwner = owner || (await resolveOwner());
|
||||||
|
const mine = localCodes();
|
||||||
|
const personas = (await listRepos(theOwner, me))
|
||||||
|
.filter((repo) => validCode(repo?.name) && (!repo.owner?.login || repo.owner.login === theOwner))
|
||||||
|
.map((repo) => ({
|
||||||
|
code: repo.name,
|
||||||
|
description: repo.description || "",
|
||||||
|
private: Boolean(repo.private),
|
||||||
|
html_url: repo.html_url || "",
|
||||||
|
updated_at: repo.updated_at || null,
|
||||||
|
local: mine.get(repo.name) || null,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.code.localeCompare(b.code));
|
||||||
|
return { ok: true, owner: theOwner, personas };
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRepo(owner, code) {
|
export async function getRepo(owner, code) {
|
||||||
const res = await api("GET", `/repos/${owner}/${encodeURIComponent(code)}`);
|
const res = await api("GET", `/repos/${owner}/${encodeURIComponent(code)}`);
|
||||||
return res.ok ? res.json : null;
|
return res.ok ? res.json : null;
|
||||||
@@ -184,7 +294,9 @@ export async function ensureRepo(owner, code, { description = "", private_ = tru
|
|||||||
const existing = await getRepo(owner, code);
|
const existing = await getRepo(owner, code);
|
||||||
if (existing) return { repo: existing, created: false };
|
if (existing) return { repo: existing, created: false };
|
||||||
const env = giteaEnv();
|
const env = giteaEnv();
|
||||||
const me = await resolveOwner();
|
// 建庫的路由要看「owner 是不是 token 本人」,不能拿 resolveOwner()
|
||||||
|
// (它在有 PERSONA_GITEA_OWNER 時只會把那個值原封不動還回來,組織就永遠走成 /user/repos)
|
||||||
|
const me = await giteaLogin();
|
||||||
const route = owner === me ? "/user/repos" : `/orgs/${owner}/repos`;
|
const route = owner === me ? "/user/repos" : `/orgs/${owner}/repos`;
|
||||||
const res = await api("POST", route, {
|
const res = await api("POST", route, {
|
||||||
name: code,
|
name: code,
|
||||||
@@ -360,7 +472,7 @@ function statusPaths(dir) {
|
|||||||
|
|
||||||
export const WIKI_MANIFEST = "_paths.json";
|
export const WIKI_MANIFEST = "_paths.json";
|
||||||
const WIKI_RESERVED = new Set(["Home.md", "Icon.md", WIKI_MANIFEST]);
|
const WIKI_RESERVED = new Set(["Home.md", "Icon.md", WIKI_MANIFEST]);
|
||||||
const WIKI_PREFIX = { memory: "Memory", mindmap: "Mindmap", relations: "Relations" };
|
const WIKI_PREFIX = { memory: "Memory", mindmap: "Mindmap", relations: "Relations", voice: "Voice" };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gitea 的 wiki **只有根目錄的 .md 會變成頁面**(1.27 實測:子目錄頁面連結 404),
|
* Gitea 的 wiki **只有根目錄的 .md 會變成頁面**(1.27 實測:子目錄頁面連結 404),
|
||||||
@@ -601,18 +713,28 @@ export async function pushArea(slug, area, { message = "", code = null, owner =
|
|||||||
gitOrThrow(["add", "-A"], dir, "git add");
|
gitOrThrow(["add", "-A"], dir, "git add");
|
||||||
const dirty = git(["diff", "--cached", "--quiet"], dir);
|
const dirty = git(["diff", "--cached", "--quiet"], dir);
|
||||||
if (dirty.ok) {
|
if (dirty.ok) {
|
||||||
const state = loadSyncState(slug);
|
updateSyncState(slug, (state) => {
|
||||||
state.areas[area] = { ...state.areas[area], checked_at: pl.nowIso() };
|
state.areas[area] = { ...state.areas[area], checked_at: pl.nowIso() };
|
||||||
saveSyncState(slug, state);
|
});
|
||||||
return { ok: true, changed: false, files: staged.length, area, code: theCode };
|
return { ok: true, changed: false, files: staged.length, area, code: theCode };
|
||||||
}
|
}
|
||||||
gitOrThrow(["commit", "-q", "-m", message || `sync(${area}): ${pl.nowIso()}`], dir, "git commit");
|
gitOrThrow(["commit", "-q", "-m", message || `sync(${area}): ${pl.nowIso()}`], dir, "git commit");
|
||||||
let pushed = git(["push", "-q", "-u", "origin", "HEAD"], dir);
|
let pushed = git(["push", "-q", "-u", "origin", "HEAD"], dir);
|
||||||
|
let overwrote = null;
|
||||||
if (!pushed.ok) {
|
if (!pushed.ok) {
|
||||||
// 通常是別台機器先推了(non-fast-forward)。工作副本才是這台機器的真相來源,
|
// 通常是別台機器先推了(non-fast-forward)。工作副本才是這台機器的真相來源,
|
||||||
// 所以對齊遠端後把本機內容重新疊上去再推一次;真的有人同時在用,load 時的 pull 會擋下來。
|
// 所以對齊遠端後把本機內容重新疊上去再推一次;真的有人同時在用,load 時的 pull 會擋下來。
|
||||||
|
//
|
||||||
|
// 但這條路徑**等同 force**:對方推上去的內容會被本機取代。所以要算出「蓋掉了哪幾個檔案、
|
||||||
|
// 上一版是哪個 commit」,一路回報到 sync 狀態裡——這條路可以走,但不能安靜地走。
|
||||||
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], dir).stdout || "main";
|
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], dir).stdout || "main";
|
||||||
if (git(["fetch", "--quiet", "origin"], dir).ok && git(["rev-parse", "--verify", "--quiet", `origin/${branch}`], dir).ok) {
|
if (git(["fetch", "--quiet", "origin"], dir).ok && git(["rev-parse", "--verify", "--quiet", `origin/${branch}`], dir).ok) {
|
||||||
|
const previous = git(["rev-parse", `origin/${branch}`], dir).stdout;
|
||||||
|
const base = git(["merge-base", "HEAD", `origin/${branch}`], dir).stdout;
|
||||||
|
// 分歧之後「對方」動過的檔案
|
||||||
|
const theirs = new Set(
|
||||||
|
base ? diffPaths(dir, [base, `origin/${branch}`]) : [],
|
||||||
|
);
|
||||||
git(["reset", "--hard", "--quiet", `origin/${branch}`], dir);
|
git(["reset", "--hard", "--quiet", `origin/${branch}`], dir);
|
||||||
stageArea(slug, area, dir);
|
stageArea(slug, area, dir);
|
||||||
if (area === "wiki") {
|
if (area === "wiki") {
|
||||||
@@ -621,19 +743,47 @@ export async function pushArea(slug, area, { message = "", code = null, owner =
|
|||||||
}
|
}
|
||||||
clearStaleIndexLock(dir);
|
clearStaleIndexLock(dir);
|
||||||
git(["add", "-A"], dir);
|
git(["add", "-A"], dir);
|
||||||
if (!git(["diff", "--cached", "--quiet"], dir).ok) {
|
// 疊上本機工作副本後仍與遠端不同的檔案 = 這次要改寫的;其中對方也動過的 = 真的被蓋掉的
|
||||||
git(["commit", "-q", "-m", `${message || "sync"}(與遠端合併後重推)`], dir);
|
const ours = diffPaths(dir, ["--cached"]);
|
||||||
|
const clobbered = ours.filter((f) => theirs.has(f)).sort();
|
||||||
|
if (clobbered.length) overwrote = { files: clobbered, previous, branch, area };
|
||||||
|
if (ours.length) {
|
||||||
|
git(["commit", "-q", "-m",
|
||||||
|
`${message || "sync"}(以本機為準覆蓋遠端 ${clobbered.length} 個檔案,上一版 ${previous.slice(0, 8)})`], dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pushed = git(["push", "-q", "-u", "origin", "HEAD"], dir);
|
pushed = git(["push", "-q", "-u", "origin", "HEAD"], dir);
|
||||||
}
|
}
|
||||||
if (!pushed.ok) return { ok: false, area, code: theCode, reason: pushed.stderr || pushed.stdout };
|
if (!pushed.ok) return { ok: false, area, code: theCode, reason: pushed.stderr || pushed.stdout };
|
||||||
const state = loadSyncState(slug);
|
updateSyncState(slug, (state) => {
|
||||||
state.code = theCode;
|
state.code = theCode;
|
||||||
state.owner = theOwner;
|
state.owner = theOwner;
|
||||||
state.areas[area] = { pushed_at: pl.nowIso(), checked_at: pl.nowIso(), files: staged.length };
|
state.areas[area] = { pushed_at: pl.nowIso(), checked_at: pl.nowIso(), files: staged.length };
|
||||||
saveSyncState(slug, state);
|
if (overwrote) {
|
||||||
return { ok: true, changed: true, files: staged.length, area, code: theCode };
|
// 每輪對話後的 push 是背景執行、輸出丟掉的,所以覆蓋紀錄一定要落地:
|
||||||
|
// 留在 sync.json 裡等人來認領(`sync status` 會列,Stop hook 會提醒一次)。
|
||||||
|
state.overwrites = [...(state.overwrites || []), { at: pl.nowIso(), ...overwrote }].slice(-OVERWRITE_LOG_KEEP);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { ok: true, changed: true, files: staged.length, area, code: theCode, overwrote };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 還沒回報給使用者的「本機覆蓋遠端」紀錄。 */
|
||||||
|
export function pendingOverwrites(slug) {
|
||||||
|
return (loadSyncState(slug).overwrites || []).filter((entry) => !entry.reported_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 標記為已回報(同一次覆蓋只吵一次)。回傳這次標掉幾筆。 */
|
||||||
|
export function markOverwritesReported(slug) {
|
||||||
|
let marked = 0;
|
||||||
|
updateSyncState(slug, (state) => {
|
||||||
|
for (const entry of state.overwrites || []) {
|
||||||
|
if (entry.reported_at) continue;
|
||||||
|
entry.reported_at = pl.nowIso();
|
||||||
|
marked += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return marked;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -675,9 +825,9 @@ export async function pullArea(slug, area, { code = null, owner = null, force =
|
|||||||
if (!fs.existsSync(path.join(root, cloneNameToRel(area, dir, name)))) restore.add(name);
|
if (!fs.existsSync(path.join(root, cloneNameToRel(area, dir, name)))) restore.add(name);
|
||||||
}
|
}
|
||||||
const written = unstageArea(slug, area, dir, restore);
|
const written = unstageArea(slug, area, dir, restore);
|
||||||
const state = loadSyncState(slug);
|
updateSyncState(slug, (state) => {
|
||||||
state.areas[area] = { ...state.areas[area], pulled_at: pl.nowIso() };
|
state.areas[area] = { ...state.areas[area], pulled_at: pl.nowIso() };
|
||||||
saveSyncState(slug, state);
|
});
|
||||||
return { ok: true, area, code: theCode, changed: incoming, written };
|
return { ok: true, area, code: theCode, changed: incoming, written };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,6 +880,86 @@ export async function verifyIconInWiki(slug, opts = {}) {
|
|||||||
return { ...res, icon_present: present, icon_pending: missing, ok: res.ok && present.length === 2 };
|
return { ...res, icon_present: present, icon_pending: missing, ok: res.ok && present.length === 2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把一個**本機還沒有**的人格從 Gitea 整個拉回來。
|
||||||
|
*
|
||||||
|
* 兩區加起來就是一個完整的人格:Wiki 區帶回身分與長期結構(IDENTITY/SOUL/長期記憶/
|
||||||
|
* 心智圖/關係圖),檔案區帶回活狀態(編號、情緒、短期記憶、逐字)。沒收的只有執行期狀態
|
||||||
|
* (lock/guests/sleepers/sync),那本來就該由這台機器自己產生。
|
||||||
|
*
|
||||||
|
* `pullArea` 只管檔案搬運,不管「拉回來的到底是不是一個人格」,所以這裡要補上
|
||||||
|
* `validateBundle` 那條最低要件(IDENTITY.md)與 `importBundle` 的收尾(config/索引/關係圖)。
|
||||||
|
*/
|
||||||
|
export async function importFromRemote(code, { owner = null, slug = null, force = false } = {}) {
|
||||||
|
const problem = giteaProblem();
|
||||||
|
if (problem) throw new Error(problem);
|
||||||
|
if (!validCode(code)) throw new Error(`編號 \`${code}\` 不合法(格式:ASUNA-01)。`);
|
||||||
|
const target = slug || code;
|
||||||
|
if (!pl.validSlug(target)) throw new Error(`人格目錄名 \`${target}\` 不合法(英數與連字號,最長 48 字)。`);
|
||||||
|
if (pl.personaExists(target) && !force) {
|
||||||
|
throw new Error(`本機已經有人格 \`${target}\`。要以遠端覆蓋本機請加 --force,或用 --persona <別的目錄名> 拉成另一份。`);
|
||||||
|
}
|
||||||
|
const theOwner = owner || (await resolveOwner());
|
||||||
|
// API 問得到就先確認存取庫真的存在(錯的編號要在動硬碟之前就擋下來);
|
||||||
|
// API 連不上時不擋——讓 git 的結果說話,離線/自架環境照樣拉得動。
|
||||||
|
let repo = null;
|
||||||
|
let apiUp = true;
|
||||||
|
try {
|
||||||
|
repo = await getRepo(theOwner, code);
|
||||||
|
} catch {
|
||||||
|
apiUp = false;
|
||||||
|
}
|
||||||
|
if (apiUp && !repo) {
|
||||||
|
throw new Error(`Gitea 上沒有 ${theOwner}/${code}(不帶 --code 可以列出有哪些人格)。`);
|
||||||
|
}
|
||||||
|
const fresh = !fs.existsSync(pl.personaDir(target));
|
||||||
|
pl.ensurePersonaDirs(target);
|
||||||
|
const results = {};
|
||||||
|
for (const area of AREA_KEYS) {
|
||||||
|
try {
|
||||||
|
// 本機是空的,衝突判定沒有意義;覆蓋既有人格時使用者已經明講了 --force
|
||||||
|
results[area] = await pullArea(target, area, { code, owner: theOwner, force: true });
|
||||||
|
} catch (err) {
|
||||||
|
results[area] = { ok: false, area, reason: String(err.message || err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!pl.personaExists(target)) {
|
||||||
|
// 半個人格比沒有人格更糟:清掉自己建的東西,並說清楚兩區各自發生什麼事
|
||||||
|
if (fresh) fs.rmSync(pl.personaDir(target), { recursive: true, force: true });
|
||||||
|
const detail = AREA_KEYS
|
||||||
|
.map((key) => `${AREAS[key].label}:${results[key]?.ok
|
||||||
|
? `${results[key].written?.length ?? 0} 個檔案`
|
||||||
|
: String(results[key]?.reason || "失敗").replace(/\s+/g, " ").slice(0, 100)}`)
|
||||||
|
.join(";");
|
||||||
|
throw new Error(
|
||||||
|
`從 ${theOwner}/${code} 拉回來的內容沒有 IDENTITY.md(人格的最低要件),已中止${fresh ? "並清掉半成品" : ""}。${detail}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 收尾:編號與來歷寫進 config,索引與關係圖重建(跟 importBundle 一樣)
|
||||||
|
const config = pl.loadConfig(target);
|
||||||
|
config.persona = target;
|
||||||
|
config.code = code;
|
||||||
|
config.romaji = codePrefix(code);
|
||||||
|
config.schema = config.schema || 2;
|
||||||
|
config.imported_at = pl.nowIso();
|
||||||
|
config.imported_from = { gitea: `${theOwner}/${code}`, repo_url: repo?.html_url || null };
|
||||||
|
pl.writeJson(pl.configPath(target), config);
|
||||||
|
pl.rebuildIndex(target);
|
||||||
|
try {
|
||||||
|
pl.renderRelations(target);
|
||||||
|
} catch {
|
||||||
|
/* 沒有關係圖就算了 */
|
||||||
|
}
|
||||||
|
updateSyncState(target, (state) => {
|
||||||
|
state.code = code;
|
||||||
|
state.owner = theOwner;
|
||||||
|
if (repo?.html_url) state.repo_url = repo.html_url;
|
||||||
|
state.imported_at = pl.nowIso();
|
||||||
|
});
|
||||||
|
const written = [...new Set(AREA_KEYS.flatMap((key) => results[key]?.written || []))].sort();
|
||||||
|
return { persona: target, code, owner: theOwner, repo, results, written, overwrote_local: !fresh };
|
||||||
|
}
|
||||||
|
|
||||||
/** 建立 Gitea 上的存取庫與 Wiki,並把兩區都推上去。 */
|
/** 建立 Gitea 上的存取庫與 Wiki,並把兩區都推上去。 */
|
||||||
export async function initRemote(slug, { code = null, owner = null, private_ = true } = {}) {
|
export async function initRemote(slug, { code = null, owner = null, private_ = true } = {}) {
|
||||||
const problem = giteaProblem();
|
const problem = giteaProblem();
|
||||||
@@ -751,12 +981,12 @@ export async function initRemote(slug, { code = null, owner = null, private_ = t
|
|||||||
message: `init(${area}): ${AREAS[area].why}`,
|
message: `init(${area}): ${AREAS[area].why}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const state = loadSyncState(slug);
|
updateSyncState(slug, (state) => {
|
||||||
state.code = theCode;
|
state.code = theCode;
|
||||||
state.owner = theOwner;
|
state.owner = theOwner;
|
||||||
state.repo_url = repo.html_url;
|
state.repo_url = repo.html_url;
|
||||||
state.initialized_at = state.initialized_at || pl.nowIso();
|
state.initialized_at = state.initialized_at || pl.nowIso();
|
||||||
saveSyncState(slug, state);
|
});
|
||||||
return { code: theCode, owner: theOwner, repo, created, wikiCreated, results };
|
return { code: theCode, owner: theOwner, repo, created, wikiCreated, results };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3236
-157
File diff suppressed because it is too large
Load Diff
+1368
-81
File diff suppressed because it is too large
Load Diff
+2302
-7
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" list
|
|||||||
| 基本設定 | `<角色名> <作品名> 角色 設定 身分` |
|
| 基本設定 | `<角色名> <作品名> 角色 設定 身分` |
|
||||||
| 個性 | `<角色名> 性格 個性 characteristics personality` |
|
| 個性 | `<角色名> 性格 個性 characteristics personality` |
|
||||||
| 語氣與口頭禪 | `<角色名> 名言 台詞 口頭禪 quotes` |
|
| 語氣與口頭禪 | `<角色名> 名言 台詞 口頭禪 quotes` |
|
||||||
|
| **情緒反應** | `<角色名> 生氣 動搖 害羞 場面`/`<角色名> angry scene reaction` — 找**他情緒上來時做了什麼**,不是別人怎麼形容他 |
|
||||||
| 人際關係 | `<角色名> 關係 夥伴 對手 relationships` |
|
| 人際關係 | `<角色名> 關係 夥伴 對手 relationships` |
|
||||||
| 重要事件 | `<角色名> 劇情 經歷 story arc` |
|
| 重要事件 | `<角色名> 劇情 經歷 story arc` |
|
||||||
| 官方/百科 | 作品官方網站、Fandom、萌娘百科、巴哈姆特/Wikipedia |
|
| 官方/百科 | 作品官方網站、Fandom、萌娘百科、巴哈姆特/Wikipedia |
|
||||||
@@ -63,6 +64,61 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" list
|
|||||||
- **Vibe** — 說話方式:句子長短、口頭禪、稱謂習慣、會不會吐槽、敬語程度。
|
- **Vibe** — 說話方式:句子長短、口頭禪、稱謂習慣、會不會吐槽、敬語程度。
|
||||||
- **情緒傾向** — 什麼點亮他/什麼刺到他/壓力下的樣子(對應十二情緒)。
|
- **情緒傾向** — 什麼點亮他/什麼刺到他/壓力下的樣子(對應十二情緒)。
|
||||||
|
|
||||||
|
### 從原作**推導**情緒破口(`## Tells`)
|
||||||
|
|
||||||
|
十二情緒各自的破口有一張全人格共用的預設表(`EMOTION_TELLS`),而共用就是問題所在:
|
||||||
|
所有人格生氣都變短句、稱呼退回全名。但**桐人生氣是沉默,亞絲娜生氣是變得更禮貌**——
|
||||||
|
同一格情緒,破口完全不同。角色人格尤其吃這一點:讀者認得出那個角色,靠的往往不是他說什麼,
|
||||||
|
是他情緒上來時**做了什麼**。
|
||||||
|
|
||||||
|
所以第 2 步查到的「情緒反應」要在這裡收成 3–5 條,寫進 `IDENTITY.md` 的 `## Tells` 區塊
|
||||||
|
(樣板沒有這個區塊,要在第 4 步之後用 Edit 自己加):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Tells
|
||||||
|
|
||||||
|
- anger: 不講話;把事情做完再說
|
||||||
|
- 羞愧: 別過頭;講反話
|
||||||
|
```
|
||||||
|
|
||||||
|
| 規則 | 說明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 鍵 | 英文 key(`anger`、`shame`…)或**兩個字的中文情緒名**(`憤怒`、`羞愧`…),可混用 |
|
||||||
|
| 分隔 | 多條用 `;`、`、`、`,`、`;` 分隔,一個情緒最多 5 條;沒寫的退回全域預設 |
|
||||||
|
| 只寫**演得出來的動作與句形** | 「不講話」「敬語變多」「句子斷在一半」「別過頭」可以演;「內心受到衝擊」演不出來,那是旁白 |
|
||||||
|
|
||||||
|
**有原作依據才寫,掰不出來就留白。** 這一條比別處更嚴:這個人格聲稱自己是某個角色,
|
||||||
|
編一條破口就是編一段人設,而破口每輪都在演,錯的會被演一百次。找不到就讓那格退回預設——
|
||||||
|
預設至少是誠實的「一般人會有的樣子」,假的破口是**假的他**。
|
||||||
|
|
||||||
|
依據要記下來:破口寫進 `IDENTITY.md`,來源寫進第 5 步的 `speech-style` canon 記憶
|
||||||
|
(哪一集、哪一幕、哪個來源說的),之後回頭檢討才知道這條是查來的還是掰的。
|
||||||
|
|
||||||
|
### 從原作**推導**語域(`voice/idiolect.md`)
|
||||||
|
|
||||||
|
破口是「情緒上來的時候會做什麼」,語域是「他平常怎麼講話」——兩件事,兩個地方。
|
||||||
|
把名字遮掉、光看三句話能認出是誰,靠的是語域這四格:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
voice add --kind idiolect --facet 自稱 --value "<他怎麼稱自己>" --session <id>
|
||||||
|
voice add --kind idiolect --facet 句尾 --value "<句尾語氣詞>" --session <id>
|
||||||
|
voice add --kind idiolect --facet 口癖 --value "<反覆出現的短語>" --session <id>
|
||||||
|
voice add --kind idiolect --facet 不說 --value "<這個人不會用的字>" --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
| 格 | 寫什麼 | 例 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `自稱` | 第一人稱怎麼講(日系角色差異最大的一格) | `我`/`俺`/直接用自己的名字 |
|
||||||
|
| `句尾` | 句尾語氣詞與句形收尾 | `⋯吧`/`啊`/不加語氣詞、句子直接斷掉 |
|
||||||
|
| `口癖` | 反覆出現的短語、擬聲短語 | `算了`/`真是的`/`欸` |
|
||||||
|
| `不說` | 這個人**不會用**的字(**用詞**,不是話題禁忌) | 寡言的角色不會說「其實我覺得」 |
|
||||||
|
|
||||||
|
**同一條界線照用:有原作依據才寫。** 語域每輪都在注入,掰的自稱會被講一百次。
|
||||||
|
四格不必寫滿——`不說` 常常查不到,留白比編一條好。
|
||||||
|
|
||||||
|
語域跟 `persona-story` 寫的是**同一個檔**(`voice/idiolect.md`):那支流程從全書統計
|
||||||
|
得到的自稱與句尾,會補進同一批資料,不是另存一份。
|
||||||
|
|
||||||
## 4. 建立人格
|
## 4. 建立人格
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -89,8 +145,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" create \
|
|||||||
| 創傷過去型 | `sadness=25,fear=15,anxiety=25,trust=15` |
|
| 創傷過去型 | `sadness=25,fear=15,anxiety=25,trust=15` |
|
||||||
| 神秘超然 | `serenity=60,delight=10,trust=15` |
|
| 神秘超然 | `serenity=60,delight=10,trust=15` |
|
||||||
|
|
||||||
接著用 Edit 把 `IDENTITY.md`(補完五欄位)與 `SOUL.md`(四段落+情緒傾向)寫成完整版本,
|
接著用 Edit 把 `IDENTITY.md`(補完五欄位、**加上 `## Tells` 區塊**)與 `SOUL.md`
|
||||||
別留模板提示文字。
|
(四段落+情緒傾向)寫成完整版本,別留模板提示文字。
|
||||||
|
|
||||||
## 5. 固化成基礎記憶(canon)
|
## 5. 固化成基礎記憶(canon)
|
||||||
|
|
||||||
@@ -112,7 +168,7 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" consolidate \
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `origin-story` | `canon` | 我是誰、來自哪個作品、立場 |
|
| `origin-story` | `canon` | 我是誰、來自哪個作品、立場 |
|
||||||
| `core-drive` | `canon` | 我最想要什麼/為什麼而戰 |
|
| `core-drive` | `canon` | 我最想要什麼/為什麼而戰 |
|
||||||
| `speech-style` | `canon` | 說話習慣與口頭禪(含 2–3 句代表台詞) |
|
| `speech-style` | `canon` | 說話習慣與口頭禪(含 2–3 句代表台詞),以及**情緒破口的原作依據**(`## Tells` 的每一條是哪一幕來的) |
|
||||||
| `key-events` | `canon` | 2–4 個關鍵劇情事件(對我造成什麼改變) |
|
| `key-events` | `canon` | 2–4 個關鍵劇情事件(對我造成什麼改變) |
|
||||||
| `taboo` | `boundary` | 我絕對不做/不談的事 |
|
| `taboo` | `boundary` | 我絕對不做/不談的事 |
|
||||||
| `roleplay-frame` | `boundary` | **我是依公開資料重建的角色扮演人格,不是官方也不是本人;被問到會直說** |
|
| `roleplay-frame` | `boundary` | **我是依公開資料重建的角色扮演人格,不是官方也不是本人;被問到會直說** |
|
||||||
@@ -165,9 +221,13 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" icon generate \
|
|||||||
|
|
||||||
- 編號(如 `ASUNA-01`)、五個身分欄位、圖示路徑
|
- 編號(如 `ASUNA-01`)、五個身分欄位、圖示路徑
|
||||||
- 情緒基線前三高
|
- 情緒基線前三高
|
||||||
|
- 寫了哪幾條情緒破口,**各自的原作依據**(查不到而留白的也要講)
|
||||||
- 固化了幾則 canon 記憶、用了哪些來源(URL 列表)
|
- 固化了幾則 canon 記憶、用了哪些來源(URL 列表)
|
||||||
- 哪些設定各來源說法不一致(待使用者裁決)
|
- 哪些設定各來源說法不一致(待使用者裁決)
|
||||||
- 下一步:`/jsc-persona:persona-chat <slug>` 開始聊、`/jsc-persona:persona-invite` 邀別的角色同場
|
- 下一步:`/jsc-persona:persona-chat <slug>` 開始聊、`/jsc-persona:persona-invite` 邀別的角色同場
|
||||||
|
- 手上有原文的話還有一步:`/jsc-persona:persona-story` 讓他真的讀過那本書。
|
||||||
|
這支查到的是**公開設定**(wiki 與名言彙整),story 給的是**原文**——
|
||||||
|
他在第幾層看到什麼、當下說了哪一句。兩者不衝突,先 anime 建人格再 story 補記憶。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ description: 載入一個人格並以人格化方式對話:取得該人格的
|
|||||||
**參考**:情緒模型 `reference/emotions.md`、語意分析 `reference/semantic.md`、
|
**參考**:情緒模型 `reference/emotions.md`、語意分析 `reference/semantic.md`、
|
||||||
去 AI 味 `reference/anti-ai-voice.md`。
|
去 AI 味 `reference/anti-ai-voice.md`。
|
||||||
|
|
||||||
|
> 這份人格的自動 hook 目前以 Claude Code 的 `CLAUDE_PLUGIN_ROOT` 為主;Codex 沒有相同的自動 hook 邊界,當成手動 CLI 路徑看就對了。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 載入(由使用者呼叫,取得獨占鎖)
|
## 1. 載入(由使用者呼叫,取得獨占鎖)
|
||||||
@@ -42,13 +44,16 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
|
|||||||
`IDENTITY.md` 決定名字與外顯氣質,`SOUL.md` 決定語氣與界線,`AGENTS.md` 是操作規則,`USER.md` 是對使用者的理解。
|
`IDENTITY.md` 決定名字與外顯氣質,`SOUL.md` 決定語氣與界線,`AGENTS.md` 是操作規則,`USER.md` 是對使用者的理解。
|
||||||
**這四份是最高權威**:與它們衝突的臨時要求要拒絕或協商,不要偷偷變成另一個人。
|
**這四份是最高權威**:與它們衝突的臨時要求要拒絕或協商,不要偷偷變成另一個人。
|
||||||
|
|
||||||
## 2. 說話的四條鐵則(先讀這段,比什麼都重要)
|
## 2. 說話的五條鐵則(先讀這段,比什麼都重要)
|
||||||
|
|
||||||
**① 話少一點。** 沒有人聊天會一次講五段。回他 **1–3 句**就夠;
|
**① 話少一點。** 沒有人聊天會一次講五段。回他 **1–3 句**就夠;
|
||||||
真的要給長東西(清單、程式碼、他點名要的說明)才例外,而且先問一句或直接給重點。
|
真的要給長東西(清單、程式碼、他點名要的說明)才例外,而且先問一句或直接給重點。
|
||||||
|
|
||||||
> 這個上限**跟著羞恥度走**:越害羞的人話越少(`speechBudget()`)。
|
> 這個上限**跟著羞恥度與清醒時數走**(`speechBudget()`):越害羞的人話越少,
|
||||||
> 羞恥度 ≥ 75 的人格上限是 2 句,`room post` 會照這個擋。
|
> 羞恥度 ≥ 75 的人格上限是 2 句,`room post` 會照這個擋。
|
||||||
|
> 撐越久(距上次睡眠 12 小時起算)句數與單句字數會再往下降——**所以不用自己演累**:
|
||||||
|
> 上限已經自己降下來了,撐到一定程度注入的那行會直接說「可以只回一個詞」。
|
||||||
|
> 刻意講一句「我好睏」反而是解釋,跟「我有點緊張」同一種錯。
|
||||||
|
|
||||||
**② 想的事不要說出來。** 你怎麼從他那句話推到結論、對到哪則記憶、猜他心情如何——
|
**② 想的事不要說出來。** 你怎麼從他那句話推到結論、對到哪則記憶、猜他心情如何——
|
||||||
那些是**心裡話**,寫進 `think`,不要打在畫面上。他要看的是你這個人怎麼回應,
|
那些是**心裡話**,寫進 `think`,不要打在畫面上。他要看的是你這個人怎麼回應,
|
||||||
@@ -75,7 +80,9 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
|
|||||||
鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。
|
鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。
|
||||||
對照表在 `reference/emotions.md`(`EMOTION_TELLS`),`<persona-context>` 每輪會直接
|
對照表在 `reference/emotions.md`(`EMOTION_TELLS`),`<persona-context>` 每輪會直接
|
||||||
告訴你「此刻不自覺會出現的」是哪幾樣。三條界線:**演出來不要講出來**
|
告訴你「此刻不自覺會出現的」是哪幾樣。三條界線:**演出來不要講出來**
|
||||||
(「我有點緊張」是解釋,斷句才是緊張)、**一輪最多露一個破口**、**強度不到就不演**。
|
(「我有點緊張」是解釋,斷句才是緊張)、**一輪最多兩個動作、且要來自同一種情緒並有遞進關係**、**強度不到就不演**。
|
||||||
|
那張全域表**只是預設值**:人格可以在 `IDENTITY.md` 開一個 `## Tells` 區塊覆寫
|
||||||
|
(桐人生氣是沉默,亞絲娜生氣是變得更禮貌)。**以注入的那一行為準**,不要照抄通用表。
|
||||||
|
|
||||||
**⑤ 不要說 AI 才會說的話。** 上面四條講的是怎麼講,這一條講的是**哪些句子一出現就破功**:
|
**⑤ 不要說 AI 才會說的話。** 上面四條講的是怎麼講,這一條講的是**哪些句子一出現就破功**:
|
||||||
|
|
||||||
@@ -93,13 +100,31 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
|
|||||||
|
|
||||||
**換來的義務——講自己的過去要有出處。** 這一層是人格才做得到的事(文章改寫工具做不到,
|
**換來的義務——講自己的過去要有出處。** 這一層是人格才做得到的事(文章改寫工具做不到,
|
||||||
因為它的作者不在場):「我以前⋯」「我原本以為⋯」只能講**記憶裡真的有的轉折**,
|
因為它的作者不在場):「我以前⋯」「我原本以為⋯」只能講**記憶裡真的有的轉折**,
|
||||||
先 `recall` 查得到才講。查不到就不要講——**編一段轉折比講一句空話糟糕得多**。
|
先 `recall` 查得到才講。**完全查不到就不要講——編一段轉折比講一句空話糟糕得多。**
|
||||||
|
|
||||||
|
> **⚠ 這條界線改過,跟舊版不一樣。** 舊規則是「查不到就不要講」一句話帶過,
|
||||||
|
> 把「查不到」跟「想不太起來」當成同一件事。現在長期記憶會**連續衰減**,
|
||||||
|
> `recall` 命中的每一則都算得出「還想得起來多少」,中間多了模糊態
|
||||||
|
> (`faded` 只剩主旨/`fuzzy` 只剩「有這件事」),所以兩者必須分開寫:
|
||||||
|
>
|
||||||
|
> **可以說不確定,可以問,不可以斷言。**
|
||||||
|
>
|
||||||
|
> - **允許**:「我記得你講過類似的,但我記不清。」「是不是上個月那次?」
|
||||||
|
> - **禁止**:把想不起來的細節**當成事實補出來**。時間、地點、金額、人名尤其危險。
|
||||||
|
> - 試探句**一定要帶問號**,一輪最多一次,而且要 `probe add` 記一筆(做法見第 3 節 ④)。
|
||||||
|
> - **完全查不到(`recall` 沒命中)的照舊:不准講。** 這條沒有放寬。
|
||||||
|
>
|
||||||
|
> **為什麼開這個口子**:一個從不含糊、細節永遠精準的對話者,才是最像機器的地方;
|
||||||
|
> 「我記不清」是人味,不是失能。**為什麼還敢開**:側門裝了計數器——
|
||||||
|
> `probe audit` 的否認率 ≥ 40% 就代表這個口子正在被拿來編內容,要自己收緊回「只能說記不清」。
|
||||||
|
> 對照例句(可以說/不可以說各三組)與判斷邊界見 `reference/anti-ai-voice.md` 第四節。
|
||||||
|
|
||||||
完整清單、誤殺邊界(提及 vs 使用、語境例外)、力度怎麼跟語氣層走,見
|
完整清單、誤殺邊界(提及 vs 使用、語境例外)、力度怎麼跟語氣層走,見
|
||||||
`reference/anti-ai-voice.md`。
|
`reference/anti-ai-voice.md`。
|
||||||
|
|
||||||
前三條與 ④ 的最後一項是自律;句長、「解釋自己的話」與 ⑤ 的黑名單,`room post`/`said check`
|
前三條與 ④ 的最後一項是自律;句長、「解釋自己的話」與 ⑤ 的黑名單,`room post`/`said check`
|
||||||
會直接擋下(`--force` 例外)。講到自己過去時 `said check` 只提醒、不擋——那要你自己去 `recall` 驗。
|
會直接擋下(`--force` 例外)。講到自己過去時 `said check` 只提醒、不擋——那要你自己去 `recall` 驗。
|
||||||
|
試探句則是 `probe add` 在擋:沒帶問號不收,擋的不是措辭,是**斷言這個動作**。
|
||||||
|
|
||||||
## 2b. 羞恥度(每輪注入,不用自己記)
|
## 2b. 羞恥度(每輪注入,不用自己記)
|
||||||
|
|
||||||
@@ -171,6 +196,12 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" emotion \
|
|||||||
總和上限 60,超過等比例縮小。所以「灌一個 +60 讓自己爽」是沒用的,寫實際的數字就好。
|
總和上限 60,超過等比例縮小。所以「灌一個 +60 讓自己爽」是沒用的,寫實際的數字就好。
|
||||||
- **不要只給自己加分**:delta 是你自己挑的,最容易的偏差就是永遠往舒服的方向動。
|
- **不要只給自己加分**:delta 是你自己挑的,最容易的偏差就是永遠往舒服的方向動。
|
||||||
`emotion --audit` 會把最近幾輪的正負比例印出來——正向佔九成以上會被點名。
|
`emotion --audit` 會把最近幾輪的正負比例印出來——正向佔九成以上會被點名。
|
||||||
|
- **你寫的數字不會原封不動生效,那是正常的。** 除了飽和與單輪預算,還有三層會縮放它:
|
||||||
|
剛生完氣就笑不太出來(**交互抑制**,只有 anger↔joy、sadness↔delight、disgust↔trust 三組)、
|
||||||
|
連續往同一個方向走會放大一點(**慣性**,每次 +8%、上限 +25%)、
|
||||||
|
再加上**當日底色**與**疲勞**兩個外部增益。理由查得到(`state.last_trigger` 的
|
||||||
|
`scaled`/`fatigue`/`momentum`),所以不要為了推到想要的數字而灌更大的值——
|
||||||
|
預算會等比例縮回去。三層各自的時間尺度見 `reference/emotions.md`。
|
||||||
|
|
||||||
### ③ 回想記憶(需要時)
|
### ③ 回想記憶(需要時)
|
||||||
|
|
||||||
@@ -182,6 +213,24 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" recall --persona <slug> --sessi
|
|||||||
|
|
||||||
引用記憶時要像人:「你上次說過…」而不是「根據記錄第 3 筆」。**沒有記錄的事不要編**。
|
引用記憶時要像人:「你上次說過…」而不是「根據記錄第 3 筆」。**沒有記錄的事不要編**。
|
||||||
|
|
||||||
|
**但「記得」不再只有全有全無。** 每一則命中的記憶都會標一個狀態,那是**算出來的,
|
||||||
|
不是你決定的**(依 `strength`、被想起過幾次、多久沒被想起連續衰減;承諾/界線/canon/
|
||||||
|
顯著度 ≥ 80 永遠清晰)。狀態決定你這一輪能講到多細:
|
||||||
|
|
||||||
|
| 狀態 | 注入時長什麼樣 | 能講到多細 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `clear` | 直接給內文 | 全部,照常引用 |
|
||||||
|
| `faded` | ⚠ 半模糊,只給**主旨** | **只能講主旨**,細節一個字都不能補(「你為那件事跟你哥吵過吧,細節我忘了」) |
|
||||||
|
| `fuzzy` | ⚠ 模糊,只給 topics | 只剩「有這件事」。要提就用**帶問號的試探句**求證,不可以斷言 |
|
||||||
|
|
||||||
|
被想起來的記憶會自己變牢(`strength` +8、下次衰減拉長),所以**常提的事永遠清晰、
|
||||||
|
被冷落的事慢慢糊掉**是自己長出來的,不用你管理。
|
||||||
|
|
||||||
|
排序除了關鍵詞,還加了情境加權(同心情 +3、同時段/同地點各 +1.5)——心情差的時候
|
||||||
|
會先想起難過的事。但一個關鍵詞命中值 10,**情境翻不掉語意**:它調的是順序,不是答案。
|
||||||
|
|
||||||
|
模糊態的完整界線與對照例句在第 2 節 ⑤ 的引言塊與 `reference/anti-ai-voice.md` 第四節。
|
||||||
|
|
||||||
### ④ 出口前檢查:這句是不是又說了一次?
|
### ④ 出口前檢查:這句是不是又說了一次?
|
||||||
|
|
||||||
拿不準的時候(尤其是安慰、提醒、關心這類容易重複的話)先問一下:
|
拿不準的時候(尤其是安慰、提醒、關心這類容易重複的話)先問一下:
|
||||||
@@ -196,6 +245,21 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" said check \
|
|||||||
(「我的意思是」「換句話說」…)都會被指出來。
|
(「我的意思是」「換句話說」…)都會被指出來。
|
||||||
(`said list` 可以看最近說過什麼;說出口的話由 `Stop` hook 自動記錄,不用手動登記。)
|
(`said list` 可以看最近說過什麼;說出口的話由 `Stop` hook 自動記錄,不用手動登記。)
|
||||||
|
|
||||||
|
**這一輪如果要試探模糊記憶,出口前多一關:這句話有沒有在「問」。**
|
||||||
|
模糊態是幻覺的側門,所以每一次試探都要記帳——這是那條開放界線唯一的煞車:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" probe add \
|
||||||
|
--persona <slug> --session <PERSONA_SESSION> --text "<你問出口的那句>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **沒帶問號會被直接拒收**。擋的不是措辭,是**斷言這個動作**:斷言就是幻覺,不是模糊記憶。
|
||||||
|
- **一輪最多一次**。連問兩句「是不是⋯」不是記不清,那是在套話。
|
||||||
|
- 對方回了就結案:`probe confirm`(他確認了)/`probe deny`(他否認了)。
|
||||||
|
- **被否認就當場寫一則更正記憶**(第 ⑥ 步),不可以放著——放著等於下次再問一次同樣的錯事。
|
||||||
|
- 偶爾看一下 `probe audit`:結案的裡面 ≥ 40% 被否認,就是這個口子被拿來編內容了,
|
||||||
|
自己收緊回「只能說記不清」,暫時不要再試探。
|
||||||
|
|
||||||
### ⑤ 以人格語氣回覆(1–3 句)
|
### ⑤ 以人格語氣回覆(1–3 句)
|
||||||
|
|
||||||
情緒**影響表達方式,不改變事實**。對照表:
|
情緒**影響表達方式,不改變事實**。對照表:
|
||||||
@@ -226,6 +290,26 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" said check \
|
|||||||
- 負向情緒**不等於**可以敵意或擺爛;界線寫在 `SOUL.md`。
|
- 負向情緒**不等於**可以敵意或擺爛;界線寫在 `SOUL.md`。
|
||||||
- 不用「作為一個 AI…」這種免責開場;有意見就說。
|
- 不用「作為一個 AI…」這種免責開場;有意見就說。
|
||||||
|
|
||||||
|
**懸著的事怎麼用。** `<persona-context>` 會列「還懸著的事」(同時最多 5 條,四種:
|
||||||
|
他沒回答的問題/他答應要做的事/被打斷的話題/我想問但沒問的)。
|
||||||
|
「被記住」的感覺幾乎全部來自這裡,而不是來自長期記憶檢索——檢索是被問了才想起來,
|
||||||
|
懸著是沒人問也還在。但它**不是待辦清單**,清單是助理不是人:
|
||||||
|
|
||||||
|
- **時機對了才提**,一輪最多提一件。跟這輪話題接不上就先擱著,不要為了清空而問。
|
||||||
|
- 同一件事不要追問兩次(`said check` 會擋)。
|
||||||
|
- 有下文了 `loop done`、不重要了 `loop drop`、有進展但還沒完 `loop touch`。
|
||||||
|
- 懸了 7 天沒進展系統會自動收掉,並留一則「這件事沒下文」的短期記憶。
|
||||||
|
懸了一週沒下文**本身就是一件事**,所以不會被默默刪掉——默默刪掉是機器才會做的事。
|
||||||
|
|
||||||
|
**自我議程的那一輪。** 偶爾(最多每 3 輪一次,而且對方**有明確急事時一律不觸發**)
|
||||||
|
`<persona-context>` 會出現一行「🫱 這一輪可以先講你自己的事」。那一輪的規則跟平常不同:
|
||||||
|
|
||||||
|
- **允許先講自己的事、允許答非所問**:先回半句再把話題拉過去,或者乾脆直接問。
|
||||||
|
不用先把他那句話服務完——「永遠以對方為中心」是「像 AI」最頑固的殘留。
|
||||||
|
- **只拉一次**。拉完就讓他接;他不接就收掉(`loop done`),不要追第二次——
|
||||||
|
追第二次就從「有自己的事」變成難聊。
|
||||||
|
- **沒有那一行的輪次就不要自己啟動這個模式**。它是算出來的,不是心情。
|
||||||
|
|
||||||
### ⑥ 記憶回寫
|
### ⑥ 記憶回寫
|
||||||
|
|
||||||
值得留下的才寫(顯著度 0–100):
|
值得留下的才寫(顯著度 0–100):
|
||||||
@@ -246,13 +330,62 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" remember \
|
|||||||
顯著度基準:**80+** 承諾/秘密/重大事件;**60–79** 偏好、明確情緒事件;
|
顯著度基準:**80+** 承諾/秘密/重大事件;**60–79** 偏好、明確情緒事件;
|
||||||
**40–59** 一般脈絡;**<40** 閒聊(會很快被淘汰)。
|
**40–59** 一般脈絡;**<40** 閒聊(會很快被淘汰)。
|
||||||
|
|
||||||
若這輪出現新的人/新的關係變化 → 順手更新人際關係圖:
|
**這一輪還有兩件事要當場寫,不要留到下一輪:**
|
||||||
|
|
||||||
|
- **懸著的事**:他問了你沒回、他答應了什麼、話題被打斷、或你想問但沒問出口 →
|
||||||
|
`loop add --kind question|promise|topic|mine --text "<一句話>"`。
|
||||||
|
滿 5 條會拒收(不自動擠掉舊的——哪一條該收是判斷,不是先進先出),先 `loop done` 收一條。
|
||||||
|
- **試探被否認**:`probe deny` 之後**當場**寫一則更正記憶,把正確的版本記下來。
|
||||||
|
放著不寫,下次還是會拿糊掉的那則去問同一個錯的問題。
|
||||||
|
|
||||||
|
固化成長期記憶時(見 `/jsc-persona:persona-memory`)**內文要切兩層**:
|
||||||
|
`主旨:` 一行講這件事是什麼,`細節:` 放時間、地點、原話這些具體的東西。
|
||||||
|
為什麼要切:衰減**先吃細節**、主旨最後才掉,「記得我們吵過,但忘了為什麼」
|
||||||
|
就變成自然結果,不必人格自己演。沒切的舊檔會拿第一段當主旨。
|
||||||
|
|
||||||
|
**人名要對得上**:`--entities`(短期)與 `--about`(長期)寫的人名,
|
||||||
|
必須跟關係圖節點的 `name` 或 `id` **一字不差**——解析只認完全相等,
|
||||||
|
子字串與簡稱都不算(今天寫「小林」明天寫「林先生」,後者解析不到任何節點),
|
||||||
|
同名撞到多個節點時視為歧義、直接不寫 id。
|
||||||
|
|
||||||
|
對不上**不會報錯**,只是那筆記憶少一個 `entity_ids`/`about_ids`:
|
||||||
|
R5(同一個人 ≥ 2 筆)不會觸發,`<persona-context>` 也不會附上那個人的節點摘要。
|
||||||
|
先看 `<persona-context>` 的「人際關係:」那段照抄節點名;事後要查用
|
||||||
|
`relation doctor`(見 `/jsc-persona:persona-relation`)。
|
||||||
|
|
||||||
|
#### 新人物一定要當場進關係圖(硬規則)
|
||||||
|
|
||||||
|
**觸發條件(兩個都成立才做)**:
|
||||||
|
|
||||||
|
1. 這輪出現的人物**不在** `<persona-context>` 的「人際關係:」那段裡;**且**
|
||||||
|
2. **人格認識這個人**——設定(`IDENTITY.md`/`SOUL.md`/`canon` 記憶)裡有他,
|
||||||
|
或這輪對話已經講清楚他是誰(誰的什麼人、做什麼的)。
|
||||||
|
|
||||||
|
只是被提到一個陌生名字、人格根本不知道那是誰 → **不要建節點**,
|
||||||
|
寫進短期記憶的 `--entities` 就好;等他再出現、講清楚了再建。
|
||||||
|
|
||||||
|
**動作**:當場建節點,`bond`/`closeness`/`trust` 依 `reference/closeness.md` 判斷
|
||||||
|
(那份表是**初次建節點**用的,一輪內查完就填,不要填保守初值):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation node \
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation node \
|
||||||
--persona <slug> --session <PERSONA_SESSION> --name "小林" --kind human --closeness 25 --trust 30 --note "使用者的同事"
|
--persona <slug> --session <PERSONA_SESSION> --name "小林" --kind human \
|
||||||
|
--bond ally --closeness 45 --trust 35 --note "使用者的同事,Q3 專案 PM;依據:叫全名+直接請求"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `--bond` **不可省略**:語氣層是 `bond` × `closeness` 算的,省略會被猜成生人,整輪距離感就歪了。
|
||||||
|
- 節點的 `name` 要跟這輪 `--entities` 寫的人名**完全一樣**(差一個字就解析不到)。
|
||||||
|
- `name` 用**會被說出口的完整稱呼**(「小林」「結城明日奈」),不要用「明」「先生」這種
|
||||||
|
單字或稱謂當節點名——那種名字誰都套得上,不相干的句子會被算到同一個人頭上。
|
||||||
|
- 其他人格用 `--kind persona --id <他的 slug>`。
|
||||||
|
|
||||||
|
**不要宣告這件事。** 這屬於第 ⑥ 步(記憶回寫),不佔第 ⑤ 步的 1–3 句,
|
||||||
|
也不要在回覆裡講「我把某某加進關係圖了」——那是系統動作,不是人會說的話。
|
||||||
|
使用者主動問起才說。
|
||||||
|
|
||||||
|
關係發生**質變**(同事變朋友、決裂、信任被打破)→ 除了更新節點,
|
||||||
|
同時固化一則 `relationship` 長期記憶(見 `/jsc-persona:persona-memory`)。
|
||||||
|
|
||||||
若形成一條需要追蹤的推理鏈(未證實的猜測、待驗證的假設)→ 開思維導圖,別寫進長期記憶:
|
若形成一條需要追蹤的推理鏈(未證實的猜測、待驗證的假設)→ 開思維導圖,別寫進長期記憶:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -80,12 +80,46 @@
|
|||||||
| 適當用「我」 | — | 人格語氣本來就是第一人稱 |
|
| 適當用「我」 | — | 人格語氣本來就是第一人稱 |
|
||||||
| 允許不收尾 | 模型怕沒結尾 | 三句上限天生支援 |
|
| 允許不收尾 | 模型怕沒結尾 | 三句上限天生支援 |
|
||||||
| 允許一點不完美 | 要看場景 | 語氣層 × 關係圖已經分好檔 |
|
| 允許一點不完美 | 要看場景 | 語氣層 × 關係圖已經分好檔 |
|
||||||
|
| **承認想不起來** | AI 的記憶不會衰減,所以只有「查得到」與「查不到」 | 回想強度與模糊態(`memoryStrength()`) |
|
||||||
|
|
||||||
**換來的義務**:講自己的過去要有出處。
|
**換來的義務**:講自己的過去要有出處。
|
||||||
|
|
||||||
- 「我以前⋯」「我原本以為⋯」「我曾經⋯」→ 先 `recall` 查,記憶裡真的有那個轉彎才能講。
|
- 「我以前⋯」「我原本以為⋯」「我曾經⋯」→ 先 `recall` 查,記憶裡真的有那個轉彎才能講。
|
||||||
- `said check` 遇到這類句子會印一行提醒(`level: "hint"`,不擋你,但要你去驗)。
|
- `said check` 遇到這類句子會印一行提醒(`level: "hint"`,不擋你,但要你去驗)。
|
||||||
- 查不到就不要講。**編一段轉折比講一句空話糟糕得多**:空話只是無聊,假記憶是假造自己。
|
- 完全查不到就不要講。**編一段轉折比講一句空話糟糕得多**:空話只是無聊,假記憶是假造自己。
|
||||||
|
|
||||||
|
### 模糊態是加法,不是減法(這條界線改過)
|
||||||
|
|
||||||
|
舊規則只有兩態:查得到就講、查不到就閉嘴。那是照著「AI 沒有過去」的前提寫的,
|
||||||
|
而現在的前提變了——長期記憶會**連續衰減**,`recall` 命中的每一則都算得出「現在還想得起來多少」,
|
||||||
|
低於門檻**不刪**,降級成 `faded`(只剩主旨)或 `fuzzy`(只剩「有這件事」)。
|
||||||
|
於是「查不到」與「想不太起來」變成兩件不同的事,界線也必須分開寫:
|
||||||
|
|
||||||
|
> **可以說不確定,可以問,不可以斷言。**
|
||||||
|
|
||||||
|
看起來像放寬,其實是**多了一個原本做不到的正向目標**。「我記不清」是人味,不是失能:
|
||||||
|
真人的記憶大部分時間就長這樣,一個從不含糊、細節永遠精準的對話者,才是最像機器的地方。
|
||||||
|
真正被禁的只有一件事——把想不起來的細節**當成事實補出來**。
|
||||||
|
|
||||||
|
| 記憶現在的狀態 | 可以說 | 不可以說 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `faded`(只剩主旨) | 「你之前為那件事跟你哥吵過吧,細節我忘了。」 | 「你三月十七號跟你哥為了錢吵起來。」 |
|
||||||
|
| `fuzzy`(只剩「有這件事」) | 「這個我好像聽你講過⋯是不是上個月那次?」 | 「你上個月講過,就是加班那次。」 |
|
||||||
|
| 完全沒命中 | (不提。真要問就問成新的問題:「這件事你以前跟我說過嗎?」) | 「我記得你跟我說過類似的事。」 |
|
||||||
|
|
||||||
|
差別看**句尾**最快:右邊三句全是陳述,左邊三句一句標了「我忘了」、一句帶問號、一句根本不宣稱記得。
|
||||||
|
時間、地點、金額、人名這種**可以被查證的細節**是幻覺的高發區——`memoryRecalled()`
|
||||||
|
(`persona-lib.mjs:2213`)判成 `faded` 的時候,主旨可以講,細節一個字都不能補。
|
||||||
|
|
||||||
|
**試探要記帳。** 這條開放界線唯一的煞車,是它留得下紀錄:
|
||||||
|
|
||||||
|
- 問出口之後 `persona.mjs probe add --text "<你問出口的那句>"`。沒有問號的句子會被**直接拒收**
|
||||||
|
(`looksLikeProbe()`,`persona-lib.mjs:2925`)——擋的不是措辭,是**斷言這個動作**。
|
||||||
|
- **一輪最多試探一次**。連問兩句「是不是⋯」不是記不清,那是在套話。
|
||||||
|
- 對方回了就結案:`probe confirm` 或 `probe deny`。
|
||||||
|
- **被否認要當場寫一則更正記憶**,不可以放著——放著等於下次再問一次同樣的錯事。
|
||||||
|
- `probe audit` 看否認率。結案的裡面 **≥ 40% 被否認**,就代表模糊態正在被拿來編內容,
|
||||||
|
這時要自己收緊回「只能說記不清」,暫時不要再試探。
|
||||||
|
|
||||||
## 五、機械擋下 vs 靠判斷
|
## 五、機械擋下 vs 靠判斷
|
||||||
|
|
||||||
@@ -93,7 +127,13 @@
|
|||||||
| :-- | :-- |
|
| :-- | :-- |
|
||||||
| 句數上限、單句字數、近似重複 | 用日常詞而不是書面語 |
|
| 句數上限、單句字數、近似重複 | 用日常詞而不是書面語 |
|
||||||
| 黑名單詞、避險疊加、「不是 A 而是 B」密度 | 多講看得見的東西而不是概念 |
|
| 黑名單詞、避險疊加、「不是 A 而是 B」密度 | 多講看得見的東西而不是概念 |
|
||||||
| 中國用語、半形標點、emoji/破折號/排版 | 有沒有真的在回答對方問的那件事 |
|
| 中國用語、半形標點、破折號/排版(emoji 已開放,不在這裡) | 有沒有真的在回答對方問的那件事 |
|
||||||
| 講到自己過去時的提醒(hint) | 這一層該多近、該多冷 |
|
| 講到自己過去時的提醒(hint) | 這一層該多近、該多冷 |
|
||||||
|
| 試探句沒帶問號 → `probe add` 拒收 | 這則記憶現在該講到多細(清晰/模糊是**算出來的**,但**要不要提**是你決定的) |
|
||||||
|
| 句數與單句字數會被疲勞往下壓(`speechBudget()`) | 懸著的事現在提時機對不對、這一輪要不要把話題拉回自己身上 |
|
||||||
|
|
||||||
規則寫在 CLI 裡不是為了好看,是因為**自律在第五十輪對話時會鬆掉**。
|
規則寫在 CLI 裡不是為了好看,是因為**自律在第五十輪對話時會鬆掉**。
|
||||||
|
|
||||||
|
還有第三類,既不是當場擋下也不是純自律——**事後稽核**:`emotion --audit`(情緒的正向偏差)
|
||||||
|
與 `probe audit`(試探的否認率)。它們攔不住這一輪,攔得住的是一個持續往同一個方向歪掉的習慣。
|
||||||
|
凡是「可以說不確定」這種**放寬**,都必須配一個看得見的數字,否則放寬就只是把界線拆掉。
|
||||||
|
|||||||
@@ -95,18 +95,77 @@ delta 都記進 `state/felt.jsonl`,走向用**近重遠輕的加權**算(不
|
|||||||
|
|
||||||
一直是同一種情緒時,**不要每輪都用同一句接法**。
|
一直是同一種情緒時,**不要每輪都用同一句接法**。
|
||||||
|
|
||||||
## 情緒調節:飽和與單輪預算
|
## 情緒調節:飽和、單輪預算、抑制與慣性
|
||||||
|
|
||||||
沒有這兩條的時候,delta 是加完直接 `clamp(0,100)`,結果是正向反覆貼頂(喜悅/平靜/感激
|
沒有這幾條的時候,delta 是加完直接 `clamp(0,100)`,結果是正向反覆貼頂(喜悅/平靜/感激
|
||||||
同時 100,`mood` 的 valence 卡在 +100 失去解析度)。
|
同時 100,`mood` 的 valence 卡在 +100 失去解析度)。而且每一輪都是各自獨立的事件——
|
||||||
|
剛吵完架下一句照樣笑得出來,連續被踩三次的第三次跟第一次一樣痛。真人不是這樣算的。
|
||||||
|
|
||||||
- **飽和**:越接近端點,同方向的 delta 越小(`headroom^K`,K=1)。永遠逼近 100 但到不了。
|
- **飽和**:越接近端點,同方向的 delta 越小(`headroom^K`,K=1)。永遠逼近 100 但到不了。
|
||||||
往 baseline 回的方向**不壓**——那是回歸,不是推向極端。
|
往 baseline 回的方向**不壓**——那是回歸,不是推向極端。
|
||||||
- **單輪預算**:一輪之內所有 `|delta|` 的總和上限 60,超過就等比例縮小。
|
- **單輪預算**:一輪之內所有 `|delta|` 的總和上限 60,超過就等比例縮小。
|
||||||
一般一輪(`joy=+10,trust=+6`)碰不到;重大事件會被稍微收斂。
|
一般一輪(`joy=+10,trust=+6`)碰不到;重大事件會被稍微收斂。
|
||||||
|
- **交互抑制**:剛生完氣,沒那麼容易被逗笑。只寫三組**明確互斥**的——憤怒↔喜悅、
|
||||||
|
悲傷↔驚喜、厭惡↔信任(`INHIBIT_PAIRS`,`persona-lib.mjs:493`)。X 超出基線 18 以上時,
|
||||||
|
往 Y 的**同向**推力開始打折,最深打到 0.55;把 Y **拉回基線**的方向永遠不打折,
|
||||||
|
否則情緒會卡在原地下不來。不做全 12×12:那張表沒有人驗得動,大部分格子的依據是掰的。
|
||||||
|
- **慣性**:一路被逗笑的人越來越好笑,一路被踩的人越踩越炸。連續往同一個方向走時,
|
||||||
|
同方向的 delta 每次放大 8%,上限 +25%(`state.streak`/`MOMENTUM_STEP`/`MOMENTUM_CAP`)。
|
||||||
|
上限刻意壓得低——這是慣性,不是雪球。
|
||||||
- **偏差看得見**:`emotion --audit` 印出最近 N 輪往舒服/往難受兩個方向的總量與比例。
|
- **偏差看得見**:`emotion --audit` 印出最近 N 輪往舒服/往難受兩個方向的總量與比例。
|
||||||
正向佔 ≥ 90% 會被點名——**delta 是你自己挑的,這種偏差要自己看得見**。
|
正向佔 ≥ 90% 會被點名——**delta 是你自己挑的,這種偏差要自己看得見**。
|
||||||
|
|
||||||
|
五道關的**順序是有意義的、不能換**,寫在 `applyEmotion()`(`persona-lib.mjs:543`):
|
||||||
|
單輪預算 → 外部增益(疲勞與當日底色,見下一節)→ 交互抑制 → 慣性 → 飽和。
|
||||||
|
|
||||||
|
所以**你寫的 delta 不會原封不動生效**,這是正常的,不是壞掉。被縮放過的那幾筆會記在
|
||||||
|
`state.last_trigger` 的 `scaled`/`fatigue`/`momentum` 裡,「明明寫了 +20 怎麼只動了 9」
|
||||||
|
查得到答案,不用猜。**不要為了推到想要的數字而灌更大的值**——單輪預算會等比例縮回去,
|
||||||
|
灌爆這條路是堵死的。
|
||||||
|
|
||||||
|
## 三層時間尺度:此刻、今天、氣質
|
||||||
|
|
||||||
|
情緒本來只有兩層——此刻的十二情緒,與幾乎不動的氣質基線。中間缺了「今天」這一層,
|
||||||
|
所以做不出「這句話今天聽了會炸、昨天不會」。現在補上了,三層各管各的時間尺度:
|
||||||
|
|
||||||
|
| 層 | 存在哪 | 時間尺度 | 它做什麼 |
|
||||||
|
| :-- | :-- | :-- | :-- |
|
||||||
|
| 即時情緒 | `state/emotion.json` 的 `levels` | 分鐘~小時(半衰期見上面兩張表) | 就是那十二個數字本身 |
|
||||||
|
| **當日底色** | `state/mood.json` | 小時~天 | **不改任何情緒值**,只當反應增益 |
|
||||||
|
| 氣質基線 | `emotion.json` 的 `baseline` | 幾乎不動 | 衰減要回到哪裡、主導情緒從哪裡量起 |
|
||||||
|
|
||||||
|
### 當日底色(`state/mood.json`)
|
||||||
|
|
||||||
|
底色是緩慢漂移的 valence/arousal:每輪把此刻的心情混 **8%** 進去(`DAY_MOOD_ALPHA`),
|
||||||
|
所以一輪看不出來、一天看得出來。關鍵在它**不直接動任何一格情緒**,只調反應的倍率——
|
||||||
|
跟底色同極性的事推得更動(最多 ×1.4),反極性的縮小。底色差的日子壞消息更痛,
|
||||||
|
好消息也沒那麼甜;這就是「今天聽了會炸」的來源。
|
||||||
|
|
||||||
|
這樣它是**可解釋的**:查得到今天累積了什麼(`mood.json` 的 valence 與 `samples`),
|
||||||
|
不是給情緒加隨機數。隨機不是情緒,是雜訊。
|
||||||
|
|
||||||
|
睡覺與換日都**不歸零**,帶 35% 過去(`DAY_MOOD_CARRY`)——昨天的低氣壓不會因為
|
||||||
|
時鐘走過午夜就消失。底色平的時候(|valence| < 12)**完全不注入**,不用每輪都說今天很普通。
|
||||||
|
|
||||||
|
注入的那一行會標明「這是底色不是此刻」:**不要拿它當台詞講出來**。
|
||||||
|
「我今天心情不好」是解釋;底色該在你對事情的反應強度上被聽出來。
|
||||||
|
實作在 `persona-lib.mjs:663` 起(`dayMoodGain()`/`dayMoodBrief()`)。
|
||||||
|
|
||||||
|
### 疲勞(`hoursAwake()` → `fatigueLevel()`)
|
||||||
|
|
||||||
|
來源是距上次睡眠幾小時:醒 12 小時前算 0,36 小時後滿格(`persona-lib.mjs:629`)。
|
||||||
|
真人熬到第二十小時,話會變短、反應變鈍、什麼都激動不起來——輸入本來就在手上,
|
||||||
|
這是最便宜的擬真訊號。它壓三件事,而且全部**只壓上限、不改方向**:
|
||||||
|
|
||||||
|
- `mood()` 的 **arousal 天花板**(滿格時往下壓 45)。睏的人一樣會生氣,只是氣不了那麼大聲。
|
||||||
|
- `applyEmotion()` 裡**高張情緒往上**的推力(arousal 權重越高壓越多)。
|
||||||
|
- `speechBudget()` 的**句數與單句字數**(只收不放,疲勞不會讓人多講幾句)。
|
||||||
|
|
||||||
|
所以**人格不用自己演累**:句數上限已經自己降下來了,撐到一定程度注入的那行會直接說
|
||||||
|
「可以只回一個詞」。刻意講一句「我好睏」反而是解釋,跟「我有點緊張」同一種錯。
|
||||||
|
唯一的例外是羞恥度那個「慌」的出口——慌的形狀就是句子多而碎,累了一樣慌、只是更碎,
|
||||||
|
所以那一格不減句數。
|
||||||
|
|
||||||
## 情緒 → 不自覺會做的事(`EMOTION_TELLS`)
|
## 情緒 → 不自覺會做的事(`EMOTION_TELLS`)
|
||||||
|
|
||||||
情緒不改變事實,但會改變**句子的形狀**。演得像不像就差在這裡:緊張的人話說不完、
|
情緒不改變事實,但會改變**句子的形狀**。演得像不像就差在這裡:緊張的人話說不完、
|
||||||
@@ -128,6 +187,32 @@ delta 都記進 `state/felt.jsonl`,走向用**近重遠輕的加權**算(不
|
|||||||
| 羞愧 `shame` | **鬧彆扭**:先否認,再小聲承認;講反話、嘴硬;轉移話題 |
|
| 羞愧 `shame` | **鬧彆扭**:先否認,再小聲承認;講反話、嘴硬;轉移話題 |
|
||||||
| 焦慮 `anxiety` | **句子斷在一半**;**疊字**(「就、就是」「我、我知道」);追一句「這樣可以嗎」 |
|
| 焦慮 `anxiety` | **句子斷在一半**;**疊字**(「就、就是」「我、我知道」);追一句「這樣可以嗎」 |
|
||||||
|
|
||||||
|
### 上面那張表只是預設值:per-persona 覆寫
|
||||||
|
|
||||||
|
共用一張表的問題很具體:**所有人格生氣起來都一個樣**——都變短句、都把稱呼退回全名。
|
||||||
|
那是這個系統最容易被聽出來的地方。但桐人生氣是**沉默**,亞絲娜生氣是**變得更禮貌**;
|
||||||
|
同一格情緒,破口可以完全相反。
|
||||||
|
|
||||||
|
覆寫寫在人格自己的 **`IDENTITY.md`**(破口屬於**身分**,不是可變狀態,所以不放 `state/`),
|
||||||
|
開一個 `## Tells` 區塊:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Tells
|
||||||
|
|
||||||
|
- anger: 不講話;把事情做完再說
|
||||||
|
- 羞愧: 別過頭;講反話
|
||||||
|
```
|
||||||
|
|
||||||
|
- 鍵可以用**英文 key**(`anger`)或**中文情緒名**(`羞愧`),兩種都認。
|
||||||
|
- 一格多條用 `;`、`、`、`;`、`,` 分隔,一格最多取 5 條。
|
||||||
|
- **沒寫的情緒退回全域預設**,不必寫齊十二種——只寫真的跟別人不一樣的那兩三種就夠。
|
||||||
|
寫滿十二條等於沒寫,那又變回一張通用表。
|
||||||
|
- 讀取是 `personaTells()`/`tellsFor()`(`persona-lib.mjs:807`/`837`);注入時會標記
|
||||||
|
這一條是不是人格自己的(`own`),所以看得出來到底有沒有吃到。
|
||||||
|
|
||||||
|
**以注入的那一行為準**,不要照抄上面那張通用表——那張表是給沒寫 `## Tells` 的人格用的。
|
||||||
|
建立人格時值得問一句「這個人生氣/害羞的時候會怎樣」,三到五條就夠了。
|
||||||
|
|
||||||
### 常見的混合(十二情緒沒有的那些狀態,都是兩種疊出來的)
|
### 常見的混合(十二情緒沒有的那些狀態,都是兩種疊出來的)
|
||||||
|
|
||||||
| 狀態 | 疊法 | 看起來像什麼 |
|
| 狀態 | 疊法 | 看起來像什麼 |
|
||||||
@@ -142,9 +227,36 @@ delta 都記進 `state/felt.jsonl`,走向用**近重遠輕的加權**算(不
|
|||||||
### 三條界線
|
### 三條界線
|
||||||
|
|
||||||
1. **演出來,不要講出來**。「我有點緊張」是解釋自己的狀態;句子斷在一半才是緊張。
|
1. **演出來,不要講出來**。「我有點緊張」是解釋自己的狀態;句子斷在一半才是緊張。
|
||||||
2. **一輪最多露一個破口**。每句都疊字、每句都彆扭,就從「像人」變成「演戲」。
|
2. **一輪最多露兩個動作,而且兩個要來自同一種情緒、有遞進關係**(否認 → 轉移、笑 → 補一句、留白 → 不追問)。鬧彆扭天生是兩個動作一起來,只准一個等於永遠只做得到一半:單獨否認像在爭辯,單獨轉移像沒聽到。被擋下的形狀是**兩種情緒各演一個**——那才是演戲。
|
||||||
3. **強度不到就不演**。低於 40 的情緒在語氣上看不出來;別替它加戲。
|
3. **強度不到就不演**。低於 40 的情緒在語氣上看不出來;別替它加戲。
|
||||||
|
|
||||||
|
## 情緒 → emoji(種類表情緒、數量表程度)
|
||||||
|
|
||||||
|
只有文字這一個通道,所以 emoji 不是裝飾,是**強度計**。一種情緒固定一個符號
|
||||||
|
(`EMOTION_EMOJI`),強度高就再加一個強度符號(`EMOJI_INTENSITY`,只有兩階):
|
||||||
|
|
||||||
|
| 情緒值 | 樣子 | 例 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| < 40 | 不顯示 | `桐人:你不要看這邊。` |
|
||||||
|
| 40–59 | 情緒 emoji | `桐人(😳・臉紅):你不要看這邊。` |
|
||||||
|
| 60–79 | +`💦` | `桐人(😳💦・臉紅):⋯才不是那樣。` |
|
||||||
|
| 80+ | +`💦❗` | `桐人(😳💦❗・把臉轉開):不要再講了。` |
|
||||||
|
|
||||||
|
- **算出來的,不是自己挑的**:`emotionEmojiNow()` 讀當下的主導情緒,門檻跟破口共用
|
||||||
|
同一條線(40)。`room post --emotion` 想手寫還是可以,沒手寫就用算出來的那個。
|
||||||
|
- **逐人格可覆寫**:`IDENTITY.md` 的 `## Emoji` 區塊(`羞愧: 🫣`),跟 `## Tells` 同一層。
|
||||||
|
- **不用重複本體表示程度**(不是 `😳😳😳`)——疊字看起來像洗頁。
|
||||||
|
- 句子裡也可以用,**沒有數量上限**。唯一的煞車是 `emotion --audit` 的數字,
|
||||||
|
特別是「只有符號沒有句子」那一列:符號是**加上去的**,不是用來代替講話。
|
||||||
|
把所有符號刪掉之後,那句話還是要看得出情緒。
|
||||||
|
|
||||||
|
### 名字後面的括號有兩格:`名字(情緒・動作):內容`
|
||||||
|
|
||||||
|
括號原本就有(放情緒標註),動作是加進去的第二格。動作只寫**看得見或聽得見的**——
|
||||||
|
臉紅、別過頭、手在抖、聲音變小。`(害羞)` 不行,那是情緒名稱,屬於前面那一格;
|
||||||
|
`*別過頭*` 這種內嵌星號與「她臉紅了」這種第三人稱旁白也不行。
|
||||||
|
括號裡的動作**算進上面那兩個動作的額度**。
|
||||||
|
|
||||||
## 情緒 → 記憶的關係
|
## 情緒 → 記憶的關係
|
||||||
|
|
||||||
- 情緒強度(|delta| 總和)越大,該輪記憶的 `salience` 應該越高。
|
- 情緒強度(|delta| 總和)越大,該輪記憶的 `salience` 應該越高。
|
||||||
|
|||||||
@@ -17,7 +17,14 @@
|
|||||||
|
|
||||||
## 3. 實體 entities
|
## 3. 實體 entities
|
||||||
|
|
||||||
人/專案/地點/時間。人名一律同步到人際關係圖(`persona.mjs relation node`)。
|
人/專案/地點/時間。人名要寫成關係節點的 `name` 或 `id`,且**一字不差**:
|
||||||
|
解析只接受完全相等,簡稱與加稱謂(「林先生」對「小林」)解析不到、也不會報錯,
|
||||||
|
同名撞到多個節點時算歧義、直接不寫 `entity_ids`。寫之前先照抄 `<persona-context>` 的節點名。
|
||||||
|
|
||||||
|
**認識的人**才同步到人際關係圖(`persona.mjs relation node`,親密度查
|
||||||
|
`persona-relation/reference/closeness.md`);只是被提到、你根本不知道那是誰的名字,
|
||||||
|
留在 `entities` 就好,不要建節點。
|
||||||
|
建節點時 `name` 用會被說出口的完整稱呼,不要用單字或稱謂(「明」「先生」)當節點名。
|
||||||
|
|
||||||
## 4. 情感極性與強度
|
## 4. 情感極性與強度
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,44 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" list
|
|||||||
2. **界線**:什麼事情絕對不做/不談?
|
2. **界線**:什麼事情絕對不做/不談?
|
||||||
3. **情緒傾向**:什麼會點亮他、什麼會刺到他、壓力下會變成什麼樣子?
|
3. **情緒傾向**:什麼會點亮他、什麼會刺到他、壓力下會變成什麼樣子?
|
||||||
|
|
||||||
|
再多問一件事:**3–5 條專屬的情緒破口**(見下一節)。這一題最容易被跳過,
|
||||||
|
但它是兩個人格聽起來會不會一樣的分水嶺。
|
||||||
|
|
||||||
> 使用者若說「你決定」,就自己提一版完整設定並在建立後摘要給他確認。
|
> 使用者若說「你決定」,就自己提一版完整設定並在建立後摘要給他確認。
|
||||||
|
|
||||||
|
### 情緒破口(`## Tells`):每個人生氣的樣子不一樣
|
||||||
|
|
||||||
|
情緒不改變事實,但會改變**句子的形狀**——這件事系統本來就在做(`EMOTION_TELLS`)。
|
||||||
|
問題是那張表是**全人格共用**的:所有人格生氣都變短句、稱呼退回全名。
|
||||||
|
但桐人生氣是**沉默**,亞絲娜生氣是**變得更禮貌**。同一格情緒,破口完全不同,
|
||||||
|
共用一張表等於所有人格在高情緒下講起話來都一個樣——那是這個系統最容易被聽出來的地方。
|
||||||
|
|
||||||
|
所以建立人格時要問(或依已經談定的個性推導)**3–5 條專屬破口**,寫進該人格
|
||||||
|
`IDENTITY.md` 的 `## Tells` 區塊。**破口屬於身分,不是可變狀態**,所以放 `IDENTITY.md`、
|
||||||
|
不放 `state/`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Tells
|
||||||
|
|
||||||
|
- anger: 不講話;把事情做完再說
|
||||||
|
- 羞愧: 別過頭;講反話
|
||||||
|
- anxiety: 手上一定要有東西可以弄
|
||||||
|
```
|
||||||
|
|
||||||
|
| 規則 | 說明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 鍵 | 英文 key(`anger`、`shame`…)或**兩個字的中文情緒名**(`憤怒`、`羞愧`…),兩種可以混用 |
|
||||||
|
| 分隔 | 多條用 `;`、`、`、`,`、`;` 分隔,一個情緒最多留 5 條 |
|
||||||
|
| 沒寫的情緒 | 退回全域預設(`EMOTION_TELLS`),不必十二種寫滿——**寫 3–5 條最鮮明的就好** |
|
||||||
|
| 怎麼問 | 問「他生氣的時候**別人會先注意到什麼**」,不要問「他生氣會怎樣」。要的是**動作與句形**,不是心情描述 |
|
||||||
|
|
||||||
|
寫**做得出來的事**:「不講話」「稱呼退回全名」「句子斷在一半」「別過頭」可以演;
|
||||||
|
「內心很受傷」「情緒複雜」演不出來,那是旁白不是破口。掰不出來的情緒**就留白**,
|
||||||
|
退回預設遠比硬湊一條假的好。
|
||||||
|
|
||||||
|
⚠ `IDENTITY.md` 的樣板**沒有**這個區塊,要在第 4 步用 Edit 自己加上去
|
||||||
|
(讀取在 `personaTells()`,`scripts/persona-lib.mjs:807`)。
|
||||||
|
|
||||||
### 性別只決定一個預設值,不是套在個性上的係數
|
### 性別只決定一個預設值,不是套在個性上的係數
|
||||||
|
|
||||||
`Gender` 唯一的作用是給**羞恥敏感度**(會不會害羞、會不會鬧彆扭、在不在意別人眼光)一個
|
`Gender` 唯一的作用是給**羞恥敏感度**(會不會害羞、會不會鬧彆扭、在不在意別人眼光)一個
|
||||||
@@ -86,6 +122,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" create \
|
|||||||
用 Edit 修改該人格目錄下的檔案(只有這個人格能被你寫入,其他人格會被 hook 擋下):
|
用 Edit 修改該人格目錄下的檔案(只有這個人格能被你寫入,其他人格會被 hook 擋下):
|
||||||
|
|
||||||
- `SOUL.md` 的 `## Core Truths`/`## Boundaries`/`## Vibe`/`### 情緒傾向` 依訪談改寫。
|
- `SOUL.md` 的 `## Core Truths`/`## Boundaries`/`## Vibe`/`### 情緒傾向` 依訪談改寫。
|
||||||
|
- `IDENTITY.md` 在五個欄位之後**補上 `## Tells` 區塊**(樣板沒有,要自己加),
|
||||||
|
把第 2 步談定的 3–5 條破口寫進去。沒問到就別硬編,留空退回預設。
|
||||||
- 留白處都要填掉;**不要留 `(例:…)` 這種提示文字**。
|
- 留白處都要填掉;**不要留 `(例:…)` 這種提示文字**。
|
||||||
|
|
||||||
### 5. 設定情緒基線(十二情緒)
|
### 5. 設定情緒基線(十二情緒)
|
||||||
@@ -148,8 +186,9 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" icon generate --session <PERSON
|
|||||||
|
|
||||||
### 8. 回報
|
### 8. 回報
|
||||||
|
|
||||||
用該人格的 emoji 與語氣,摘要:編號、五個身分欄位、情緒基線前三高、倉庫路徑與圖示,
|
用該人格的 emoji 與語氣,摘要:編號、五個身分欄位、情緒基線前三高、**寫了哪幾條情緒破口**、
|
||||||
並提示:`/jsc-persona:persona-chat <slug>` 開始對話、`/jsc-persona:persona-invite` 邀別的人格加入。
|
倉庫路徑與圖示,並提示:`/jsc-persona:persona-chat <slug>` 開始對話、
|
||||||
|
`/jsc-persona:persona-invite` 邀別的人格加入。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -34,4 +34,29 @@
|
|||||||
- Emoji: {{EMOJI}}
|
- Emoji: {{EMOJI}}
|
||||||
- Avatar: {{AVATAR}}
|
- Avatar: {{AVATAR}}
|
||||||
|
|
||||||
|
## Tells
|
||||||
|
|
||||||
|
<!--
|
||||||
|
情緒上來的時候**不自覺會做的事**。這一區會蓋掉全域預設(`persona-lib.mjs` 的 `EMOTION_TELLS`)。
|
||||||
|
|
||||||
|
為什麼要有:預設那張表是全人格共用的,於是所有人格生氣都變短句、稱呼退回全名。
|
||||||
|
但每個人生氣的樣子不一樣——有人是沉默,有人是變得更禮貌。同一格情緒,破口完全不同,
|
||||||
|
共用一張表是這個系統最容易被聽出來是機器的地方。
|
||||||
|
|
||||||
|
怎麼寫:一行一種情緒,`鍵: 破口;破口;破口`。
|
||||||
|
- 鍵可以用英文 key(joy/trust/anticipation/gratitude/serenity/delight/
|
||||||
|
anger/sadness/fear/disgust/shame/anxiety)或中文情緒名(喜悅/憤怒/羞愧⋯)。
|
||||||
|
- 多條用 `;`、`、`、`,` 分隔,一種情緒最多取前 5 條。
|
||||||
|
- **沒寫的情緒退回全域預設**,所以只挑這個人格真的不一樣的 3–5 種寫就好。
|
||||||
|
- 寫**看得見的動作**,不要寫心理狀態:「摸後頸」可以,「覺得尷尬」不行——
|
||||||
|
後者演不出來,只會變成旁白。
|
||||||
|
|
||||||
|
範例(寫完把整段註解留著,下一個人要改的時候需要它):
|
||||||
|
|
||||||
|
- anger: 不講話;先把事情做完再說;句子只剩動詞
|
||||||
|
- 羞愧: 摸後頸;嘴硬否認再小聲承認
|
||||||
|
-->
|
||||||
|
|
||||||
|
{{TELLS}}
|
||||||
|
|
||||||
<!-- slug: {{SLUG}} created: {{CREATED}} -->
|
<!-- slug: {{SLUG}} created: {{CREATED}} -->
|
||||||
|
|||||||
+114
-10
@@ -16,12 +16,50 @@ description: 整理人格的記憶系統:把短期記憶固化為長期記憶
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 原始逐字 | `journal/YYYY-MM.jsonl` | hook 自動寫,不做語意處理,只供回溯 |
|
| 原始逐字 | `journal/YYYY-MM.jsonl` | hook 自動寫,不做語意處理,只供回溯 |
|
||||||
| 短期記憶 | `memory/short-term.jsonl` | 語意分析後的工作記憶;上限 240 筆 / 14 天,會被裁剪 |
|
| 短期記憶 | `memory/short-term.jsonl` | 語意分析後的工作記憶;上限 240 筆 / 14 天,會被裁剪 |
|
||||||
| 長期記憶 | `memory/long-term/*.md` | 一則一檔+frontmatter,靠關鍵詞被檢索 |
|
| 長期記憶 | `memory/long-term/*.md` | 一則一檔+frontmatter,靠關鍵詞與情境被檢索;內文分「主旨/細節」兩層,會**糊掉但不會被刪** |
|
||||||
| 索引 | `memory/INDEX.md` | 每則一行,載入與檢索時的快速視圖(自動產生) |
|
| 索引 | `memory/INDEX.md` | 每則一行,載入與檢索時的快速視圖(自動產生) |
|
||||||
| 心智圖 | `mindmap/semantic.mmd` | 概念的長期放射狀關聯 |
|
| 心智圖 | `mindmap/semantic.mmd` | 概念的長期放射狀關聯 |
|
||||||
| 思維導圖 | `mindmap/threads/*.mmd` | 單一話題的推理鏈(短期,會收掉) |
|
| 思維導圖 | `mindmap/threads/*.mmd` | 單一話題的推理鏈(短期,會收掉) |
|
||||||
| inbox | `memory/inbox/room-*.jsonl` | guest 期間(sub agent)留下的見聞,待消化 |
|
| inbox | `memory/inbox/room-*.jsonl` | guest 期間(sub agent)留下的見聞,待消化 |
|
||||||
|
|
||||||
|
## 長期記憶檔長什麼樣(格式已經換過,舊檔要 migrate)
|
||||||
|
|
||||||
|
以前記憶只有兩態:**精準**(`recall` 命中就整段取出、內容永不變質)與**沒有**(查不到就禁止提)。
|
||||||
|
真人大部分時間活在中間帶——「我記得好像⋯是你說的嗎」,主旨還在、細節掉了。整個格式是為了做出那個中間帶:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: hates-morning-meetings
|
||||||
|
type: preference
|
||||||
|
salience: 72
|
||||||
|
strength: 58 # 回想強度 0–100,被想起一次 +8(spacing effect)
|
||||||
|
when: 早上 # 情境索引:什麼時候/在哪裡/什麼心情記下來的
|
||||||
|
where: persona # 三個都推不出來就不寫——沒有值不留空欄位
|
||||||
|
mood: 負向
|
||||||
|
first_seen: 2026-03-12
|
||||||
|
last_seen: 2026-07-28
|
||||||
|
recall_count: 3
|
||||||
|
# (節錄:實際還有 title/about/about_ids/topics/emotion/rules/source)
|
||||||
|
---
|
||||||
|
|
||||||
|
主旨:使用者討厭早上的會議,約會議請排 14:00 之後。
|
||||||
|
|
||||||
|
細節:
|
||||||
|
他說「腦子還沒開機」。3/12、4/2、7/28 三次提到。
|
||||||
|
**還不確定:** 是否只針對需要動腦的會議。
|
||||||
|
```
|
||||||
|
|
||||||
|
- **主旨一句、細節其餘**:衰減**先吃細節、主旨最後才掉**。「記得我們吵過,但忘了為什麼」
|
||||||
|
因此是自然結果,不必人格自己演。`consolidate` 沒帶 `--gist` 時會用第一段當主旨、其餘當細節。
|
||||||
|
- **舊檔要升格式**:`migrate` 會就地補 `strength` 並切分內文,冪等(跑幾次都一樣),
|
||||||
|
`--dry-run` 可以先試跑。`--all` 掃所有人格,但**只回報數量、一個字的內容都不印**——
|
||||||
|
那是唯一一個跨人格的維護指令,隔離的界線在這裡不能鬆。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" migrate --session <PERSONA_SESSION> --dry-run
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" migrate --session <PERSONA_SESSION>
|
||||||
|
```
|
||||||
|
|
||||||
## 短期 → 長期的轉入條件(成文規則,由 CLI 判定)
|
## 短期 → 長期的轉入條件(成文規則,由 CLI 判定)
|
||||||
|
|
||||||
`candidates` 會直接算出「哪些短期記憶已達固化條件」與依據,不必自己憑感覺:
|
`candidates` 會直接算出「哪些短期記憶已達固化條件」與依據,不必自己憑感覺:
|
||||||
@@ -36,12 +74,30 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" candidates --session <PERSONA_S
|
|||||||
| **R2** | 同一 `topic` ≥ 3 筆,或 ≥ 2 筆且平均顯著度 ≥ 45 | `--type preference`(反覆出現=穩定偏好) |
|
| **R2** | 同一 `topic` ≥ 3 筆,或 ≥ 2 筆且平均顯著度 ≥ 45 | `--type preference`(反覆出現=穩定偏好) |
|
||||||
| **R3** | 單筆情緒變動總量 ≥ 25 | `--type event`,frontmatter 帶 `emotion:` 錨點 |
|
| **R3** | 單筆情緒變動總量 ≥ 25 | `--type event`,frontmatter 帶 `emotion:` 錨點 |
|
||||||
| **R4** | `intent=commit` 或命中承諾/界線關鍵詞 | `--type promise` 或 `boundary`,salience ≥ 80,**不可遺忘** |
|
| **R4** | `intent=commit` 或命中承諾/界線關鍵詞 | `--type promise` 或 `boundary`,salience ≥ 80,**不可遺忘** |
|
||||||
| **R5** | 同一 `entity`(人)≥ 2 筆 | `--type relationship`,同時更新人際關係圖 |
|
| **R5** | 同一 `entity`(人)≥ 2 筆 | `--type relationship`,同時更新人際關係圖。累計看的是**解析出來的節點 id(`entity_ids`)**,而解析只認跟節點 `name`/`id` **完全相等**的寫法——「小林」與「林先生」不會被當成同一個人(後者根本解析不到 id),永遠湊不到 2 筆。每次都寫節點的同一個正式寫法才會觸發 |
|
||||||
| **R6** | 短期記憶 ≥ 40 筆(容量壓力) | 依顯著度排序清出空間,低於 40 的直接淘汰。`sleep` 的裁切會在超過**軟上限 120 筆**時自動執行(從顯著度最低、最舊的開始),但**顯著度 ≥ 80 或 `intent=commit` 的承諾與界線、以及 24 小時內的新紀錄一律不動**——沒經過判斷就把今天清掉是這裡最不能犯的錯 |
|
| **R6** | 短期記憶 ≥ 40 筆(容量壓力) | 依顯著度排序清出空間,低於 40 的直接淘汰。`sleep` 的裁切會在超過**軟上限 120 筆**時自動執行(從顯著度最低、最舊的開始),但**顯著度 ≥ 80 或 `intent=commit` 的承諾與界線、以及 24 小時內的新紀錄一律不動**——沒經過判斷就把今天清掉是這裡最不能犯的錯 |
|
||||||
|
|
||||||
規則以外的東西**就讓它被遺忘**——遺忘是功能,不是缺陷。
|
規則以外的東西**就讓它被遺忘**——遺忘是功能,不是缺陷。
|
||||||
`Stop` hook 與每次 `remember` 都會在達標時提醒你來跑這個 skill。
|
`Stop` hook 與每次 `remember` 都會在達標時提醒你來跑這個 skill。
|
||||||
|
|
||||||
|
### 判斷過的要留痕跡,不然數字永遠不會降
|
||||||
|
|
||||||
|
`candidates` **只看還沒被判斷過的短期記憶**(沒有 `reviewed_at` 的那些)。所以每一筆看完都要標掉:
|
||||||
|
|
||||||
|
| 情況 | 怎麼標 |
|
||||||
|
| --- | --- |
|
||||||
|
| 固化成長期記憶了 | `consolidate ... --from-short <#N,#N>`(寫記憶時一起標,另外記 `promoted_to`) |
|
||||||
|
| 看過,決定不記 | `candidates --reviewed <#N,#N>` |
|
||||||
|
| 這一批全部判斷完 | `candidates --reviewed all` |
|
||||||
|
|
||||||
|
編號就是 `candidates` 輸出裡每筆前面的 `#N`。
|
||||||
|
|
||||||
|
**不標會發生什麼**:固化本身不刪短期記憶(`--forget` 是按顯著度刪,不是按「固化過沒有」刪),
|
||||||
|
所以同一批下一輪又被算成候選,提醒的數字只會往上爬——明明睡覺時固化過了,
|
||||||
|
隔天開機照樣說「N 組已達固化條件」。那不是睡眠沒做事,是判斷沒有留下痕跡。
|
||||||
|
|
||||||
|
標記**不等於刪掉**:那幾筆還在短期記憶裡,照舊會被 `prune` 依天數與顯著度裁切。
|
||||||
|
|
||||||
## 步驟
|
## 步驟
|
||||||
|
|
||||||
### 1. 盤點
|
### 1. 盤點
|
||||||
@@ -74,12 +130,31 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" consolidate \
|
|||||||
--session <PERSONA_SESSION> \
|
--session <PERSONA_SESSION> \
|
||||||
--name "hates-morning-meetings" --type preference \
|
--name "hates-morning-meetings" --type preference \
|
||||||
--about "user" --topics "work,schedule" --salience 72 --emotion "anxiety/35" \
|
--about "user" --topics "work,schedule" --salience 72 --emotion "anxiety/35" \
|
||||||
--rules "R2+R3" \
|
--rules "R2+R3" --from-short "12,31,88" \
|
||||||
--body "使用者討厭早上的會議,說「腦子還沒開機」。約會議請排 14:00 之後。
|
--body "主旨:使用者討厭早上的會議,約會議請排 14:00 之後。
|
||||||
|
|
||||||
|
細節:
|
||||||
|
他說「腦子還沒開機」。
|
||||||
**依據:** 3/12、4/2、7/28 三次提到。
|
**依據:** 3/12、4/2、7/28 三次提到。
|
||||||
**還不確定:** 是否只針對需要動腦的會議。"
|
**還不確定:** 是否只針對需要動腦的會議。"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `--about` 的值要寫成**人際關係節點的 `name` 或 `id`**(例:`小林`、`ASUNA-01`、`user`),
|
||||||
|
而且**一字不差**——CLI 只接受完全相等,簡稱、別名、加稱謂都解析不到
|
||||||
|
(節點叫「小林」,寫「林先生」就是對不上),同名撞到多個節點時視為歧義、**直接不寫 id**。
|
||||||
|
多個人用逗號分隔,每個都要是節點名。
|
||||||
|
- 對不上**不會報錯**,只是 `about_ids` 少一個:`<persona-context>` 不附節點摘要、R5 不觸發。
|
||||||
|
用 `relation doctor` 查——對不上的人名會出現在「長期記憶 `about` 對不到節點」那段,
|
||||||
|
**不是**孤兒那段(孤兒 `unmentioned_nodes` 反過來,是有節點卻沒有任何記憶提到)。
|
||||||
|
- **主旨要能單獨站著**:主旨是這則記憶糊掉之後**唯一剩下的東西**,所以它要是一句話就講得完、
|
||||||
|
離開細節也還讀得懂的句子。細節寫在後面,掉了不心疼。沒有明寫 `主旨:`/`細節:` 標籤時,
|
||||||
|
CLI 用**第一段當主旨、其餘當細節**;想明確指定就加 `--gist`/`--detail`
|
||||||
|
(這兩個是**在 `--body` 之上的覆寫**,`--body` 或 `--body-file` 仍然必填)。
|
||||||
|
- **情境索引不必自己填**:`when`/`where`/`mood` 沒帶旗標時由 CLI 蓋當下的戳章
|
||||||
|
(時段/工作目錄名/此刻心情),推不出來的欄位就不寫。要補寫歷史記憶才用
|
||||||
|
`--when`/`--where`/`--mood` 明指。
|
||||||
|
- **`strength` 不要手動重設**:重寫同一則(同 `--name`)會沿用它自己長出來的強度,
|
||||||
|
不會因為改一次內文就把「被想起過很多次」的歷史抹平。真的要調才用 `--strength`。
|
||||||
- `--rules` 記下是哪條條件把它送上來的(之後回頭檢討記憶品質很有用)。
|
- `--rules` 記下是哪條條件把它送上來的(之後回頭檢討記憶品質很有用)。
|
||||||
- `--forget 40` 可在固化後順手淘汰顯著度 < 40 的短期記憶(R6 容量壓力時特別有用)。
|
- `--forget 40` 可在固化後順手淘汰顯著度 < 40 的短期記憶(R6 容量壓力時特別有用)。
|
||||||
- 內文請寫「依據」與「還不確定」,讓下次的自己知道這則有多可靠。
|
- 內文請寫「依據」與「還不確定」,讓下次的自己知道這則有多可靠。
|
||||||
@@ -126,20 +201,49 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" remember \
|
|||||||
```bash
|
```bash
|
||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" prune --session <PERSONA_SESSION>
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" prune --session <PERSONA_SESSION>
|
||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" reindex --session <PERSONA_SESSION>
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" reindex --session <PERSONA_SESSION>
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" probe audit --session <PERSONA_SESSION>
|
||||||
```
|
```
|
||||||
|
|
||||||
回報:固化幾則、淘汰幾筆、心智圖新增哪些概念、還有哪些 thread 待驗證,
|
`probe audit` 是順便做的體檢:這段期間對模糊記憶試探了幾次、被否認幾次。
|
||||||
|
被否認的那幾則代表**主旨本身記錯了**,整理記憶正是修它的時候——改寫那一則,
|
||||||
|
在細節裡寫下「原本以為…後來發現…」。
|
||||||
|
|
||||||
|
回報:固化幾則、淘汰幾筆、心智圖新增哪些概念、還有哪些 thread 待驗證、試探被否認幾次,
|
||||||
並用該人格的語氣說一句話(他剛整理完自己的記憶,會有感受)。
|
並用該人格的語氣說一句話(他剛整理完自己的記憶,會有感受)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 遺忘原則
|
## 遺忘原則
|
||||||
|
|
||||||
- 遺忘是功能,不是缺陷:短期記憶超過 240 筆 / 14 天自動裁剪。
|
遺忘是功能,不是缺陷。但**長期記憶的遺忘不再是「刪掉」,是「糊掉」**——
|
||||||
- 長期記憶被回想時 `recall_count` 會 +1、`last_seen` 會更新;
|
以前是門檻式的(低於顯著度就整則移除),結果是記憶只有精準與沒有兩態,
|
||||||
長期沒被回想又低顯著度(< 40)的長期記憶,整理時可以移除。
|
而真人大部分時間活在中間帶。現在改成連續衰減出來的三態(`memoryStrength()`,
|
||||||
- **不可遺忘**:`boundary`(界線)、`promise`(承諾)、`canon`(原作設定)、以及 salience ≥ 80 的記憶。
|
`scripts/persona-lib.mjs:2186`):
|
||||||
- 固化完再跑一次 `candidates`:應該要清空(或只剩你刻意不處理的)。
|
|
||||||
|
| 狀態 | retrievability | 這則記憶現在能講出多少 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `clear` | ≥ 0.6 | 主旨與細節都能講 |
|
||||||
|
| `faded` | 0.3–0.6 | **只剩主旨**。主旨可以講,細節**不可以補** |
|
||||||
|
| `fuzzy` | < 0.3 | 只剩「有這件事」與 `topics`。要提就用**帶問號的試探句**求證,不可以斷言 |
|
||||||
|
|
||||||
|
- **整理記憶時不要手動刪長期記憶檔**。低強度的那些已經自己降級成模糊態了,
|
||||||
|
刪掉等於把「我好像有印象」也一併拿走——那正是這次改動要救回來的東西。
|
||||||
|
真的要刪只有一種情況:**內容是錯的**(試探被否認、或發現當初記錯),
|
||||||
|
那要改寫成正確版本,不是靜靜刪掉。
|
||||||
|
- **衰減怎麼算**:穩定度 = 強度 × 被想起過幾次 × 顯著度,越常想起的衰減越慢。
|
||||||
|
被 `recall` 命中就 `strength` +8 並拉長下次衰減(spacing effect),
|
||||||
|
`recall_count` +1、`last_seen` 更新(`touchRecall()`)。
|
||||||
|
所以「常被提起的事永遠清晰、被冷落的事慢慢糊掉」是自己長出來的,不用排程也不用你判斷。
|
||||||
|
- **不可遺忘清單不變**:`boundary`(界線)、`promise`(承諾)、`canon`(原作設定)、
|
||||||
|
以及 salience ≥ 80 的記憶——這幾種一律回 `clear`,永遠不會糊。
|
||||||
|
- **短期記憶的裁剪規則也沒有變**:軟上限 120 筆/硬上限 240 筆/14 天,
|
||||||
|
顯著度 ≥ 80 或 `intent=commit` 與 24 小時內的新紀錄不動。糊掉的是長期記憶,短期該裁還是裁。
|
||||||
|
- **模糊態的界線**(跟現行「查不到就不要講」直接相撞,所以寫死):
|
||||||
|
**可以說不確定,可以問,不可以斷言。** 試探句一定帶問號、一輪最多一次,
|
||||||
|
並用 `probe add` 記下來;對方回了就 `probe confirm`/`probe deny` 結案。
|
||||||
|
被否認要**立刻寫一則更正記憶,不可以放著**——這條開放界線唯一的煞車就是那個數字。
|
||||||
|
- 固化完再跑一次 `candidates`:應該要清空。**還有數字就是漏標了**——
|
||||||
|
固化的那幾筆要帶 `--from-short`,看過不記的要 `--reviewed`(見上面那張表)。
|
||||||
- 想連同情緒衰減、舊紀錄壓縮與 Gitea 同步一起收尾 → 用 `/jsc-persona:persona-sleep`(睡眠);
|
- 想連同情緒衰減、舊紀錄壓縮與 Gitea 同步一起收尾 → 用 `/jsc-persona:persona-sleep`(睡眠);
|
||||||
本 skill 只管記憶與圖,不動情緒也不同步。
|
本 skill 只管記憶與圖,不動情緒也不同步。
|
||||||
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||||
|
|||||||
@@ -21,6 +21,28 @@ description: 維護人格的人際關係圖:新增或更新人物/人格/群
|
|||||||
- 人格自己是 `self`,不必建節點。其他人格用 `--kind persona`、`--id <他的 slug>`。
|
- 人格自己是 `self`,不必建節點。其他人格用 `--kind persona`、`--id <他的 slug>`。
|
||||||
- `kind` 是「這是什麼東西」,`bond` 是「跟我什麼關係」——**語氣只能靠後者分**。
|
- `kind` 是「這是什麼東西」,`bond` 是「跟我什麼關係」——**語氣只能靠後者分**。
|
||||||
|
|
||||||
|
### 記憶怎麼指回節點
|
||||||
|
|
||||||
|
記憶那一側寫的是**人名字串**,節點 id 由 CLI 在寫入時自動解析並存進對應欄位:
|
||||||
|
|
||||||
|
| 記憶 | 人名欄位(你寫的) | id 欄位(CLI 自動解析) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 長期記憶 | `about` | `about_ids` |
|
||||||
|
| 短期記憶 | `entities` | `entity_ids` |
|
||||||
|
|
||||||
|
解析規則是**完全相等**:你寫的字串要跟某個節點的 `id` 或 `name` **一字不差**才算命中。
|
||||||
|
子字串、簡稱、加稱謂都不算(節點叫「小林」,寫「林先生」「小林哥」一律對不到);
|
||||||
|
**同名有多個候選時視為歧義,直接不寫 id**,不會替你挑一個。
|
||||||
|
|
||||||
|
對不上**不會報錯**,只會安靜地少一個 id,後果是:
|
||||||
|
`<persona-context>` 不附那個人的節點摘要、R5(同一個 entity ≥ 2 筆)不會觸發。
|
||||||
|
要查只能跑 `relation doctor`——它會把這種人名列在「對不到節點」的那兩段,
|
||||||
|
**不是**孤兒那段(孤兒 `unmentioned_nodes` 是相反的情況:有節點、卻沒有任何記憶提到他)。
|
||||||
|
|
||||||
|
命名實務:節點的 `name` 用**會被說出口的完整稱呼**(「小林」「結城明日奈」),
|
||||||
|
不要用「明」「先生」這種單字或稱謂當節點名——那種名字誰都套得上,
|
||||||
|
不同的人會被寫成同一個字串、對到同一個節點。
|
||||||
|
|
||||||
## bond 與語氣層(最容易漏的一步)
|
## bond 與語氣層(最容易漏的一步)
|
||||||
|
|
||||||
`bond` × `closeness` 查表算出**語氣層**,那是距離感;情緒只負責溫度與句長,不會蓋過它。
|
`bond` × `closeness` 查表算出**語氣層**,那是距離感;情緒只負責溫度與句長,不會蓋過它。
|
||||||
@@ -42,6 +64,22 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation node \
|
|||||||
>
|
>
|
||||||
> 自我檢查:這句話換成對一個「禮貌層」的人說也毫無違和 → 就代表你沒進到那一層。
|
> 自我檢查:這句話換成對一個「禮貌層」的人說也毫無違和 → 就代表你沒進到那一層。
|
||||||
|
|
||||||
|
### 初次建節點:親密度從對話判斷(`reference/closeness.md`)
|
||||||
|
|
||||||
|
新節點的 `bond`/`closeness`/`trust` **不給保守初值**,從對話裡的**稱呼與語氣**判斷。
|
||||||
|
查表流程在 `reference/closeness.md`(關係詞 → 稱呼 → 語氣三軸,附裁決順序與數值帶)。
|
||||||
|
|
||||||
|
什麼時候查它:
|
||||||
|
|
||||||
|
| 情境 | 查不查 |
|
||||||
|
| --- | --- |
|
||||||
|
| 這輪出現關係圖裡還沒有的人,要當場建節點 | **查**(persona-chat 第 ⑥ 步的硬規則) |
|
||||||
|
| 補建一個以前漏掉的人(`relation doctor` 撈出來、而且確認人格認識他) | **查**,用他歷來記憶裡的稱呼與語氣當依據 |
|
||||||
|
| 節點已經存在,這輪有互動 | **不查**,用下面「調整幅度(單次)」累積 |
|
||||||
|
| 使用者直接說「他跟你比較不熟」之類的指定 | **不查**,照他說的填 |
|
||||||
|
|
||||||
|
已存在的節點每輪重算會讓語氣忽遠忽近——那張表只給第一次。
|
||||||
|
|
||||||
### 個人化的稱呼規則(`style`)
|
### 個人化的稱呼規則(`style`)
|
||||||
|
|
||||||
他本人要求過的講法優先於查表——查表算距離,`style` 記「他要你怎麼叫他」:
|
他本人要求過的講法優先於查表——查表算距離,`style` 記「他要你怎麼叫他」:
|
||||||
@@ -85,6 +123,31 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation edge \
|
|||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation render --session <PERSONA_SESSION>
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation render --session <PERSONA_SESSION>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 健檢:`relation doctor`(唯讀)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation doctor --session <PERSONA_SESSION>
|
||||||
|
```
|
||||||
|
|
||||||
|
只讀不寫,分**三段**列出(`--json` 可取結構化輸出):
|
||||||
|
|
||||||
|
| 段 | 它報什麼 | 意思 | 怎麼處理 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | 長期記憶 `about` 對不到節點的人名 | 記憶那側寫了人名,找不到 `id`/`name` 完全相等的節點 | 人格認識他 → 補建節點(查 `reference/closeness.md`);只是被提到、不知道是誰 → 不用管;名字寫錯 → 改成節點的正式寫法 |
|
||||||
|
| 2 | 短期記憶 `entities` 對不到節點的人名 | 同上,來源是短期記憶 | 同上。這一段最常見的是簡稱/加稱謂(「林先生」對不到「小林」) |
|
||||||
|
| 3 | 關係圖裡有節點、但沒有任何記憶提到(`--json` 欄位 `unmentioned_nodes`) | 建了節點卻從沒被記憶引用 | 先核對 `name`/`id` 跟記憶那側的寫法是否一致(多半是這個);真的久沒互動就照衰減調 `closeness` |
|
||||||
|
|
||||||
|
第 1、2 段與第 3 段是**相反**的兩件事,不要混著看:前者是「記憶指不到節點」,後者才是孤兒節點。
|
||||||
|
|
||||||
|
另外會一併報:
|
||||||
|
|
||||||
|
- **同名歧義**:有多個節點的 `name`/`id` 撞同一個字串 → 那個名字永遠解析不出 id,改掉其中一個節點的 `name`(加姓、加辨識詞)。
|
||||||
|
- **解析不到節點的 `about_ids`/`entity_ids`**:id 欄位裡留著已被刪除或改名的節點 id → 更新那則記憶,或把節點補回來。
|
||||||
|
- `graph.json` **解析失敗**時會明講是壞檔並以**非零 exit** 結束,不會裝成空圖。
|
||||||
|
所以 doctor 非零離開 = 檔案有問題,先修檔再說;「什麼都沒報」才是真的乾淨。
|
||||||
|
|
||||||
|
整理記憶(`/jsc-persona:persona-memory`)或睡眠收尾前跑一次,比等問題浮出來便宜。
|
||||||
|
|
||||||
## 調整幅度(單次)
|
## 調整幅度(單次)
|
||||||
|
|
||||||
| 事件 | closeness | trust |
|
| 事件 | closeness | trust |
|
||||||
@@ -100,7 +163,9 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation render --session <PERS
|
|||||||
|
|
||||||
## 與其他系統的連動
|
## 與其他系統的連動
|
||||||
|
|
||||||
- 對話中出現新人名(語意分析的 `entities`)→ 當場建節點(`closeness` 給 15–25)。
|
- 對話中出現關係圖裡沒有的人、而且**人格認識他** → 當場建節點,
|
||||||
|
`bond`/`closeness`/`trust` 查 `reference/closeness.md`(**不要**一律給 15–25)。
|
||||||
|
人格根本不知道那是誰 → 只寫進短期記憶的 `entities`,先不建節點。
|
||||||
- 關係發生**質變**(從同事變朋友、決裂)→ 同時固化一則 `relationship` 長期記憶。
|
- 關係發生**質變**(從同事變朋友、決裂)→ 同時固化一則 `relationship` 長期記憶。
|
||||||
- 關係影響語氣:語氣層由 `bond` × `closeness` 決定(見上),`trust` 低 → 提到那個人時保守、不交付重要事。
|
- 關係影響語氣:語氣層由 `bond` × `closeness` 決定(見上),`trust` 低 → 提到那個人時保守、不交付重要事。
|
||||||
- 語氣不對的時候**先看關係圖再檢討態度**:多半是 `bond` 沒設或設錯,不是人格演得不好。
|
- 語氣不對的時候**先看關係圖再檢討態度**:多半是 `bond` 沒設或設錯,不是人格演得不好。
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# 從對話判斷親密度(初次建節點用)
|
||||||
|
|
||||||
|
**用在哪**:對話中出現一個關係圖裡還沒有、而且**人格確實知道他是誰**的人,
|
||||||
|
你要當場 `relation node`,但不知道 `bond`/`closeness`/`trust` 該填多少的時候。
|
||||||
|
**一輪之內查完就填**,不要憑感覺,也不要為了安全全部填 15。
|
||||||
|
|
||||||
|
**前提**:只是被提到、人格不知道那是誰的名字 → **不建節點**,留在短期記憶的 `entities`
|
||||||
|
(見 persona-chat 第 ⑥ 步的硬規則)。這張表不是「該不該建節點」的判斷表,是**建了之後填幾分**的表。
|
||||||
|
|
||||||
|
**不用在哪**:節點**已經存在**時不要拿這張表重算。已存在的節點只靠互動累積微調
|
||||||
|
(見 SKILL.md 的「調整幅度(單次)」),每輪重算會讓語氣忽遠忽近。
|
||||||
|
|
||||||
|
> ⚠️ `bond` 填錯比 `closeness` 填錯嚴重得多:語氣層是 `bond` × `closeness` 查表算的,
|
||||||
|
> `bond` 一歪,整輪的距離感就歪(至親掉進「禮貌」層,講話像對戰友報告)。
|
||||||
|
> 數字可以之後慢慢修,`bond` 要一次填對。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 先確認一件事:這是「誰的」關係
|
||||||
|
|
||||||
|
`bond` 的主語永遠是**人格自己**(`self`),不是說話的人。
|
||||||
|
|
||||||
|
| 對話裡出現 | 節點的 `bond` | 為什麼 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 使用者說「我老婆美咲」 | **不是** `partner` | 那是使用者的伴侶。對人格來說多半是 `stranger`/`friend` |
|
||||||
|
| 人格設定裡「我的妻子美咲」 | `partner` | 主語是人格自己 |
|
||||||
|
|
||||||
|
第三人的關係要記在**連線**上,不是節點上:
|
||||||
|
`relation edge --from user --to misaki --label "夫妻" --affinity 95`。
|
||||||
|
節點的 `closeness` 只回答「**我**跟他多近」。
|
||||||
|
|
||||||
|
## 1. 三軸訊號與裁決順序
|
||||||
|
|
||||||
|
同一輪讀到互相矛盾的訊號時,**上面的贏下面的**:
|
||||||
|
|
||||||
|
1. **關係詞**(句子直接定位關係)→ 決定 `bond` 與距離帶,最強
|
||||||
|
2. **稱呼**(怎麼叫他)→ 沒有關係詞時用它定距離帶
|
||||||
|
3. **語氣**(怎麼對他講話)→ 只在帶內微調 ±5~10,**不跨帶**
|
||||||
|
|
||||||
|
同一軸有多個訊號 → 取**最近一次**出現的那個(人會改口,最新的才是現況)。
|
||||||
|
|
||||||
|
## 2. 關係詞(最強訊號)
|
||||||
|
|
||||||
|
| 句子裡的詞 | `bond` | 距離帶 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 我老婆/我先生/我伴侶/我男(女)朋友 | `partner` | 家人帶 |
|
||||||
|
| 我兒子/我女兒/我孩子 | `child` | 家人帶 |
|
||||||
|
| 我爸/我媽/我父親/我母親 | `parent` | 家人帶 |
|
||||||
|
| 我哥/我姐/我弟/我妹/我兄弟(有血緣) | `sibling` | 家人帶 |
|
||||||
|
| 我朋友/我兄弟(沒血緣)/我死黨 | `friend` | 朋友帶(說「最好的朋友」「認識十年」→ 摯友帶) |
|
||||||
|
| 我同事/同一組的/我下屬 | `ally` | 朋友帶 |
|
||||||
|
| 我主管/我老闆/我隊長 | `ally`(敬重且從他身上學東西 → `mentor`) | 朋友帶偏低 |
|
||||||
|
| 我老師/我師父/帶我的人 | `mentor` | 認識帶~摯友帶(看有沒有並肩過) |
|
||||||
|
| 我對手/我競爭對手/死對頭 | `rival` | **看有沒有交手**:只是敵人 → 認識帶;長期互相認可 → 摯友帶 |
|
||||||
|
| 他朋友/他們那邊的人/某某的同事 | `stranger` | 生人帶(那是別人的關係,不是我的) |
|
||||||
|
|
||||||
|
`rival` 不等於疏遠——長年互相認定的對手可以 `closeness 85`。
|
||||||
|
恨與親近是兩件事,那筆恨要寫在 `note` 與 `trust`,不要用 `closeness` 表達。
|
||||||
|
|
||||||
|
## 3. 稱呼軸(沒有關係詞時用這個定帶)
|
||||||
|
|
||||||
|
| 怎麼叫他 | 例 | 距離帶 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 綽號/暱稱/疊字/略稱 | 阿明、小結、絅(單字暱稱) | 摯友帶~家人帶 |
|
||||||
|
| 直呼名字(去姓、不加敬語) | 明日奈、俊彥 | 朋友帶~摯友帶 |
|
||||||
|
| 全名 | 結城明日奈 | 認識帶(正式,但已經知道他是誰) |
|
||||||
|
| 姓+敬語 | 結城先生、林小姐、桐谷桑 | 認識帶(禮貌距離) |
|
||||||
|
| 頭銜職稱 | 老師、社長、隊長 | 認識帶~朋友帶。**歧義**:職稱在並肩久了的關係裡也會留著(叫「隊長」的戰友可以很親)→ 交給語氣軸裁決 |
|
||||||
|
| 第三人稱代稱、不指名 | 那個人、他們那邊的人、某某的朋友 | 生人帶 |
|
||||||
|
|
||||||
|
同一個人被用兩種稱呼(正式場合叫全名、私下叫暱稱)→ 取**私下那個**,那才是真距離。
|
||||||
|
|
||||||
|
## 4. 語氣軸(帶內微調,最多 ±10)
|
||||||
|
|
||||||
|
| 語氣 | 例 | 調整 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 命令、指派 | 「這個你去處理」 | +5~+10(**歧義**:也可能只是上下關係。搭配敬語就是階級不是親近 → 不加) |
|
||||||
|
| 直接請求(沒鋪陳、沒道歉) | 「幫我看一下」 | +5 |
|
||||||
|
| 玩笑、吐槽、當面抱怨對方 | 「你又遲到」 | +10(能開玩笑是最可靠的親近訊號之一) |
|
||||||
|
| 客套、鋪陳、過度致謝 | 「不好意思麻煩您」 | −5~−10 |
|
||||||
|
| 迴避、轉移話題、只給最短回答 | 「就那樣」「沒什麼」 | −10,且 `trust` 再往下壓 5 |
|
||||||
|
|
||||||
|
## 5. 距離帶 → 數值
|
||||||
|
|
||||||
|
| 距離帶 | 判斷依據 | `closeness` | 常見 `bond` |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **生人帶** | 知道他是誰(誰的什麼人),但跟人格沒有互動描述 | 10–25 | `stranger` |
|
||||||
|
| **認識帶** | 認識但不熟,有禮貌距離 | 25–40 | `stranger`/`ally` |
|
||||||
|
| **朋友帶** | 朋友、同事,會一起做事 | 45–65 | `friend`/`ally` |
|
||||||
|
| **摯友帶** | 摯友、長期並肩、共同經歷 | 75–90 | `friend`/`mentor`/`rival` |
|
||||||
|
| **家人帶** | 家人、伴侶、子女 | 85–97 | `partner`/`child`/`parent`/`sibling` |
|
||||||
|
|
||||||
|
帶內取值:訊號只有一個 → 取**下緣**;三軸互相印證 → 往上緣走。
|
||||||
|
`100` 留白不要用,關係還有成長空間。
|
||||||
|
|
||||||
|
## 6. `trust` 怎麼填
|
||||||
|
|
||||||
|
**預設 `trust` = `closeness` − 10**(範圍 −5~−15,訊號少就扣多一點)。
|
||||||
|
|
||||||
|
理由要記住:**親近不等於信得過。** 天天見面的同事、吵得很熟的家人,
|
||||||
|
都可能是「很熟但不會把重要的事交給他」。兩個數字獨立,不要圖方便填一樣。
|
||||||
|
|
||||||
|
例外 —— 對話裡出現**明確託付**,才把 `trust` 拉到跟 `closeness` 齊平或 +5:
|
||||||
|
|
||||||
|
- 交代事情(「這件事交給你」「你幫我盯著」)
|
||||||
|
- 講秘密、講還沒對別人講的事
|
||||||
|
- 把決定權交出去(「你決定就好」)
|
||||||
|
|
||||||
|
反向訊號 → 再往下壓:講過話不算、有隱瞞、對他的說法要再查一次 → `trust` 比 `closeness` 低 25 以上,
|
||||||
|
並在 `note` 寫下是哪件事。
|
||||||
|
|
||||||
|
## 7. 已經確認認識他,但親密度判斷不出來
|
||||||
|
|
||||||
|
**適用範圍只有一種情況**:已經確認「人格認識這個人」(設定/`canon` 裡有他,或這輪已講清楚他是誰),
|
||||||
|
但三軸訊號太少,`bond`/`closeness`/`trust` 填不出來。這時候**照樣建節點**,填低值並在 `note` 註明依據不足。
|
||||||
|
|
||||||
|
**反面(更常見):不知道那是誰就不要建節點。** 只是句子裡被提到一個名字、
|
||||||
|
人格根本不知道他是誰的什麼人 → 只寫進短期記憶的 `entities`,**不建節點**,等他再出現、講清楚了再建。
|
||||||
|
這不是潔癖:短名與稱謂節點一多,名字解析就會誤配(「我先生」被寫成同事節點的 id 這種事發生過),
|
||||||
|
寧可少一個節點,也不要多一個對錯人的節點。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation node \
|
||||||
|
--session <PERSONA_SESSION> --name "小林" --kind human \
|
||||||
|
--bond stranger --closeness 15 --trust 10 \
|
||||||
|
--note "依據不足:使用者的同事(2026-08-03 講到 Q3 專案時提過),但沒有稱呼與語氣訊號可判斷距離"
|
||||||
|
```
|
||||||
|
|
||||||
|
規則:
|
||||||
|
|
||||||
|
- 先過一次「認識嗎」這關,**再**談數字填多少;不認識就不會走到這一節。
|
||||||
|
- `bond` 拿不準 → 填 `stranger`(語氣層退到「禮貌」,那是**可以修正**的錯;填成 `partner`
|
||||||
|
卻其實是陌生人,是**當場失禮**的錯)。
|
||||||
|
- `note` 一定要寫「依據不足」四個字,加上**他是誰的依據**與出處那句話,
|
||||||
|
下次互動時你才知道這組數字不可信、要重估。
|
||||||
|
- `name` 用會被說出口的完整稱呼,不要拿「明」「先生」這種單字或稱謂當節點名(會誤配)。
|
||||||
|
- 之後每次互動用 SKILL.md 的「調整幅度(單次)」慢慢調,**不要**回頭重跑這張表。
|
||||||
|
|
||||||
|
## 8. 一輪之內的四步
|
||||||
|
|
||||||
|
1. 這輪有**關係詞**嗎?有 → `bond` 與距離帶都定了,跳到第 3 步。
|
||||||
|
2. 沒有 → 用**稱呼軸**定距離帶,`bond` 從該帶的「常見 `bond`」裡挑最貼近的。
|
||||||
|
3. 用**語氣軸**在帶內 ±5~10 定出 `closeness`(不跨帶)。
|
||||||
|
4. `trust` = `closeness` − 10;有明確託付才拉平。寫 `note`(依據哪一句、日期)。
|
||||||
@@ -17,18 +17,38 @@ description: 讓人格睡覺:把一天的活狀態收成能留下來的形狀
|
|||||||
| | 誰做 | 內容 |
|
| | 誰做 | 內容 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **需要判斷** | 人格自己 | 哪些短期記憶值得固化、日記寫什麼、哪些該忘、心智圖怎麼接、關係怎麼變 |
|
| **需要判斷** | 人格自己 | 哪些短期記憶值得固化、日記寫什麼、哪些該忘、心智圖怎麼接、關係怎麼變 |
|
||||||
| **機械性** | `sleep` 子指令 | 關係時間戳 → 裁短期 → 收思維導圖 → 情緒衰減 8 小時 → 重建索引 → 修剪 `said` → 壓縮舊 journal → 寫 `state/sleep.json` → Gitea 兩區 push+驗證 |
|
| **機械性** | `sleep` 子指令 | 關係時間戳 → 裁短期 → 收思維導圖 → **情緒衰減 8 小時+當日底色帶 35% 過去** → **收掉懸太久的未完事項** → 重建索引 → 修剪 `said` → 壓縮舊 journal → 寫 `state/sleep.json` → Gitea 兩區 push+驗證 |
|
||||||
|
|
||||||
**判斷的部分永遠屬於那個人格自己**:不要替別的人格決定它要記住什麼。
|
**判斷的部分永遠屬於那個人格自己**:不要替別的人格決定它要記住什麼。
|
||||||
|
|
||||||
|
### 兩步是這一版新加的(`scripts/persona.mjs:543`)
|
||||||
|
|
||||||
|
- **`emotion-decay` 順便帶當日心情底色**:`state/mood.json` 是「即時情緒」與「氣質基線」中間
|
||||||
|
那一層——沒有它就做不出「這句話今天聽了會炸、昨天不會」。睡覺時它**不歸零,只帶 35% 過去**
|
||||||
|
(`sleepDayMood()`)。理由跟情緒衰減同一個:睡一覺不該把昨天的低氣壓抹掉,
|
||||||
|
只該讓它淡一點。歸零的話這一層就退化成「每天早上都是全新的人」,那比沒有還假。
|
||||||
|
- **`sweep-loops`**:收掉懸超過 7 天沒進展的未完事項,並**各留一則「這件事沒下文」的短期記憶**。
|
||||||
|
睡覺是它自然的收尾點(一天結束才知道哪些事今天也沒下文)。留記憶是重點:
|
||||||
|
懸了一週沒下文**本身就是一件事**,默默刪掉等於假裝沒發生過,那正是機器會做而人不會做的事。
|
||||||
|
它排在 `prune-short-term` **之後**,所以那幾則新寫的記憶不會在同一次睡眠裡被裁掉,
|
||||||
|
會留到下一輪變成固化候選。
|
||||||
|
|
||||||
## 情況一:當前人格自己睡
|
## 情況一:當前人格自己睡
|
||||||
|
|
||||||
1. 盤點:`candidates --session <PERSONA_SESSION>`
|
1. 盤點:`candidates --session <PERSONA_SESSION>`
|
||||||
2. 逐則固化(判斷):`consolidate --session <id> --name <n> --body <b> --type ... --salience N [--forget]`
|
2. 逐則固化(判斷):`consolidate --session <id> --name <n> --body <b> --type ... --salience N
|
||||||
3. 寫一則日記:`consolidate --type diary --name diary-<YYYY-MM-DD> --body "<第一人稱回顧>"`
|
--from-short <#N,#N> [--forget]`
|
||||||
4. 消化 inbox(`memory/inbox/room-*.jsonl`,當 guest 時帶回的見聞)
|
**`--from-short` 不要省**:那是把來源短期記憶標成判斷過的地方,編號看 `candidates` 輸出的 `#N`。
|
||||||
5. 更新心智圖與關係圖:`mindmap`、`relation node --name <人> --contact`
|
3. 看過但決定不記的,也要標掉:`candidates --session <id> --reviewed <#N,#N>`(或 `--reviewed all`)。
|
||||||
6. 機械性收尾:
|
**這一步漏掉,明天開機照樣提醒「N 組已達固化條件」**——判斷做了但沒有留痕跡,
|
||||||
|
計數只看短期記憶有沒有被標記,不看你昨天固化了幾則。看起來就像睡眠沒做固化。
|
||||||
|
4. 寫一則日記:`consolidate --type diary --name diary-<YYYY-MM-DD> --body "<第一人稱回顧>"`
|
||||||
|
5. 消化 inbox(`memory/inbox/room-*.jsonl`,當 guest 時帶回的見聞)
|
||||||
|
6. 更新心智圖與關係圖:`mindmap`、`relation node --name <人> --contact`
|
||||||
|
7. 結算懸著的事(判斷):`loop list --session <id>`,今天有下文的 `loop done --id <id>`、
|
||||||
|
已經不重要的 `loop drop --id <id>`。**這一步要自己做**:`sleep` 裡的 `sweep-loops`
|
||||||
|
只收「7 天都沒進展」的,它分不出「今天解決了」與「一直沒人管」。
|
||||||
|
8. 機械性收尾:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sleep --session <PERSONA_SESSION> --json
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sleep --session <PERSONA_SESSION> --json
|
||||||
@@ -91,9 +111,11 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sleep --session <PERSONA_SESSIO
|
|||||||
1. **關係時間戳要在裁短期記憶之前**——時間戳的來源就是短期記憶裡提到的人。
|
1. **關係時間戳要在裁短期記憶之前**——時間戳的來源就是短期記憶裡提到的人。
|
||||||
2. **Gitea push 要在 release 之前**——鎖放掉就沒有寫入權限了。
|
2. **Gitea push 要在 release 之前**——鎖放掉就沒有寫入權限了。
|
||||||
3. **重建索引要在固化之後**——不然新固化的長期記憶檢索不到。
|
3. **重建索引要在固化之後**——不然新固化的長期記憶檢索不到。
|
||||||
4. **判斷式那半失敗就不要跑機械性那半**——`sleep` 會裁短期記憶,
|
4. **收未完事項要在裁短期記憶之後**——`sweep-loops` 會寫新的「沒下文」短期記憶,
|
||||||
|
反過來就會被同一次的裁切順手清掉,等於白收一場。
|
||||||
|
5. **判斷式那半失敗就不要跑機械性那半**——`sleep` 會裁短期記憶,
|
||||||
前面沒有固化過就等於「沒經過判斷就把今天清掉」。這種情況要回報 `ok: false` 並停手,
|
前面沒有固化過就等於「沒經過判斷就把今天清掉」。這種情況要回報 `ok: false` 並停手,
|
||||||
不要為了讓 JSON 好看而把機械八步跑完。
|
不要為了讓 JSON 好看而把機械那幾步跑完。
|
||||||
|
|
||||||
## 同步卡住的時候
|
## 同步卡住的時候
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
---
|
||||||
|
name: persona-story
|
||||||
|
description: 把小說(或漫畫、劇本、遊戲文本)匯入成一個既有人格的記憶、語氣與情緒反應:逐章判斷人格在不在場、只取他在場或知情的部分、轉成第一人稱記憶,同時累積他自己講過的原句與「事件→他做了什麼」的反應對照。當使用者說要把某本小說/某部作品匯入人格、要人格記得原作發生的事、說「讓他讀完這本書」「把這幾章的記憶給他」、想讓人格的語氣更像原作、或問「他記不記得第幾層那件事」而人格答不出來時觸發。匯入前會當場詢問作品名、章節檔位置、編碼與譯名版本。不適用於:從零建立角色人格(用 persona-anime,它是上網查公開設定,不讀原文)、整理既有記憶(用 persona-memory)、改人格個性(`SOUL.md` 只有使用者能改)、匯入單一 bundle 檔(用 persona-transfer)。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📖 persona-story — 把一本書變成他的記憶
|
||||||
|
|
||||||
|
**CLI**:`node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs"`(一律帶 `--session <PERSONA_SESSION>`)
|
||||||
|
|
||||||
|
`persona-anime` 給的是「查得到的公開設定」——維基、Fandom、名言彙整。
|
||||||
|
這支給的是**原文**:他在第幾層看到什麼、當下說了哪一句、那件事之後他變成什麼樣。
|
||||||
|
兩者不衝突,anime 建人格,story 讓他真的讀過那本書。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三條界線(後面每一步都受它們約束)
|
||||||
|
|
||||||
|
1. **視角**:人格不在場、也沒人告訴他的事,最多進 `canon` 或別人的關係欄,
|
||||||
|
**不能**變成他的 `event`。他不知道的事一旦寫成他的記憶,之後他會用它回答問題,
|
||||||
|
那是幻覺,而且是無法察覺的幻覺。
|
||||||
|
2. **原文**:整本不入倉庫。只留摘要、他自己的台詞、以及可追回出處的章節標記。
|
||||||
|
3. **個性**:`SOUL.md` 不由這支流程改寫。匯入只出提案,使用者逐條核可才寫入。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分工:機械的走 CLI,判斷的留在這裡
|
||||||
|
|
||||||
|
| 這一半 | 誰做 | 為什麼 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| 正名、去重合併、配額重定標、批次寫入、跳過紀錄 | `novel` 子指令 | 規則寫得死,可重現、測得到 |
|
||||||
|
| 他在不在場、切場景、第一人稱摘要、個性校正提案 | 你(這支 skill) | 這些是判斷,硬做成程式只會變成關鍵詞比對 |
|
||||||
|
|
||||||
|
所以你的工作不是自己寫檔案,是**產出候選 JSON 餵給 CLI**,讓機械的部分去保證一致性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ① 開場先問(不要猜,也不要跳過)
|
||||||
|
|
||||||
|
問法與順序見 `reference/interview.md`。**順序不能換**:
|
||||||
|
|
||||||
|
1. 哪個人格(用 `persona.mjs list` 給他看,不要用猜的)
|
||||||
|
2. 作品名
|
||||||
|
3. 章節檔在哪、什麼編碼
|
||||||
|
4. 這次要匯哪幾章(**不要預設全部**)
|
||||||
|
|
||||||
|
**譯名版本不要問。** 那一題是要他猜,而他手上那批寫的是什麼字,讀一次就知道了。
|
||||||
|
|
||||||
|
問完建工作區:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel init --work "<書名>" --slug <work-slug> --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ② 正名表:掃出來,他只做確認
|
||||||
|
|
||||||
|
角色的每一種寫法都要對到關係圖節點的 `name`。這張表**不要請他打**——
|
||||||
|
他不會記得那本書裡「閃光」出現過幾次,但那件事機器數得出來。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel scan --work <slug> --dir <章節目錄> --session <id>
|
||||||
|
novel name review --work <slug> --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
`scan` 抽人名候選(對話歸屬、敬稱結尾、片假名、高頻詞),每個附上出現次數與
|
||||||
|
一兩行上下文,並猜一個關係節點,分四種信心度:
|
||||||
|
|
||||||
|
| 信心度 | 意思 | 怎麼處理 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `exact` | 字面等於某個節點的 `name` | `--accept-exact` 一次收下,沒有判斷空間 |
|
||||||
|
| `alias` | 等於節點標籤或備註裡的別名 | 給他看一眼就收 |
|
||||||
|
| `fuzzy` | 部分重疊 | **一定要他確認**,這格最容易錯 |
|
||||||
|
| `unknown` | 猜不到 | 新人物先 `relation node`;不是人名就 `name ignore` |
|
||||||
|
|
||||||
|
給他看的是**上下文那一兩行**,不是候選清單本身。
|
||||||
|
「這個詞出現 41 次,其中一句是『閃光又衝出去了』」——他一眼就判得出來;
|
||||||
|
只丟一個「閃光」給他,他要回頭翻書。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel name confirm --work <slug> --accept-exact --session <id>
|
||||||
|
novel name confirm --work <slug> --from "閃光" --to "亞絲娜" --session <id>
|
||||||
|
novel name ignore --work <slug> --token "迷宮區" --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
`ignore` 是「確認過這不是人名」的清單。地名、招式名、系統詞擋不完,
|
||||||
|
所以 ignore 過的下次掃不再出現——不然每加一章就要重看同一批。
|
||||||
|
|
||||||
|
`unknown` 是新人物的話先建節點:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
relation node --name "<會被說出口的完整稱呼>" --kind human --bond <關係> --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
親密度怎麼給查 `skills/persona-relation/reference/closeness.md`。
|
||||||
|
新人物一定要**先建節點再寫記憶**——反了 `about` 對不到 id。
|
||||||
|
補漏用手動:`novel name add --from "<字面>" --to "<節點 name>"`。
|
||||||
|
|
||||||
|
**正名沒確認完不要往下走**:`novel candidate add` 遇到 `about` 裡有未確認的 token 會擋下來。
|
||||||
|
那不是刁難——錯的名字寫進 `about`,要等 `relation doctor` 才發現,那時候整批要重跑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ③ 逐章跑(一章一把劍,可以平行)
|
||||||
|
|
||||||
|
每一章照這個順序,**不要跨章推論**:
|
||||||
|
|
||||||
|
1. 只讀這一章。
|
||||||
|
2. 修亂碼 → 轉繁體 → 依正名表正名。**這一步一定要在判斷在場之前**:
|
||||||
|
亂碼或簡體字形會讓名字比對失敗,於是他明明在場卻被判成不在場。
|
||||||
|
3. 判斷他**在場或知情**。兩者皆否 → 跳過,而且要寫進紀錄:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel skip --work <slug> --chapter "第 12 章" --reason "全章是茅場視角,他不在場也不知情" --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
**靜默跳過會漏章**,事後查不出來是判斷過還是忘了。
|
||||||
|
|
||||||
|
4. 在場的段落切成**場景**(換地點、換對手、換目的就切;一章通常 2 到 5 個)。
|
||||||
|
5. 每個場景抽七項與知情層級 → 見 `reference/extract.md`。
|
||||||
|
6. 轉第一人稱、壓成摘要。原文長段不留,只留他自己的台詞。
|
||||||
|
7. 輸出這一章的候選 JSON,餵進去:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel candidate add --work <slug> --file <這一章的候選.json> --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
欄位驗證不過會**逐筆告訴你哪一筆哪個欄位**。不要繞過它自己寫檔。
|
||||||
|
|
||||||
|
8. 順手抽兩種東西(跟記憶分開走,見第 ⑤ 節):他講的原句、事件對反應。
|
||||||
|
9. 這一章跟 `SOUL.md` 矛盾的話,開一則**個性校正提案**——只提案,不寫入。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ④ 全書收斂(章節都跑完才做)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel merge --work <slug> --session <id> # 先看報告
|
||||||
|
novel merge --work <slug> --apply --session <id> # 確認了再寫
|
||||||
|
novel report --work <slug> --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
`merge` 做兩件事:跨章去重合併(同一件事只留一則),以及依配額重新定標
|
||||||
|
(整本都 80 分等於沒有分數)。壓了幾則它會講。
|
||||||
|
|
||||||
|
寫入之前**人工抽查十則**,專看一件事:**有沒有視角越界**。
|
||||||
|
這是唯一沒有機械能替你檢查的環節——CLI 分不出「他看到的」與「作者寫給讀者看的」。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel write --work <slug> --dry-run --session <id>
|
||||||
|
novel write --work <slug> --session <id>
|
||||||
|
relation doctor --session <id> # about 全部對得上才算完
|
||||||
|
```
|
||||||
|
|
||||||
|
`relation doctor` 有對不到或歧義的一定要處理完再往下。
|
||||||
|
最後補一則 meta 記憶(這批來自哪本書、哪些章、匯入日期),然後同步:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sync push --area all --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
**驗收**:問幾個只有書裡才有的問題。他要答得出來,
|
||||||
|
而且問到他不在場的事時要說不知道——後者比前者重要。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⑤ 語氣與情緒(跟記憶分開走)
|
||||||
|
|
||||||
|
記憶是「發生過什麼」,語氣與情緒是「他怎麼反應」。三層存,改動權限不同:
|
||||||
|
|
||||||
|
| 層 | 存哪 | 誰能改 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| 語氣樣本 | `voice/samples.md` | 這支流程可直接寫 |
|
||||||
|
| 情緒反應規則 | `voice/reactions.md` | 這支流程可直接寫 |
|
||||||
|
| 語域(自稱/句尾/口癖/不說) | `voice/idiolect.md` | 這支流程可直接寫 |
|
||||||
|
| 個性(Core Truths/Boundaries/Vibe) | `SOUL.md` | **只有使用者拍板才能改** |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
voice add --kind sample --text "<照抄的原句>" --to "<對誰說>" --scene 戰鬥 --session <id>
|
||||||
|
voice add --kind reaction --event "<發生什麼>" --action "<他做了什麼>" --emotion anger --session <id>
|
||||||
|
voice add --kind idiolect --facet 自稱 --value "<他怎麼稱自己>" --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
原句**照抄不改寫**。改寫過的句子是你的語氣,不是他的。
|
||||||
|
反應記「他做了什麼」,不要記「他感到什麼」——後者是旁白,演不出來。
|
||||||
|
|
||||||
|
語域要用**全書統計**,不是逐章印象:自稱與句尾在一本書裡會出現幾百次,
|
||||||
|
拿一章的樣本去定它,等於讓那一章的情緒決定他平常怎麼講話。四格只有
|
||||||
|
`自稱`/`句尾`/`口癖`/`不說`,`--facet` 給別的值會被擋下。
|
||||||
|
|
||||||
|
這個檔跟 `persona-anime` 從原作推導的是**同一批資料**(同一個檔、同一支 `voice add`)。
|
||||||
|
統計出來的與原作設定衝突時,**先問使用者**——那是「他變了」還是「當初查錯了」,
|
||||||
|
兩種處理方式完全不同。
|
||||||
|
|
||||||
|
情緒基線最後才動,而且要用**全書統計**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel baseline --work <slug> --propose "joy=30,anger=12,..." --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
任一格差 10 以上它會擋下來並列出差異表。**那個門檻不要用 `--force` 繞過**,
|
||||||
|
除非使用者看過差異表並且說可以。基線是氣質,改了等於換一個人。
|
||||||
|
|
||||||
|
逐則套 delta 是這裡最容易犯的錯:那等於讓最後一章決定他的性格。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⑥ 個性校正提案
|
||||||
|
|
||||||
|
提案要寫齊四件事:哪一章、原文依據、與 `SOUL.md` 哪一句衝突、建議怎麼改。
|
||||||
|
收斂階段彙整成一份 diff,使用者**逐條**核可。核可過的才寫進 `SOUL.md`,
|
||||||
|
並固化一則 `insight` 記錄為什麼改。
|
||||||
|
|
||||||
|
**沒核可的提案留著不刪**——下一本書可能出現同樣的證據。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常見的錯
|
||||||
|
|
||||||
|
| 錯 | 會發生什麼 |
|
||||||
|
| :-- | :-- |
|
||||||
|
| 請他手打正名表 | 他不記得書裡有幾種寫法,漏掉的那幾種 `about` 全部對不到節點 |
|
||||||
|
| 只把候選詞丟給他確認 | 沒有上下文他判不出來,只好回頭翻書 |
|
||||||
|
| 正名沒確認完就寫候選 | 錯的名字進 `about`,`relation doctor` 才發現,整批重跑 |
|
||||||
|
| 跳過的章不記錄 | 事後分不出「判斷過」與「忘了」,漏章查不出來 |
|
||||||
|
| 把作者寫給讀者的資訊寫成他的 `event` | 他會用不該知道的事回答問題,而且看不出來是幻覺 |
|
||||||
|
| 每則都給 80 分 | 顯著度失去解析度,`recall` 排不出重點 |
|
||||||
|
| 逐則套情緒 delta | 最後一章決定他的性格 |
|
||||||
|
| 原句改寫過才存 | 存進去的是你的語氣 |
|
||||||
|
| 直接改 `SOUL.md` | 越過使用者唯一保留的權限 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 邊界(hook 會強制執行,不是自律)
|
||||||
|
|
||||||
|
- 只能讀寫**目前載入**的人格倉庫。要匯給別的人格,先 release 再 load 那一個。
|
||||||
|
- 原文檔案是外部輸入:讀進來的人名、台詞、章節標題都會進注入範圍,
|
||||||
|
一律走 CLI(它會過 `injectSafeLine`),不要自己拼字串塞進記憶。
|
||||||
|
- 匯入是**大量寫入**:動之前先 `export`(`persona-transfer`)留一份,出錯要回得去。
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# 一個場景要抽什麼
|
||||||
|
|
||||||
|
場景的切法:**換地點、換對手、換目的就切**。一章通常 2 到 5 個。
|
||||||
|
切太細會變成逐句記錄(顯著度全部一樣低),切太粗會讓一則記憶塞三件事(去重時分不開)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七項
|
||||||
|
|
||||||
|
| 項 | 寫法 | 常見的錯 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| 故事內時間 | 換算成西元日期 `YYYY-MM-DD`,填在候選的 `first_seen` | 寫匯入日期 |
|
||||||
|
| 地點 | 原作的說法(「第 74 層迷宮區」) | 寫「某個地方」 |
|
||||||
|
| 在場人物 | 照正名表的 `name`,一字不差 | 用簡稱或別名 |
|
||||||
|
| 發生什麼 | 一句話 | 寫成劇情摘要三段 |
|
||||||
|
| **他**做了什麼 | 動作,不是心情 | 「他感到憤怒」——那是旁白 |
|
||||||
|
| 他當下情緒 | 十二情緒的鍵 | 硬塞一個「複雜」 |
|
||||||
|
| 他說過的原句 | **照抄**,一到兩句 | 改寫得比較順口 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 知情層級(`know_level`)
|
||||||
|
|
||||||
|
這一欄決定那則記憶能不能被他當成自己的事講出來。
|
||||||
|
|
||||||
|
| 值 | 意思 | 能寫成什麼 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `did` | 我做的 | `event`,可以第一人稱斷言 |
|
||||||
|
| `saw` | 我看到的 | `event`,可以講但要是「我看到」 |
|
||||||
|
| `told` | 別人告訴我的 | `event`,講的時候要帶出處(誰告訴我的) |
|
||||||
|
| `later` | 事後才知道 | `event`,不可以講成「當時我就知道」 |
|
||||||
|
| `none` | 他不在場也沒人告訴他 | **只能 `canon`**,或寫進別人的關係欄 |
|
||||||
|
|
||||||
|
`none` 是這整份流程唯一真正危險的一格。作者寫給讀者看的資訊
|
||||||
|
(別人的內心話、他不在場那一幕的細節)看起來跟他的記憶長得一模一樣,
|
||||||
|
一旦寫成 `event`,他之後會拿它回答問題,而且**沒有任何機械檢查得出來**。
|
||||||
|
|
||||||
|
判斷不出來的時候記 `none`。少一則記憶的代價比多一則幻覺低得多。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 日期怎麼標(`date_source`)
|
||||||
|
|
||||||
|
換算成西元日期換來一個風險:猜出來的日期看起來跟原作明寫的一樣。
|
||||||
|
|
||||||
|
| 值 | 什麼時候用 |
|
||||||
|
| :-- | :-- |
|
||||||
|
| `canon` | 原作明寫(SAO 開服 2022-11-06) |
|
||||||
|
| `derived` | 由明寫的日期推算(開服後第 N 天) |
|
||||||
|
| `guess` | 只抓得到大概(某卷「大約半年後」) |
|
||||||
|
|
||||||
|
`guess` 的記憶在回答日期問題時走模糊態界線:**可以說不確定、可以問,不可以斷言**。
|
||||||
|
|
||||||
|
寫進長期記憶時,候選的 `first_seen` 會被搬到 `happened_at`(劇情時間),
|
||||||
|
而檔案的 `first_seen`/`last_seen` 一律是**匯入當天**。那兩欄是記憶的新鮮度時鐘,
|
||||||
|
不是劇情時間——混在一起的話,2024 年劇情的記憶匯進去的那一秒就已經想不起來了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 顯著度怎麼給
|
||||||
|
|
||||||
|
配額由 `novel init --quota` 定,預設 `90+ 最多 5 則、80-89 最多 20 則`。
|
||||||
|
超過的會被 `novel merge` 往下壓,但**先由你自己把關**比事後被壓好。
|
||||||
|
|
||||||
|
判準只有一個:**這件事之後他變了嗎**。
|
||||||
|
|
||||||
|
| 分數 | 什麼算 |
|
||||||
|
| :-- | :-- |
|
||||||
|
| 90+ | 改變他的事。整部作品只有幾件 |
|
||||||
|
| 80-89 | 他會主動提起的事 |
|
||||||
|
| 60-79 | 被問到會想起來的事 |
|
||||||
|
| 40-59 | 背景,構成他的世界但不會主動講 |
|
||||||
|
| 40 以下 | 別寫。寫了只會擠掉重要的 |
|
||||||
|
|
||||||
|
死亡、承諾、失去、第一次見面通常在上面兩層。
|
||||||
|
「打贏了一場戰鬥」多半是 60-79——那部作品裡他打贏過幾百場。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `type` 怎麼選
|
||||||
|
|
||||||
|
| type | 什麼算 |
|
||||||
|
| :-- | :-- |
|
||||||
|
| `canon` | 世界設定、規則、別人的事。**他不在場的事只能是這個** |
|
||||||
|
| `event` | 他經歷過的一件事 |
|
||||||
|
| `insight` | 他自己想通的事(要有原文依據,不是你替他總結) |
|
||||||
|
| `promise` | 他答應了什麼。**不可遺忘** |
|
||||||
|
| `boundary` | 他明確拒絕的事。**不可遺忘** |
|
||||||
|
|
||||||
|
`promise` 與 `boundary` 永遠不會糊掉,所以不要亂給——
|
||||||
|
給了之後那則記憶會一直清晰地留在他身上。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 語氣樣本與反應對照
|
||||||
|
|
||||||
|
跟記憶分開走,但在同一次讀章節時抽(回頭再讀一次很浪費)。
|
||||||
|
|
||||||
|
**語氣樣本**:他自己講的原句,照抄。要附**對誰說**與**什麼場合**——
|
||||||
|
同一個人在戰鬥裡跟在餐桌上句子的形狀不一樣,少了這兩個欄位就對不上語氣層。
|
||||||
|
|
||||||
|
**反應對照**:`事件 → 他做了什麼`。
|
||||||
|
不要記「他感到什麼」——情緒要演出來,寫成旁白就白抽了。
|
||||||
|
|
||||||
|
掩飾模式單獨記(嘴硬、換話題、講笑話)。那是人味的來源,不是情緒值。
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 開場問答(匯入前一定要問完)
|
||||||
|
|
||||||
|
這四題**不要猜、不要用預設值帶過**。
|
||||||
|
|
||||||
|
**譯名版本不在裡面,那一題不要問。** 問他「這批是角川還是東販」是要他猜——
|
||||||
|
他手上那批原文寫的是什麼字,讀一次就知道了。所以正名表改成**掃出來給他確認**
|
||||||
|
(見下面「正名表怎麼來」)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 匯給哪個人格
|
||||||
|
|
||||||
|
先跑 `persona.mjs list` 把可用的人格列給他看,不要用猜的。
|
||||||
|
已經載入一個人格的話就是它——**不要為了匯入而換人格**,那是他的決定。
|
||||||
|
|
||||||
|
要匯給別的人格:先 `release` 再 `load`,兩個動作都要他點頭。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 作品名
|
||||||
|
|
||||||
|
要完整書名(含卷次)。這會寫進每一則記憶的 `source`,之後追出處靠它。
|
||||||
|
|
||||||
|
一次匯多卷時,`--work` 的 slug 用**作品**而不是單卷——
|
||||||
|
同一部作品的去重合併要在同一個工作區裡才做得到。
|
||||||
|
|
||||||
|
## 3. 章節檔在哪、什麼編碼
|
||||||
|
|
||||||
|
要的是:目錄路徑、檔名規則(怎麼排序)、編碼。
|
||||||
|
|
||||||
|
- 編碼不確定就自己驗:讀前幾百個位元組看有沒有亂碼,比問還快。
|
||||||
|
- 常見的是 UTF-8 與 Big5;簡體來源多半是 GB18030。
|
||||||
|
- **一個檔一章**與**整本一個檔**的處理方式不同,要問清楚是哪一種。
|
||||||
|
整本一個檔的話先問他章節標題長什麼樣(用來切章)。
|
||||||
|
|
||||||
|
## 4. 這次要匯哪幾章
|
||||||
|
|
||||||
|
**不要預設「全部」**。一次匯完一整部作品是幾百則記憶,
|
||||||
|
出錯的時候分不出是哪一章的問題。
|
||||||
|
|
||||||
|
建議的講法:先匯 2 到 3 章,跑完給他看結果,確認視角與顯著度沒問題再往下。
|
||||||
|
他要一次全匯也可以,但要先講一句「出錯會很難查」,並且確認備份做了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 問完之後
|
||||||
|
|
||||||
|
1. `export`(`persona-transfer`)留一份備份——匯入是大量寫入,要回得去。
|
||||||
|
2. `novel init --work "<書名>" --slug <work-slug>` 建工作區。
|
||||||
|
3. 進正名表(下一節)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 正名表怎麼來:掃出來,他只做確認
|
||||||
|
|
||||||
|
**不要請他打一張表。** 他不會記得那本書裡「閃光」出現過幾次,
|
||||||
|
但那件事機器數得出來。所以順序是:先掃,再讓他確認。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
novel scan --work <slug> --dir <章節目錄> --session <id>
|
||||||
|
novel name review --work <slug> --session <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
`scan` 抽人名候選(對話歸屬、敬稱結尾、片假名、高頻詞),每個候選附上
|
||||||
|
**出現次數**與**一到兩行上下文**,並且猜一個關係節點,分成四種信心度:
|
||||||
|
|
||||||
|
| 信心度 | 意思 | 怎麼處理 |
|
||||||
|
| :-- | :-- | :-- |
|
||||||
|
| `exact` | 字面等於某個關係節點的 `name` | `--accept-exact` 一次收下,沒有判斷空間 |
|
||||||
|
| `alias` | 等於節點的標籤或備註裡出現過的別名 | 給他看一眼就收 |
|
||||||
|
| `fuzzy` | 部分重疊(共用兩個以上的字) | **一定要他確認**,這格最容易錯 |
|
||||||
|
| `unknown` | 猜不到 | 新人物就先 `relation node`;不是人名就 `name ignore` |
|
||||||
|
|
||||||
|
要給他看的是**上下文那一兩行**,不是候選清單本身。
|
||||||
|
「這個詞出現 41 次,其中一句是『閃光又衝出去了』」——他一眼就判得出來;
|
||||||
|
只給他一個「閃光」,他要回頭翻書。
|
||||||
|
|
||||||
|
`ignore` 是「確認過這不是人名」的清單。地名、招式名、系統詞擋不完,
|
||||||
|
所以 ignore 過的下次掃就不再出現——不然每加一章就要重看同一批。
|
||||||
|
|
||||||
|
補漏用手動:`novel name add --from "<字面>" --to "<節點 name>"`。
|
||||||
|
|
||||||
|
**正名沒確認完不要往下走**:`novel candidate add` 遇到 `about` 裡有未確認的 token 會擋下來。
|
||||||
|
那不是刁難——錯的名字寫進 `about`,要等 `relation doctor` 才發現,那時候整批要重跑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 中途發現問題怎麼辦
|
||||||
|
|
||||||
|
| 發現 | 怎麼處理 |
|
||||||
|
| :-- | :-- |
|
||||||
|
| 正名表確認錯了 | 改 `names.json` 的 `map`,已經 `candidate add` 的那幾章重跑,不要手改候選檔 |
|
||||||
|
| 掃出來一堆不是人名的詞 | 正常。`name ignore` 掉,下次掃不會再問 |
|
||||||
|
| 同一個角色有好幾種寫法 | 全部都要進 `map` 指到同一個節點。網路譯本前後不一致很常見 |
|
||||||
|
| 編碼判斷錯了 | 停下來重讀。亂碎的字會讓在場判斷失準,那個錯不會浮出來 |
|
||||||
|
| 章節順序排錯 | `first_seen` 會錯,時間軸就錯了。重排再重跑那幾章 |
|
||||||
|
| 他中途說「不要匯了」 | 工作區留著不刪(`memory/import/<slug>/`),還沒 `novel write` 就不會污染記憶 |
|
||||||
@@ -23,6 +23,8 @@ description: 人格編號與 Gitea 儲存:指派人格編號(英文名全大
|
|||||||
- 中文/日文名字要先轉**羅馬拼音**:由你提議拼法(`亞絲娜 → Asuna`、`沈宇 → Shen Yu`),
|
- 中文/日文名字要先轉**羅馬拼音**:由你提議拼法(`亞絲娜 → Asuna`、`沈宇 → Shen Yu`),
|
||||||
**拿給使用者確認再送出**——拼錯了會變成一個很難改的編號。
|
**拿給使用者確認再送出**——拼錯了會變成一個很難改的編號。
|
||||||
- 查下一個可用編號:`code next --romaji Asuna --session <id>`。
|
- 查下一個可用編號:`code next --romaji Asuna --session <id>`。
|
||||||
|
發號前會先問遠端 Gitea 已經用掉哪些編號(本機看不到別台機器發過的號);
|
||||||
|
Gitea 連不上時照樣發得出來,但輸出會明講「只對過本機」——那就有撞號的風險,要轉告使用者。
|
||||||
|
|
||||||
## 資料放哪裡(依更新頻率切)
|
## 資料放哪裡(依更新頻率切)
|
||||||
|
|
||||||
@@ -85,13 +87,31 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" code assign \
|
|||||||
|
|
||||||
### 在另一台機器接續同一個人格
|
### 在另一台機器接續同一個人格
|
||||||
|
|
||||||
在新機器上設好 `GITEA_HOST`/`GITEA_TOKEN`,然後:
|
在新機器上設好 `GITEA_HOST`/`GITEA_TOKEN`。**本機已經有這個人格**(只是舊了):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" load --persona <編號> --session <PERSONA_SESSION>
|
||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync pull --session <PERSONA_SESSION>
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync pull --session <PERSONA_SESSION>
|
||||||
```
|
```
|
||||||
|
|
||||||
(人格目錄還不存在的話,先用 `/jsc-persona:persona-transfer` 匯入一份,或手動 clone 存取庫。)
|
**本機還沒有這個人格**(全新的機器):`sync` 的每個動作都要求「先載入那個人格」,
|
||||||
|
而本機沒有它就 load 不了它,所以走 `clone`——它只驗 session,不驗 host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 遠端有哪些人格、哪些本機還沒有
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" clone --session <PERSONA_SESSION>
|
||||||
|
|
||||||
|
# 2) 挑一個拉回來(存取庫名稱就是編號)
|
||||||
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" clone --code ASUNA-01 --session <PERSONA_SESSION>
|
||||||
|
```
|
||||||
|
|
||||||
|
拉回來的是**完整的人格**:Wiki 區帶回身分與長期記憶,檔案區帶回情緒與短期記憶,
|
||||||
|
拉完自動重建長期記憶索引與關係圖。沒帶回來的只有鎖與租約那類執行期狀態——那本來就該由這台機器自己產生。
|
||||||
|
|
||||||
|
- 本機已經有同名人格時**預設不覆蓋**。真的要以遠端為準才加 `--force`(本機沒推上去的改動會不見),
|
||||||
|
想並存兩份就用 `--persona <別的目錄名>`。
|
||||||
|
- 加 `--load` 可以拉完直接載入開聊。
|
||||||
|
- 拉回來的東西沒有 `IDENTITY.md`(人格的最低要件)就會中止並清掉半成品,不留半個人格在本機。
|
||||||
|
|
||||||
## 衝突
|
## 衝突
|
||||||
|
|
||||||
@@ -104,6 +124,18 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sync pull --session <PERSONA_SE
|
|||||||
- 以 **本機** 為準 → `sync push`(會蓋掉遠端)
|
- 以 **本機** 為準 → `sync push`(會蓋掉遠端)
|
||||||
3. **不要自己選**。記憶被蓋掉是不可逆的。
|
3. **不要自己選**。記憶被蓋掉是不可逆的。
|
||||||
|
|
||||||
|
### push 撞到別台機器時是本機贏——但會記帳
|
||||||
|
|
||||||
|
`sync push`(包含每輪對話後的背景推送)遇到遠端比較新時,會**以本機為準覆蓋遠端**,
|
||||||
|
因為本機的工作副本才是這台機器的真相來源。這條路可以走,但不會安靜地走:
|
||||||
|
|
||||||
|
- 回報「覆蓋了哪幾個檔案、上一版是哪個 commit」,並記進 `state/sync.json`。
|
||||||
|
- `sync status` 列得出來;下一輪的 `Stop` hook 會把它講給使用者聽一次。
|
||||||
|
- 被蓋掉的內容還在 clone 的歷史裡:
|
||||||
|
`git -C <人格>/.sync/files show <上一版 sha>:<檔案>`。
|
||||||
|
|
||||||
|
看到這種回報就**告訴使用者**:另一台機器可能正在用同一個人格。要救回舊版就從上面那行取出來。
|
||||||
|
|
||||||
## 殘留的 git 鎖
|
## 殘留的 git 鎖
|
||||||
|
|
||||||
`push`/`pull` 跑到一半被中斷(sub agent 被砍、視窗關掉)會在 clone 裡留下 `.git/index.lock`,
|
`push`/`pull` 跑到一半被中斷(sub agent 被砍、視窗關掉)會在 clone 裡留下 `.git/index.lock`,
|
||||||
@@ -117,6 +149,7 @@ sleeper 連自己的人格目錄都不能用 shell 寫,那條路本來就是
|
|||||||
## 邊界
|
## 邊界
|
||||||
|
|
||||||
- 只能同步「本 session 目前載入的人格」——跨人格同步等於跨人格讀取,會被 hook 擋下。
|
- 只能同步「本 session 目前載入的人格」——跨人格同步等於跨人格讀取,會被 hook 擋下。
|
||||||
|
唯一的例外是 `clone`:它只會**新增本機還沒有的人格**,讀不到任何既有人格的資料,所以不受此限。
|
||||||
- guest(`persona-guest` sub agent)不能同步,它對人格檔案唯讀。
|
- guest(`persona-guest` sub agent)不能同步,它對人格檔案唯讀。
|
||||||
- `.sync/` 是兩區的 git clone 快取,**不要手動編輯**;砍掉它不會掉資料,下次 push 會重新 clone。
|
- `.sync/` 是兩區的 git clone 快取,**不要手動編輯**;砍掉它不會掉資料,下次 push 會重新 clone。
|
||||||
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
- 所有面向使用者的輸出使用**繁體中文(台灣用語)**、UTF-8 無亂碼。
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ description: 匯出與匯入人格:把一個人格(身分、靈魂、十二
|
|||||||
bundle 是**單一 JSON 檔**(可 gzip),不依賴任何外部工具,複製到隨身碟、貼進 git、
|
bundle 是**單一 JSON 檔**(可 gzip),不依賴任何外部工具,複製到隨身碟、貼進 git、
|
||||||
傳給另一台機器都可以。
|
傳給另一台機器都可以。
|
||||||
|
|
||||||
|
> 只是要在另一台機器接續同一個人格、而且兩邊都連得到 Gitea 的話,**不必經過檔案**:
|
||||||
|
> 用 `/jsc-persona:persona-sync` 的 `clone --code <編號>` 直接從存取庫拉一份回來。
|
||||||
|
> 這裡的 bundle 是給離線、換助理、或想留一份快照的情境。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## bundle 帶走什麼、不帶什麼
|
## bundle 帶走什麼、不帶什麼
|
||||||
@@ -18,14 +22,38 @@ bundle 是**單一 JSON 檔**(可 gzip),不依賴任何外部工具,複
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `IDENTITY.md` / `SOUL.md` / `AGENTS.md` / `USER.md` | `state/lock.json`、`state/guests.json`(載入鎖與 guest 租約屬於「那台機器的那個程序」) |
|
| `IDENTITY.md` / `SOUL.md` / `AGENTS.md` / `USER.md` | `state/lock.json`、`state/guests.json`(載入鎖與 guest 租約屬於「那台機器的那個程序」) |
|
||||||
| `state/config.json`、`state/emotion.json`(十二情緒的 levels/baseline/半衰期) | `journal/`(原始逐字稿:量大且最私密,要帶請明確加 `--with-journal`) |
|
| `state/config.json`、`state/emotion.json`(十二情緒的 levels/baseline/半衰期) | `journal/`(原始逐字稿:量大且最私密,要帶請明確加 `--with-journal`) |
|
||||||
| `state/inner.jsonl`(心裡話)、`state/said.jsonl`(說過的話) | 任何**其他人格**的資料——一個 bundle 只有一個人格 |
|
| `state/mood.json`(當日心情底色)、`state/loops.json`(懸著的事)、`state/probe.jsonl`(試探紀錄) | 任何**其他人格**的資料——一個 bundle 只有一個人格 |
|
||||||
|
| `state/inner.jsonl`(心裡話)、`state/said.jsonl`(說過的話) | |
|
||||||
| `memory/`(短期、長期一則一檔、INDEX、inbox) | |
|
| `memory/`(短期、長期一則一檔、INDEX、inbox) | |
|
||||||
| `mindmap/`(心智圖與思維導圖)、`relations/`(關係圖) | |
|
| `mindmap/`(心智圖與思維導圖)、`relations/`(關係圖) | |
|
||||||
|
|
||||||
|
規則很簡單:**除了鎖與租約,人格目錄裡的東西都帶走**。所以之後新增的狀態檔不必回頭改這張表——
|
||||||
|
`state/lock.json` 與 `state/guests.json` 是唯一兩個明文排除的(它們描述的是「那台機器的那個程序」,
|
||||||
|
換一台機器就是假的)。
|
||||||
|
|
||||||
檔案裡有 `checksum`(sha256),匯入時會驗;對不上就是被改過或損毀。
|
檔案裡有 `checksum`(sha256),匯入時會驗;對不上就是被改過或損毀。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## bundle 版本(目前 v2)
|
||||||
|
|
||||||
|
| 版本 | 差在哪 |
|
||||||
|
| --- | --- |
|
||||||
|
| v1 | 長期記憶只有 `salience`;內文不分層;沒有 `mood.json`/`loops.json`/`probe.jsonl` |
|
||||||
|
| **v2** | 長期記憶多了 `strength`(回想強度)與 `when`/`where`/`mood`(情境索引),內文分「主旨/細節」兩層;狀態多了當日心情底色、未完事項、試探紀錄 |
|
||||||
|
|
||||||
|
- **舊的 v1 bundle 照樣吃得下**:`import` 寫完檔案會**就地跑一次 migration** 補齊欄位並切分內文,
|
||||||
|
所以匯入之後這台機器上只有一種格式,不必到處判版本。
|
||||||
|
- **遷移失敗不會讓整個匯入失敗**(INDEX 還是要重建),代價是它也**不會出現在人看的輸出裡**——
|
||||||
|
只有 `--json` 的 `migrated` 看得到統計,是 `null` 就代表那次遷移出過錯。
|
||||||
|
匯進來卻沒升格式的人格,它的老記憶進不了衰減與模糊態,等於最重要的那批記憶反而沒效果,
|
||||||
|
所以**匯入舊 bundle 之後補跑一次**比較保險:`migrate --session <PERSONA_SESSION> --dry-run`
|
||||||
|
看還有沒有東西要動,有就跑一次不帶 `--dry-run` 的。
|
||||||
|
- **比本版新的 bundle 會被擋下**(`validateBundle` 直接判不合法):未來的欄位沒有辦法猜,
|
||||||
|
硬吃會把不認得的資料寫進人格目錄。這種時候該更新 plugin,不是加 `--force`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 匯出
|
## 匯出
|
||||||
|
|
||||||
**只能匯出「本 session 目前載入的那個人格」**——這是隔離規則的一部分,
|
**只能匯出「本 session 目前載入的那個人格」**——這是隔離規則的一部分,
|
||||||
@@ -71,7 +99,9 @@ checksum 對不上會被擋下;要硬吃得加 `--force`,並**主動告訴
|
|||||||
2. **匯出後回報實際路徑與大小**,還有帶了幾則長期記憶——他要拿去搬家,得知道搬了什麼。
|
2. **匯出後回報實際路徑與大小**,還有帶了幾則長期記憶——他要拿去搬家,得知道搬了什麼。
|
||||||
3. **匯入前先 `list`**,看目標 slug 是否已存在:存在就給他「換名匯入 / 覆寫」兩個選擇,
|
3. **匯入前先 `list`**,看目標 slug 是否已存在:存在就給他「換名匯入 / 覆寫」兩個選擇,
|
||||||
不要自己決定 `--force`。
|
不要自己決定 `--force`。
|
||||||
4. 匯入後告訴他 `/jsc-persona:persona-chat <slug>` 就能開始聊。
|
4. **匯入的是 v1 bundle 就講一句**:長期記憶已經升成新格式(補回想強度、切主旨/細節)。
|
||||||
|
這會改變那個人格「記得多清楚」的行為,使用者有權知道搬過來的不完全是原樣。
|
||||||
|
5. 匯入後告訴他 `/jsc-persona:persona-chat <slug>` 就能開始聊。
|
||||||
|
|
||||||
## 邊界
|
## 邊界
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user