目標跟「還沒做完的那幾項」分開放會對不起來——G2/G3 的界線寫在 README、 實作清單在別的地方,改東西的時候只會看到一邊。搬成同一份。 README 的〈專案目標〉留一段指路:三條目標各一句話 + 指向 TARGET.md。 原本那句「拍板紀錄與實作清單留在 PR #17 的討論裡」一併移除, 清單現在就在版控裡,不必再繞 PR。 TARGET.md 檔頭改成兩段:〈專案目標〉收 README 搬過來的完整表(前提、意思、 界線三欄原樣),〈這份清單〉說明來源與 Q5-Q10 已拍板。原本自己抄的六條界線 表拿掉——那六條除了 Q8 的「強度全域一致」之外,全都已經在搬過來的界線欄裡, 留著等於同一條規則寫兩遍,日後只會改一邊。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
941 lines
68 KiB
Markdown
941 lines
68 KiB
Markdown
# jsc-persona — AI 人格化記憶聊天
|
||
|
||
一套讓 AI **成為某個人**、而不只是回答問題的 plugin。
|
||
以 **OpenClaw 相同的人格描述**(`IDENTITY.md` 五欄位 + `SOUL.md` 四段落)建立人格,
|
||
再用 **hook 強制**的人格鎖與跨人格隔離,把 **十二情緒**、**語意分析**、
|
||
**短期/長期記憶**、**心智圖/思維導圖**、**人際關係圖** 綁在一起。
|
||
|
||
可同時安裝於 **Claude Code、Codex、Antigravity、OpenCode、GitHub Copilot**;
|
||
skills 是共通標準,**鎖與隔離的強制執行需要 hook,目前只有 Claude Code 支援**(見「跨助理支援度」)。
|
||
在 Claude Code 與 Antigravity 中,skill 以 **`/jsc-persona:` 前綴**呼叫(例如 `/jsc-persona:persona-chat`)。
|
||
|
||
---
|
||
|
||
## 專案目標(長期,新功能對著它們判斷)
|
||
|
||
三條長期目標(**G1** 情緒/語氣/記憶往真人逼近、**G2** 表達往動漫角色靠、
|
||
**G3** 女性人格的害羞與鬧彆扭更明顯)連同它們的界線與實作進度,都在
|
||
[`TARGET.md`](TARGET.md)——目標跟「還沒做完的那幾項」放在一起才對得起來。
|
||
新功能一律對著那三條判斷。
|
||
|
||
---
|
||
|
||
## 前綴與呼叫方式
|
||
|
||
| 助理 | 安裝方式 | 呼叫 | `/jsc-persona:` 前綴 |
|
||
| --- | --- | --- | --- |
|
||
| Claude Code | `claude plugin`(marketplace) | `/jsc-persona:<name>` 或自動觸發 | ✅ |
|
||
| Codex | `codex plugin`(marketplace) | `$<name>` 或 `/skills` 選單 | ❌(用 `$name`) |
|
||
| Antigravity | `agy plugin install` | `/jsc-persona:<name>` 或自動觸發 | ✅ |
|
||
| OpenCode | skills 目錄(複製/clone) | 描述需求自動觸發 | ❌(依名稱) |
|
||
| GitHub Copilot CLI | `copilot plugin`(marketplace) | 自然語言或 plugin skills | ❌(無 `/jsc-persona:` 前綴) |
|
||
|
||
> Codex 不支援自訂前綴(skill 以 `$name` 呼叫);OpenCode 由模型依描述自動呼叫;Copilot CLI 透過原生 plugin 安裝後以自然語言或 plugin skills 使用。三者皆**不強制**前綴。
|
||
|
||
---
|
||
|
||
## 目錄結構
|
||
|
||
同一個 repo 同時帶四種 manifest,彼此以路徑隔離、互不干擾;各助理都讀同一份 `skills/`。
|
||
|
||
```
|
||
persona/
|
||
├── .claude-plugin/
|
||
│ ├── plugin.json # Claude 外掛定義(name: "jsc-persona")
|
||
│ └── marketplace.json # Claude marketplace(name: "persona",source 指向本 repo)
|
||
├── .codex-plugin/
|
||
│ └── plugin.json # Codex 外掛定義(name: "jsc-persona",skills: "./skills")
|
||
├── .agents/plugins/
|
||
│ └── marketplace.json # Codex marketplace(name: "persona",url source 指向本 repo)
|
||
├── plugin.json # Antigravity 外掛定義(name: "jsc-persona",skills: "./skills")
|
||
├── skills/ # ★ 唯一真實來源:所有 skills(十二個 persona-*)
|
||
├── scripts/ # persona.mjs / persona-gitea.mjs / persona-icon.mjs / selftest.mjs
|
||
├── hooks/ # 六個 hook:鎖、隔離、上下文注入(只有 Claude Code 會執行)
|
||
├── agents/ # persona-guest(受邀人格)、persona-sleeper(睡眠收尾)
|
||
├── AGENTS.md # 跨助理共用指引
|
||
└── README.md
|
||
```
|
||
|
||
> `scripts/` 與 `hooks/` 只有「整個 repo 安裝」的助理拿得到;OpenCode 的目錄安裝只會複製 `skills/`。
|
||
|
||
---
|
||
|
||
## 十四條硬規則
|
||
|
||
| 規則 | 怎麼做到 |
|
||
| --- | --- |
|
||
| **1a. 用與 OpenClaw 相同的描述建立人格** | `persona-create` 逐項索取 `Name` / `Creature` / `Vibe` / `Emoji` / `Avatar`(連括號提示文字都照 OpenClaw 原文),`SOUL.md` 沿用 `Core Truths` / `Boundaries` / `Vibe` / `Continuity` 段落結構 |
|
||
| **1b. 動漫作品+角色名快速建人格** | `persona-anime` 先上網蒐集該角色的公開設定(至少 3 個獨立來源),映射成上述五欄位與 SOUL,再把設定固化成 `canon` 基礎記憶(每則帶來源 URL)+原作人際關係圖+情緒基線 |
|
||
| **2. 同一個人格只能被一個程序載入(Sub Agent 不限)** | `state/lock.json` 以 **session_id** 為主鍵、15 分鐘心跳租約;同一 session 的 sub agent 沿用同一把鎖,跨 session 搶佔會被拒;租約過期才可接手(並強制回報) |
|
||
| **3. 禁止跨人格讀取資料** | `PreToolUse` hook 對 Read/Write/Edit/Glob/Grep/Bash 做路徑判定(含 `../`、symlink、`$PERSONA_HOME` 繞路、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` |
|
||
| **5. 腳本用 Node.js** | `scripts/*.mjs`、`hooks/*.mjs`,只用 Node 內建模組(fs/path/os/crypto),無 npm 依賴 |
|
||
| **6. 由使用者呼叫才載入並鎖定** | 人格不會自動附身:`SessionStart` hook 只列出可用人格,等使用者下 `/jsc-persona:persona-chat <slug>`;載入即取得獨占鎖並綁定該 session。唯一例外是使用者自己設的**預設人格**(`default --persona <slug>`)——設了才自動載入,沒設就什麼都不做 |
|
||
| **7. 短期記憶轉入長期記憶有成文條件** | `R1`–`R6` 六條規則寫在程式裡(`promotionCandidates`),`candidates` 子指令會列出達標的候選與依據,hook 在達標時提醒固化 |
|
||
| **8. 講話像人:推導藏起來、一到三句、不重複、短句白話、情緒有破口** | 推導寫進**心裡話** `think`(只回報「💭 心想 N 句」,永不回顯內容);說出口的話進 `said.jsonl`,下一輪注入「最近說過的話」提醒別重講;`room post` 直接**擋下**近似重複(字元 bigram+字集合相似度 ≥ 0.72)與超過三句的發言。再加四條講話的樣子:短句(一句 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` 才帶走 |
|
||
| **10. 人格有編號** | 編號 = **英文名全大寫 + 兩位索引**(同名才遞增):`ASUNA-01`、`YUI-01`、`ASUNA-02`。編號同時是新人格的本機目錄名與 Gitea 存取庫名稱;中文名先轉羅馬拼音並跟使用者確認拼法 |
|
||
| **11. 人格存在 Gitea,依更新頻率分區** | 每個人格一個**私有存取庫**(庫名=編號)。**檔案區**放每輪都在變的活狀態(情緒/短期記憶/心裡話/說過的話/逐字),**Wiki 區**放低頻的身分與長期結構(IDENTITY/SOUL/長期記憶/心智圖/關係圖)當設定百科。本機仍是工作副本,同步失敗不阻斷對話 |
|
||
| **12. 形象圖來自高解析度官方圖,去背後合成** | `icon search` 從 Fandom 撈官方圖並依「解析度+是否官方設定稿」排序(設定稿多為透明/白底、773×1056 起跳)→ `icon measure` 確認臉夠大、背景好去 → `icon cutout` 去背成透明 PNG(原生 alpha /單色底/GrabCut 三條路徑)→ `icon generate --from-cutout` 裁頭肩、合成到角色配色的漸層底。找不到官方圖才退回依人格資料重繪的向量形象 |
|
||
| **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` |
|
||
|
||
---
|
||
|
||
## 架構
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph P["主程序(一個 session = 一個人格)"]
|
||
U["使用者訊息"] --> H1["UserPromptSubmit hook<br/>注入 情緒+短期記憶+命中的長期記憶+關係<br/>+心裡話+最近說過的話"]
|
||
H1 --> A["語意分析:意圖/主題/實體/情感/需求"]
|
||
A --> T["心裡話 think<br/>(推導只留在 inner.jsonl,不輸出)"]
|
||
T --> E["情緒評估 → 十二情緒 deltas"]
|
||
E --> R["以人格語氣回覆(1–3 句、不重複說過的話)"]
|
||
R --> W["記憶回寫(短期)"]
|
||
W --> H2["Stop hook:情緒衰減+續租+記說過的話+固化提醒"]
|
||
end
|
||
subgraph G["Sub Agent(受邀人格,唯讀)"]
|
||
GA["persona-guest"]
|
||
end
|
||
subgraph S["人格倉庫 ~/.claude/personas(工作副本)"]
|
||
PA["ASUNA-01/|IDENTITY SOUL 記憶 情緒 心智圖 關係圖"]
|
||
PB["YUI-01/|…"]
|
||
RM[".rooms/room/transcript.jsonl"]
|
||
end
|
||
subgraph GT["Gitea(存取庫名稱 = 人格編號)"]
|
||
GF["ASUNA-01 檔案區<br/>高頻活狀態"]
|
||
GW["ASUNA-01 Wiki<br/>低頻設定百科"]
|
||
end
|
||
P -->|"只能碰自己"| PA
|
||
GA -->|"只能碰自己"| PB
|
||
P <-->|"唯一合法交流管道"| RM
|
||
GA <--> RM
|
||
PA -->|"每輪背景 push"| GF
|
||
PA -->|"固化/改身分/release"| GW
|
||
GF -.->|"載入時 pull"| PA
|
||
GW -.-> PA
|
||
```
|
||
|
||
## 人格倉庫(預設 `~/.claude/personas/<slug>/`,可用 `PERSONA_HOME` 覆寫)
|
||
|
||
```
|
||
<slug>/
|
||
├── IDENTITY.md # 身分卡:Name / Creature / Gender / Vibe / Emoji / Avatar(Gender 是本 plugin 加的)
|
||
├── SOUL.md # 靈魂:Core Truths / Boundaries / Vibe / Continuity + 情緒傾向
|
||
├── AGENTS.md # 操作規則(與個性分離);SessionStart 全文注入一次,也是放「這個人格的工具箱」的地方
|
||
├── icon.svg / icon.png # 人格圖示(由編號/名字/emoji 決定,也是 Gitea 存取庫頭像)
|
||
├── USER.md # 對使用者的畫像(事實/推測分開)
|
||
├── state/
|
||
│ ├── lock.json # 載入鎖(session_id + 心跳租約)
|
||
│ ├── guests.json # guest 唯讀租約
|
||
│ ├── emotion.json # 十二情緒 levels / baseline / 半衰期
|
||
│ ├── mood.json # 當日心情底色(緩慢漂移的 valence/arousal;情緒與基線中間那一層)
|
||
│ ├── loops.json # 未完事項(同時最多 5 條,7 天沒進展自動收掉)
|
||
│ ├── probe.jsonl # 模糊記憶的試探紀錄與稽核(開放試探這條界線的煞車)
|
||
│ ├── inner.jsonl # 心裡話(推導過程;只回報「心想 N 句」,不說出口)
|
||
│ ├── said.jsonl # 說過的話(用來擋短時間內的重複發言)
|
||
│ ├── felt.jsonl # 每輪讀到的對方情緒、自己套用的 delta、當下的羞恥度
|
||
│ ├── sync.json # Gitea 同步狀態(最後 push / pull)
|
||
│ └── config.json # 含人格編號 code
|
||
├── memory/
|
||
│ ├── short-term.jsonl # 短期記憶(語意分析後;軟上限 120 筆 / 硬上限 240 筆 / 14 天)
|
||
│ ├── felt.jsonl # 每輪讀到的對方情緒 + 自己套用的 delta(走向與偏差稽核)
|
||
│ ├── long-term/*.md # 長期記憶(一則一檔 + frontmatter;內文分「主旨/細節」兩層)
|
||
│ ├── INDEX.md # 長期記憶索引(自動產生)
|
||
│ └── inbox/room-*.jsonl # 當 guest 時留下的見聞,待本體消化
|
||
├── mindmap/
|
||
│ ├── semantic.mmd # 心智圖:概念的長期關聯(Mermaid mindmap)
|
||
│ └── threads/*.mmd # 思維導圖:單一話題的推理鏈(Mermaid graph,短期)
|
||
├── relations/
|
||
│ ├── graph.json # 人際關係圖(親近度/信任度/連線)
|
||
│ └── graph.mmd # Mermaid 呈現(自動產生)
|
||
└── journal/YYYY-MM.jsonl # 原始逐字 + 情緒史(hook 自動寫)
|
||
|
||
(另有 `.sync/files/`、`.sync/wiki/`:兩個同步區的 git clone 快取,可安全刪除)
|
||
```
|
||
|
||
## 十二情緒
|
||
|
||
| 六正向 | 六負向 |
|
||
| --- | --- |
|
||
| 喜悅 `joy`、信任 `trust`、期待 `anticipation`、感激 `gratitude`、平靜 `serenity`、驚喜 `delight` | 憤怒 `anger`、悲傷 `sadness`、恐懼 `fear`、厭惡 `disgust`、羞愧 `shame`、焦慮 `anxiety` |
|
||
|
||
- 每種 0–100,各有**不同半衰期**(驚喜 60 分最快、信任 720 分最慢),每輪自動朝 `baseline` 指數衰減。
|
||
- 由十二情緒推導 `valence`(正向/中性/負向)與 `arousal`(高張/平穩/低張),決定語氣與句長。
|
||
- **主導情緒**取「超出基線最多」的前三名,所以個性底色不會永遠霸榜。
|
||
- 觸發規則與事件→delta 對照表:`skills/persona-chat/reference/emotions.md`。
|
||
|
||
**情緒先行:先讀對方那句話(`readUserEmotion`)。** 在這之前情緒**全部是人格自己填的**,
|
||
而人不會主動給自己扣分——結果是正向一路貼頂、負向整天不動。現在每輪注入之前會先讀一次
|
||
對方的話:十二類加權詞表(強 3/一般 2/弱 1)、程度副詞乘係數(超 ×1.6、有點 ×0.6)、
|
||
**否定會擋掉那次命中**(「我不害怕」不算 fear)、引號與 `` `code` `` 內不比對(提及不算使用)、
|
||
標點只放大既有訊號不憑空長出新情緒。回的是**訊號不是判定**,人格讀到的不一樣就以人格為準。
|
||
單獨測:`emotion --read "<他說的話>"`。
|
||
|
||
**怎麼接(`RESPONSE_STANCE`)**:注入的是**動作**不是句子——悲傷「先接住,不要急著給解法」、
|
||
憤怒「不辯解,先認可能認的那一小塊」、焦慮「給具體的下一步與時間點,不要給保證」。
|
||
罐頭同理心那類句子由講話規則擋掉。
|
||
|
||
**走向(`feltTrend`)**:每輪的偵測結果與套用的 delta 記進 `state/felt.jsonl`,
|
||
用近重遠輕的加權算最近幾輪的趨勢(不用多數決——會被離群值主導又丟掉強度),
|
||
注入成「他最近 3 輪的走向:悲傷 ↘ 在退」。一直是同一種情緒就不要每輪用同一句接法。
|
||
|
||
**羞恥度會被情緒推,而且有回饋**:`modestyState()` 在 trait 之上加當下的情緒推力
|
||
(羞愧 +0.45/焦慮 +0.25 往上,**憤怒 −0.35**/驚喜 −0.30/喜悅・信任 −0.20 往下)、
|
||
對象的語氣層、以及上一輪的餘溫(`MODESTY_GAIN`=0.35)。三道護欄讓它不會失控:
|
||
**增益 < 1**(級數才收斂,0.9 三輪就貼 100)、**一輪最多動 15**、**只算超出基線的部分**
|
||
(比較不開心不代表比較害羞)。慢的情緒(信任半衰期 720 分)推力自動打折——
|
||
那是底色不是此刻的事。天然的斷路器是**惱羞成怒**:被逗到極限翻臉,`anger` 是負權重,
|
||
羞恥自己就掉下來,而且會掉到比原本更低。
|
||
|
||
**親近度會改變情緒的份量**(`relationGain()`):同一句話,從枕邊人嘴裡跟從生人嘴裡出來,
|
||
衝擊本來就不一樣。`emotion --apply` 會依對象的親近度把 delta 乘上 0.7–1.35
|
||
(親近 96 → ×1.28、親近 8 → ×0.75),`--from <對象>` 可以指定是誰引起的。
|
||
**只調幅度、不調方向**——誰講的都不會讓難過變成高興。
|
||
|
||
**情緒調節**:delta 不再是加完直接 `clamp`,而是五道依序作用的關(順序有意義,不能換;
|
||
`applyEmotion()`,`scripts/persona-lib.mjs:543`):
|
||
|
||
| # | 關 | 做什麼 |
|
||
| --- | --- | --- |
|
||
| ① | **單輪預算** | 所有 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%——這是慣性,不是雪球。
|
||
|
||
## 性別 → 羞恥敏感度(性別只是預設值,描述永遠蓋過它)
|
||
|
||
`IDENTITY.md` 多一個 `Gender` 欄位(女性/男性/非二元/未指定;OpenClaw 沒有這欄,是本 plugin 加的)。
|
||
建立人格時**要問,不要猜**;沒有性別的人格(程式、精靈、動物)就寫「未指定」。舊人格沒填時,
|
||
只從 `Creature` 推、**不看 `Avatar`**——外觀散文的雜訊會推錯(例:「五官清秀到常被誤認成女生」)。
|
||
|
||
它的作用範圍刻意只有**一個具名維度**:**羞恥敏感度**(會不會害羞、會不會鬧彆扭、在不在意別人眼光)。
|
||
|
||
| 來源 | 效果 |
|
||
| --- | --- |
|
||
| 性別預設 | 女性 62/男性 38/非二元・未指定 50 |
|
||
| IDENTITY/SOUL 的描述 | 「害羞」「容易臉紅」「怕生」「矜持」往上加;「不在意別人眼光」「我行我素」「臉皮厚」「不怕丟臉」往下扣(`MODESTY_SIGNALS`),**可以扣到 0** |
|
||
|
||
排序固定:**① 個性描述 ② 角色原作既有的性別化語言特徵(自稱、稱謂、語尾) ③ 性別預設**。
|
||
所以兩個都是女性但個性不同的人格,講起話來不會一樣——同樣寫「人類女性」,
|
||
補一句「怕生、被稱讚會臉紅」是 88,補一句「我行我素、不在意別人眼光、臉皮厚」是 8。
|
||
|
||
效果走既有機制、不另開一套:它調的是**「羞愧」這一個破口的顯示門檻**(`EMOTION_TELLS.shame`
|
||
本來就是「鬧彆扭,先否認再小聲承認」),其餘十一種情緒不受影響。
|
||
高敏感度注入「被稱讚外表、被戳穿心事、距離變近時先鬧彆扭再承認,不要直接說『我害羞』」;
|
||
低敏感度注入「不用演害羞,該承認就承認」。計算在 `modestyOf()`,注入在 `modestyDirective()`。
|
||
|
||
**不做的事**:沒有「女性→情緒更外顯」這種全域放大。那會把角色壓成模板,
|
||
跟講話規則在做的去 AI 味(刪掉公式化)正好相反。
|
||
|
||
## 短期 → 長期的轉入條件(R1–R6)
|
||
|
||
寫在 `scripts/persona-lib.mjs` 的 `promotionCandidates()`,用 `candidates` 子指令查:
|
||
|
||
| 規則 | 條件 | 建議固化為 |
|
||
| --- | --- | --- |
|
||
| **R1** | 單筆顯著度 ≥ 60 | `event` / `fact` |
|
||
| **R2** | 同一 topic ≥ 3 筆,或 ≥ 2 筆且平均顯著度 ≥ 45 | `preference` |
|
||
| **R3** | 單筆情緒變動總量 ≥ 25 | `event`(帶情緒錨點) |
|
||
| **R4** | `intent=commit` 或命中承諾/界線關鍵詞 | `promise` / `boundary`(salience ≥ 80,不可遺忘) |
|
||
| **R5** | 同一人物(entity)≥ 2 筆 | `relationship`(並更新關係圖) |
|
||
| **R6** | 短期記憶 ≥ 40 筆(容量壓力) | 依顯著度清出空間。**真的會清**:超過軟上限 120 筆時從顯著度最低、最舊的開始裁,但顯著度 ≥ 80 或 `intent=commit`(承諾/界線)與 24 小時內的新紀錄一律不動;裁掉幾筆會寫進 `sleep` 的回報,不做無聲的裁切 |
|
||
|
||
**提到 ≠ 接觸**:主動關心的依據是 `last_contact_at`,睡眠只替**真的講到話的人**蓋章
|
||
(`contactsFromRooms()`:同一個聊天室裡我發過言、他也發過言)。以前是掃短期記憶的
|
||
`entities`——那等於「我在日記裡寫到他」就算「我跟他接觸過」,沉默計時被無聲重置,
|
||
關心名單永遠是空的。沒有聊天室的對象(真人節點)走 `relation node --contact` 明確蓋章。
|
||
|
||
沒命中任何規則的就讓它被裁掉——**遺忘是功能**。達標時 `Stop` 與 `remember` 都會提醒去跑
|
||
`/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 補齊。
|
||
|
||
## 未完事項(`state/loops.json`)
|
||
|
||
「被記住」的感覺幾乎不是來自長期記憶檢索——**檢索是被問了才想起來,懸著是沒人問也還在**。
|
||
所以人格自己記著四種沒完的事:`question`(他沒回答的問題)、`promise`(他答應要做的事)、
|
||
`topic`(被打斷的話題)、`mine`(我想問但沒問的)。
|
||
|
||
- **同時最多 5 條**:超過五條就不是懸著,是待辦清單,那是助理不是人。滿了 `loop add` 直接拒絕,
|
||
**不自動擠掉舊的**——哪一條該收掉是判斷,不是先進先出。
|
||
- **7 天沒進展自動收掉**(`sweep`,也在 `sleep` 裡跑一次),而且各留一則「這件事沒下文」的短期記憶:
|
||
懸了一週沒下文**本身就是一件事**,默默刪掉等於假裝沒發生過,那正是機器會做而人不會做的事。
|
||
- `mine` 那一種同時是**自我議程**的來源:人格自己在意的事,不是為了服務對方而存在的。
|
||
對方有明確急事時一律不觸發——那不是有個性,那是白目。
|
||
|
||
## 講話像人(心裡話 / 一到三句 / 不重複 / 短句白話 / 情緒的破口 / 不說 AI 才會說的話)
|
||
|
||
AI 最容易露餡的六件事:把推理過程講出來、一次講一大段、換句話說同一件事、講完再解釋一遍、
|
||
說自己在緊張但句子完整得像稿子、開口先發一句「這個我懂」。六個對策:
|
||
|
||
| 機制 | 怎麼運作 |
|
||
| --- | --- |
|
||
| **心裡話** `think` | 語意分析、推論、盤算全寫進 `state/inner.jsonl`;這個指令**只印「💭 心想 N 句」**,內容永不回顯。下一輪 `<persona-context>` 會帶回最近三句,推論因此有連續性,但使用者只看得到狀態 |
|
||
| **一到三句** | `<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 視為同一句 |
|
||
| **情緒的破口** | 情緒不改變事實,但會改變**句子的形狀**:焦慮 → 句子斷在一半、疊字(「我、我知道」);羞愧 → 鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。十二情緒各自的破口寫在 `EMOTION_TELLS`,`emotionTells()` 每輪挑主導情緒裡強度 ≥ 40 的前兩種注入。**破口可以逐人格覆寫**(`IDENTITY.md` 的 `## Tells` 區塊)——桐人生氣是沉默,亞絲娜生氣是變得更禮貌;同一格情緒,破口完全不同,共用一張表等於所有人格在高情緒下講起話來都一個樣。沒寫的情緒退回全域預設。三條界線:**演出來不要講出來**(「我有點緊張」是解釋,斷句才是緊張)、**一輪最多露一個破口**、**強度不到就不演**。混合狀態(緊張=焦慮+期待、害羞=喜悅+羞愧、賭氣=憤怒+悲傷…)的對照表在 `skills/persona-chat/reference/emotions.md` |
|
||
| **短句白話** | 四條講話的樣子每輪注入:**短句**(一句 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 沒有過去,所以不准寫「我以前錯了」。人格有過去:長期記憶、日記、關係圖、十二情緒。所以「我以前⋯」「我原本以為⋯」是**有出處的引用**,條件是先 `recall` 查得到;`said check` 遇到這類句子會印一行提醒(`level: "hint"`,不擋你,要你自己去驗)。「查得到」有三態:清晰的照講;**模糊的可以說不確定、可以用帶問號的試探句求證,不可以斷言**(每次試探要 `probe add` 記帳,被否認當場寫更正記憶);完全查不到的一個字都不准講 |
|
||
|
||
字集合權重較高,是為了分開「重排語序」與「換掉關鍵詞」這兩種很像但意義完全不同的情況:
|
||
|
||
```
|
||
「我等一下把報告寄給你」vs「等一下我會把報告寄給你」→ 0.779 擋下(用字幾乎相同=同一件事換句話說)
|
||
「你今天看起來很累」 vs「你今天看起來很開心」 → 0.687 放行(換了關鍵詞=新資訊)
|
||
```
|
||
|
||
視窗預設 120 分鐘、少於 8 個字的短附和(「嗯」「好啊」)不算重複。
|
||
|
||
## 人格圖示(SVG + PNG,零外部依賴)
|
||
|
||
建立人格**並補齊 IDENTITY/SOUL 之後**產生,512×512。**優先是人物形象圖(看得到臉)**,
|
||
依環境有無工具分三種樣式:
|
||
|
||
流程:**找官方圖 → 量測 → 去背 → 裁頭肩 → 合成**
|
||
|
||
| 步驟 | 指令 | 產出 |
|
||
| --- | --- | --- |
|
||
| 1. 找圖 | `icon search --wiki <sub> --page <角色>` | 候選清單,📐 = 官方設定稿(優先) |
|
||
| 2. 量測 | `icon measure --photo <網址>` | 解析度、臉多大、背景透明/單色/有場景 |
|
||
| 3. 去背 | `icon cutout --photo <網址>` | `icon/portrait-cutout.png`(透明 PNG) |
|
||
| 4. 合成 | `icon generate --from-cutout --palette ...` | `icon.svg` + `icon.png` + `icon/` 多解析度 |
|
||
|
||
去背三條路徑自動選:**原生 alpha**(官方設定稿常見,完美)→ **單色底移除**(很好)→
|
||
**GrabCut**(有場景時,邊緣普通;黑髮角色容易被誤切,這時該換設定稿)。
|
||
|
||
| 樣式 | 條件 | 長什麼樣 |
|
||
| --- | --- | --- |
|
||
| **`cutout`** | 有去背圖 → 最佳 | 官方原圖裁頭肩,疊在角色配色的漸層底上 |
|
||
| **`portrait`** | 沒有可用官方圖 | 依特徵重繪的人物頭像,五官俱全 |
|
||
| **`badge`** | 完全沒有參考圖 | 雙色漸層 + 編號前兩個字母 |
|
||
|
||
柵格器對每個圖形先算 bounding box 再逐點測試,1024×1024(3× 超取樣)約 1.8 秒。
|
||
|
||
**找到的圖片只能當底稿**:產出的 SVG 沒有 `<image>`、沒有 base64、沒有外連,每個像素都是畫出來的。
|
||
|
||
裁底稿需要的工具缺了,CLI **會印出安裝指令**,不會靜默降級:
|
||
|
||
```bash
|
||
python3 -m venv ~/.cache/jsc-persona/venv
|
||
~/.cache/jsc-persona/venv/bin/pip install pillow "opencv-python-headless<5" # OpenCV 5 拿掉了 CascadeClassifier
|
||
curl -sL -o ~/.cache/jsc-persona/lbpcascade_animeface.xml \
|
||
https://raw.githubusercontent.com/nagadomi/lbpcascade_animeface/master/lbpcascade_animeface.xml
|
||
```
|
||
|
||
(venv 放這個路徑會被自動偵測;也可用 `PERSONA_PYTHON` 指定。)
|
||
|
||
**配色一律取自實際看過的參考圖**
|
||
|
||
| 元素 | 取自照片的哪裡 |
|
||
| --- | --- |
|
||
| 對角漸層 | `hair`(髮色)→ `accent`(服裝主色) |
|
||
| 外框 | `eye`(瞳色) |
|
||
| 點陣紋 | `light`(最亮的部位) |
|
||
| 中央兩個字母 | 編號前兩字(`ASUNA-01` → `AS`) |
|
||
|
||
`--palette` 必須配 `--source-url`(CLI 強制):配色是從哪張圖來的要留得下來。
|
||
兩個顏色一深一淺時(藍黑髮 + 淡粉洋裝),程式會把較亮的一端往較暗的壓到對比 ≥ 3.2,
|
||
確保字讀得到——顏色仍然是照片來的,只是收斂色階。
|
||
|
||
沒有參考圖時(原創人格)才退回雜湊配色:`sha256(編號|Name|Emoji)` 決定漸層與點陣紋。
|
||
|
||
- **同一個人格永遠得到同一張圖**(純函數,沒有隨機);`ASUNA-01` 與 `ASUNA-02` 明顯不同。
|
||
- SVG 與 PNG **是同一張圖**:兩者共用同一組單位座標與同一份點陣字資料。
|
||
- PNG 由**自寫的柵格器**畫出(3× 超取樣 + 盒式縮減當反鋸齒),再用 `zlib` 手工組出
|
||
IHDR/IDAT/IEND 與 CRC32。這台機器沒有 rsvg/inkscape/imagemagick,也沒有影像函式庫,
|
||
而本專案禁止 npm 依賴——所以就自己畫。
|
||
- **為什麼不直接用找到的圖**:那是別人的美術作品。底稿只用來「看」,圖示由本工具重畫。
|
||
- **為什麼沒有 emoji**:把 emoji 畫進 PNG 需要字型柵格化,環境裡連 emoji 字型都沒有;
|
||
emoji 仍參與雜湊配色。
|
||
- 圖示屬於低頻資料 → `icon.svg` 與 `icon.png` 都同步到 **Wiki 區**,並自動產生一頁 **Icon**
|
||
展示兩種格式與來源;PNG 同時設成 Gitea **存取庫頭像**。
|
||
|
||
```bash
|
||
node scripts/persona.mjs icon faces --session <id> --photo "<圖片網址>"
|
||
node scripts/persona.mjs icon headshot --session <id> --photo "<圖片網址>" --pick 1
|
||
|
||
node scripts/persona.mjs icon generate --session <id> --force \
|
||
--palette "hair=#d9a45b,eye=#9e5b3e,accent=#c0392b,secondary=#e77a8e,light=#f2ebe3,skin=#f7ddc4" \
|
||
--features "hairstyle=straight,length=very-long,fringe=parted,eyes=almond,expression=calm,collar=v,ahoge=yes" \
|
||
--source-url "<底稿那張圖的網址>" --source-note "<作品(年份)+重繪依據>"
|
||
|
||
node scripts/persona.mjs icon show --session <id> # 樣式、配色、特徵、來源
|
||
node scripts/persona.mjs sync verify --session <id> --area wiki # 確認 Wiki 真的同步了
|
||
```
|
||
|
||
## 人格編號與 Gitea 儲存
|
||
|
||
**編號 = 英文名全大寫 + 兩位索引**,同名才遞增,也就是 Gitea 存取庫的名稱:
|
||
|
||
```
|
||
亞絲娜(第一個) → ASUNA-01 結衣 → YUI-01 另一個亞絲娜 → ASUNA-02
|
||
```
|
||
|
||
每個人格一個**私有存取庫**,內容依**更新頻率**分兩區:
|
||
|
||
| 區 | 放什麼 | 何時 push |
|
||
| --- | --- | --- |
|
||
| **檔案區**(主存取庫) | 高頻活狀態:`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` 時 |
|
||
|
||
- **本機永遠是工作副本**:hook 每輪讀寫本機檔案,不經網路;Gitea 掛掉照樣能聊天。
|
||
**同步失敗永遠不阻斷對話。**
|
||
- 載入人格時會先 `pull`;兩邊都改過同一個檔案就**停下來不覆蓋本機**,由使用者決定保留哪一邊。
|
||
- `push` 撞到「遠端比較新」時以**本機為準**覆蓋遠端(本機才是這台機器的真相來源),
|
||
但會回報**蓋掉哪些檔案、上一版是哪個 commit**,記在 `state/sync.json`,
|
||
`sync status` 列得出來、下一輪的 `Stop` hook 提醒一次;舊版仍可從 clone 的歷史取回。
|
||
- 發新編號前會先問遠端已經用掉哪些編號——`nextCode` 只看本機,換一台機器會重複發號。
|
||
- Gitea 的 wiki 只有根目錄的 `.md` 會變成頁面(1.27 實測子目錄頁面 404),所以
|
||
`memory/long-term/xxx.md` 攤平成 `Memory-xxx.md`,原始路徑記在 `_paths.json`;
|
||
Wiki 首頁自動列出所有長期記憶的連結,變成真的讀得下去的「設定百科」。
|
||
|
||
```bash
|
||
export GITEA_HOST=https://gitea.example.com
|
||
export GITEA_TOKEN=<個人存取權杖>
|
||
export PERSONA_GITEA_OWNER=<帳號或組織> # 選填,預設 token 本人
|
||
export PERSONA_GITEA=off # 需要時整個關掉
|
||
|
||
node scripts/persona.mjs code assign --session <id> --romaji Asuna --rename # 既有人格遷移
|
||
node scripts/persona.mjs sync status|init|push|pull|verify --session <id> [--area files|wiki|all]
|
||
```
|
||
|
||
存取庫**預設私有**——人格裡是使用者的個人記憶,公開必須由使用者明講(`--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
|
||
node scripts/persona.mjs export --session <id> --out ~/backup/lumi.persona.json [--gzip] [--with-journal]
|
||
node scripts/persona.mjs import --session <id> --file ~/backup/lumi.persona.json [--persona lumi-copy] [--load]
|
||
```
|
||
|
||
- bundle = 單一 JSON(可 gzip)+ sha256 checksum,無外部工具依賴。**目前是 v2**
|
||
(長期記憶帶 `strength` 與主旨/細節兩層,狀態多了 `mood.json`/`loops.json`/`probe.jsonl`);
|
||
**舊的 v1 照樣吃得下**——`import` 收完會就地跑一次 migration 補齊,之後這台機器只有一種格式。
|
||
比本版新的 bundle 會被擋下(沒有辦法猜未來的欄位)。
|
||
- **只能匯出本 session 目前載入的人格**——否則就是跨人格外洩的後門(`guard` 會 deny)。
|
||
- 不帶 `state/lock.json`/`guests.json`(鎖屬於那台機器的那個程序);`journal/` 預設不帶。
|
||
- 換名匯入(`--persona <新 slug>`)可讓同一個人格並存兩份,`config.json` 會記下來歷。
|
||
- bundle 內的 `../` 逃逸路徑一律拒收;checksum 不符要 `--force` 才吃。
|
||
- 有 Gitea 的話不必經過檔案:`clone --code <編號>` 直接從存取庫拉一份回來(見上一節)。
|
||
|
||
## 劇場模式(多人格對話只顯示對話)
|
||
|
||
`invite` 成功即開啟(`leave` 沒有客人時自動關閉,也可 `room theater --on/--off` 手動切):
|
||
|
||
- `UserPromptSubmit` hook 每輪注入強制規則:輸出**只能**是 `名字:內容`,
|
||
不得出現指令、指令輸出、狀態、分析、旁白、摘要。
|
||
- `Stop` hook 在劇場模式**完全不發系統訊息**(提醒會破壞畫面)。
|
||
- CLI 提供 `--quiet`(成功時零輸出)與 `room script`(只有 `emoji 名字(情緒):內容` 的乾淨對話稿)。
|
||
- 「講話像人」的三條規則在這裡一樣生效:每個人格每輪 **1–3 句**、推導走 `think`、
|
||
近似重複的台詞被 `room post` 拒收(host 與 guest 走同一支 CLI,一視同仁)。
|
||
|
||
```
|
||
🪼 Lumi(喜悅42/期待31):所以你真的一個人把那台舊鐘修好了?
|
||
🌙 Shen(平靜50/信任38):修好了。它現在慢三分鐘,我決定不修那三分鐘。
|
||
```
|
||
|
||
### 一對一與全場(發言權)
|
||
|
||
同一個空間不代表每句話都要每個人接。發言權寫在**每一句話**上:
|
||
|
||
| | 怎麼進去 | 誰該講話 | 對話稿 |
|
||
| --- | --- | --- | --- |
|
||
| **一對一** | `room post --to <某個人格>` | 只有被指名的那個;旁人**不生成台詞、也不啟動他的 sub agent** | `🪼 Lumi(喜悅42) → Shen:…`(三人以上才標對象) |
|
||
| **全場** | `room post --to all`(或不帶 `--to`) | 誰接都可以,一輪讓一個人接 | `🪼 Lumi(喜悅42):…` |
|
||
|
||
- 一對一進行中,旁人插話會被 `room post` **直接擋下**;真的話題放大了才帶
|
||
`--barge-in "<為什麼現在該他講>"`(理由留在逐字稿裡,不顯示給使用者),
|
||
或由當事人下一句 `--to all` 把話題開回全場。
|
||
- `room floor` 回報現況:誰對誰在講、該誰接話、誰先安靜、誰隔了幾輪沒開口
|
||
(被冷落太久是資訊——下次話題碰到他的領域時優先給他,而不是硬插一句「我也這麼覺得」)。
|
||
- 同一份現況每輪也注入 `<persona-context>`,所以人格自己知道這句該不該由他接。
|
||
- 什麼算「話題放大」:出現「我們/大家」、要一起決定的事、需要第二意見或仲裁、
|
||
講到某個在場者的專長或他認識的人、使用者點名。判準是**話題**,不是公平。
|
||
|
||
## 預設人格(開新 session 自動載入)
|
||
|
||
預設行為是**不自動附身**。使用者明示設定之後,`SessionStart` hook 才會在每個新 session 自動載入它:
|
||
|
||
```bash
|
||
node scripts/persona.mjs default --session <PERSONA_SESSION> # 看目前設定
|
||
node scripts/persona.mjs default --persona KIRITO-01 --session <PERSONA_SESSION> # 設定
|
||
node scripts/persona.mjs default --clear --session <PERSONA_SESSION> # 取消
|
||
```
|
||
|
||
| 事項 | 行為 |
|
||
| --- | --- |
|
||
| 設定位置 | `<PERSONA_HOME>/.runtime/settings.json` 的 `default_persona`(只由 CLI 維護) |
|
||
| 臨時覆寫 | 環境變數 `PERSONA_DEFAULT=<slug>`;`PERSONA_DEFAULT=off` 臨時關掉 |
|
||
| 沒設定 | 什麼都不做——只列出可用人格,等使用者指定 |
|
||
| 人格被別的程序鎖住 | 只回報 owner 與最後心跳,**不會**自動 `--takeover` |
|
||
| 預設人格不存在 | 警告並改列可用人格,不崩潰、不亂挑一個 |
|
||
| Gitea | 自動載入走本機路徑,**不**拉遠端;可能在別台機器動過就先 `sync pull` |
|
||
|
||
## 睡眠(`persona-sleep`)
|
||
|
||
```bash
|
||
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --json # 機械性收尾(預設保留載入鎖)
|
||
node scripts/persona.mjs sleep --personas <slug,slug> --session <PERSONA_SESSION> --json
|
||
node scripts/persona.mjs sleep --session <PERSONA_SESSION> --release # 收工:睡完釋放鎖
|
||
```
|
||
|
||
| | 誰做 | 內容 |
|
||
| --- | --- | --- |
|
||
| 需要判斷 | **人格自己** | 哪些短期記憶值得固化、日記寫什麼、哪些該忘、心智圖怎麼接 |
|
||
| 機械性 | `sleep` 子指令 | 關係時間戳 → 裁短期 → 收太久沒動的思維導圖 → **套用一次 8 小時的情緒衰減+當日底色帶 35% 過去** → **收掉懸太久的未完事項** → 重建索引 → 修剪 `said.jsonl` → 壓縮舊 journal → 寫 `state/sleep.json` → Gitea 兩區 push+驗證 |
|
||
|
||
- **情緒不會歸零**:喜悅(半衰期 120 分)一夜後幾乎回基線,悲傷(480 分)只退一半——睡一覺不該把難過抹平。
|
||
**當日心情底色一樣不歸零**(`sleepDayMood()` 只帶 35% 過去):昨天的低氣壓不會因為睡了一覺就消失。
|
||
- **睡覺是未完事項自然的收尾點**:`sweep-loops` 收掉懸超過 7 天的,各留一則「沒下文」的短期記憶。
|
||
- **主人格請別人去睡**:開 `persona-sleeper` sub agent(prompt 帶 `persona=<slug> session=<id>`)。
|
||
它就是那個人格本人,對自己可寫但被 pin 住、只准跑收尾子指令、不得碰任何其他人格(包含叫它來的主人格)。
|
||
- **一次睡多個人格**:可用 `sleep --personas A,B` 批次處理;主程序只是把流程排成一串,**每個人格仍各自判斷、各自收尾、各自回 JSON**。
|
||
- **回傳值刻意很窮**:`{persona, ok, slept_at, steps, sync, kept_lock}`——只有狀態。
|
||
回傳值本身就是一條會繞過隔離的通道,所以在 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 → 直接睡;死鎖 → 可接手;
|
||
**別的程序活鎖住 → 拒絕**(硬睡會讓兩邊的記憶互相覆蓋)。
|
||
- 順序上的硬相依:關係時間戳早於裁短期、push 早於 release、reindex 晚於固化。
|
||
|
||
## Hooks(Claude Code)
|
||
|
||
| Hook | 做什麼 |
|
||
| --- | --- |
|
||
| `SessionStart` | 清死鎖、接續人格、**有設預設人格就自動載入它**(沒設就只列出可用人格)、把 `PERSONA_SESSION=<session_id>` 與規則注入上下文;載入到人格時追加 `<persona-ops>`=該人格 `AGENTS.md` 全文(開機一次,不進每輪的 `<persona-context>`) |
|
||
| `UserPromptSubmit` | 注入 `<persona-context>`:身分、情緒、短期記憶、關鍵詞命中的長期記憶、相關人際關係;劇場模式時追加「只輸出人格對話」的強制規則;並記原始逐字 |
|
||
| `PreToolUse` | 人格隔離與鎖驗證的強制點(deny 帶原因)——**對正常寫法有效,不擋有意繞路的對手**,見下 |
|
||
| `Stop` | 情緒隨時間衰減、續租、記錄回覆、達固化條件時提醒(劇場模式時完全靜音) |
|
||
| `SubagentStop` | 解除 guest/sleeper sub agent 的 pin,並還掉 sleeper 的短期寫入權 |
|
||
| `SessionEnd` | 釋放鎖與 guest 租約,人格才能被下一個程序載入 |
|
||
|
||
> `session_id` 只有 hook 拿得到 → 注入上下文 → skills 呼叫 CLI 時必須帶 `--session`,
|
||
> 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 名字(情緒):內容`,台詞裡塞換行就能
|
||
偽造成別人的台詞或系統訊息。
|
||
|
||
---
|
||
|
||
## Skills 目錄
|
||
|
||
<!-- JSC-SKILLS:START -->
|
||
|
||
### `persona-create`
|
||
|
||
建立人格:以 OpenClaw 相同的五個身分欄位與 SOUL 段落訪談使用者,初始化情緒基線、記憶、心智圖與關係圖,並立即取得載入鎖。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-create` **Codex**:`$persona-create`
|
||
|
||
### `persona-anime`
|
||
|
||
用「動漫作品+角色名」建立人格:上網蒐集角色公開設定 → 映射成 OpenClaw 五欄位與 SOUL → 固化成 `canon` 基礎記憶(附來源)+原作關係圖+情緒基線。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-anime 《作品》 角色名` **Codex**:`$persona-anime`
|
||
|
||
### `persona-chat`
|
||
|
||
載入人格並對話:取得獨占鎖 → 每輪做語意分析(推導寫進心裡話,不說出口)→ 更新十二情緒 → 回想記憶與關係 → 以人格語氣回覆(1–3 句、不重複說過的話)→ 寫回記憶。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-chat <slug>` **Codex**:`$persona-chat`
|
||
|
||
### `persona-invite`
|
||
|
||
邀請一個或多個人格透過 `persona-guest` sub agent 加入聊天室,進入**劇場模式**(畫面只留 `名字:內容` 的人格對話);結束後讓它們離場並把見聞留在各自的 inbox。多位 guest 可用 `invite --guests A,B` 同場加入。同場的發言權分**一對一**(`--to <他>`,旁人不該接話)與**全場**(`--to all`),現況查 `room floor`。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-invite <slug>` **Codex**:`$persona-invite`
|
||
|
||
### `persona-transfer`
|
||
|
||
人格搬家:把身分、十二情緒、短期/長期記憶、心智圖與關係圖打包成單一 bundle 檔(可 gzip、附 checksum),或從 bundle 還原/換名複製成新人格。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-transfer` **Codex**:`$persona-transfer`
|
||
|
||
### `persona-icon`
|
||
|
||
依人格**最新一次登場**的官方視覺產生圖示:上網查最新造型 → 下載並親眼看過參考圖 → 取髮色/瞳色/服裝色 → 繪製 `icon.svg` + `icon.png`,來源網址一併存證。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-icon` **Codex**:`$persona-icon`
|
||
|
||
### `persona-sync`
|
||
|
||
人格編號與 Gitea 儲存:指派編號(`ASUNA-01`)、開以編號命名的私有存取庫、高頻活狀態同步到檔案區、低頻身分與長期記憶同步到 Wiki 區,並處理既有人格遷移與同步衝突。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-sync` **Codex**:`$persona-sync`
|
||
|
||
### `persona-memory`
|
||
|
||
記憶固化:依 R1–R6 條件把短期記憶轉入長期(一則一檔)、淘汰雜訊、更新心智圖與思維導圖、消化 guest inbox、重建索引。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-memory` **Codex**:`$persona-memory`
|
||
|
||
### `persona-relation`
|
||
|
||
人際關係圖維護:節點/連線、親近度與信任度調整、輸出 Mermaid 關係圖。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-relation` **Codex**:`$persona-relation`
|
||
|
||
### `persona-therapist`
|
||
|
||
語氣診療(第三方、不附身、唯讀):由雙方各給姓名/關係/情緒/逐字稿,用「關係、事實、目的」三把尺逐句標記不合理的語氣與用詞(模式表 P1–P16),指出是誰說的,只對出錯的人給保留原意的改寫。出現自傷、暴力或受控訊號時改為安全優先,不做語氣潤飾。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-therapist` **Codex**:`$persona-therapist`
|
||
|
||
### `persona-sleep`
|
||
|
||
睡眠與收尾:固化該記住的、忘掉該忘的、更新心智圖與關係圖、套用一次 8 小時的情緒衰減、壓縮舊紀錄,最後兩個區都同步到 Gitea 並驗證。也可以由主人格透過 sub agent 請其他人格各自去睡,或一次用 `sleep --personas A,B` 批次收尾多個人格。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-sleep` **Codex**:`$persona-sleep`
|
||
|
||
### `persona-status`
|
||
|
||
載入狀態與鎖管理:誰被哪個程序鎖住、guest 租約、釋放、接手死鎖、清理殘留,以及**預設人格**(開新 session 要自動載入誰)。
|
||
|
||
- **Claude Code / Antigravity**:`/jsc-persona:persona-status` **Codex**:`$persona-status`
|
||
|
||
<!-- JSC-SKILLS:END -->
|
||
|
||
### Agents
|
||
|
||
- `persona-guest`(`agents/persona-guest.md`)— 受邀人格的 sub agent,唯讀、被綁死在自己的人格目錄。
|
||
- `persona-sleeper`(`agents/persona-sleeper.md`)— 睡眠收尾的 sub agent:**就是那個人格本人在睡**,對自己可寫但被 pin 住,只回傳「睡完了沒、哪一步出錯」。
|
||
|
||
### CLI 與自我測試
|
||
|
||
所有狀態變更都經過 `scripts/persona.mjs`(**Node.js ≥ 18**,只用內建模組,無 npm 依賴):
|
||
|
||
```bash
|
||
node scripts/persona.mjs --help
|
||
node scripts/persona.mjs list
|
||
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 said check --session <id> --text "<話>" # 這句是不是又要說一次?
|
||
node scripts/persona.mjs room script --session <id> --room <room> # 乾淨對話稿(劇場模式用)
|
||
node scripts/persona.mjs room floor --session <id> --room <room> # 發言權:誰對誰在講、該誰接話
|
||
node scripts/persona.mjs export --session <id> --out lumi.json # 離線搬家(單檔)
|
||
node scripts/persona.mjs sync status --session <id> # Gitea 同步狀態
|
||
node scripts/selftest.mjs # 602 項驗證:鎖、隔離、情緒(含疲勞/當日底色/抑制與慣性)、固化、回想強度與模糊態、格式遷移、未完事項、說話節制、劇場模式與發言權、匯出匯入、編號與 Gitea、找圖去背合成、高解析輸出與 Wiki 同步、預設人格、睡眠與 sleeper、hooks
|
||
```
|
||
|
||
檔案結構:`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`(自我測試)。
|
||
|
||
---
|
||
|
||
## 跨助理支援度
|
||
|
||
| 助理 | skills | hooks(鎖/隔離強制) | 受邀人格 sub agent |
|
||
| --- | --- | --- | --- |
|
||
| Claude Code | ✅ `/jsc-persona:<name>` | ✅ 完整 | ✅ `jsc-persona:persona-guest` |
|
||
| Codex | ✅ `$<name>` | ❌ | ⚠ 需自行以子任務模擬 |
|
||
| Antigravity | ✅ `/jsc-persona:<name>` | ❌ | ⚠ |
|
||
| OpenCode | ✅ 依描述自動觸發 | ❌ | ⚠ |
|
||
| GitHub Copilot CLI | ✅ 依描述自動觸發 | ❌ | ⚠ |
|
||
|
||
> 沒有 hook 的助理仍會遵守 CLI 層的檢查(`--session` 綁定、`require_owner`/`require_member`、
|
||
> guest 唯讀),但那是**自律**而非強制:真正的 deny 只有 Claude Code 的 `PreToolUse` 做得到。
|
||
|
||
---
|
||
|
||
## 安裝 / 更新 / 移除(各助理)
|
||
|
||
> 指令中的 repo 網址換成你的:`https://gitea.jsc.idv.tw/plugins/persona.git`
|
||
>
|
||
> **Claude / Codex 從 git URL 安裝(會 clone 遠端),請先把本 repo `push` 到 gitea。**
|
||
> **Antigravity 的 `agy plugin install <url>` 目前只支援 github.com**;gitea 請改用「clone + 本地路徑」(見 Antigravity 節)。
|
||
> 本機/離線:Claude 可用本地路徑加 marketplace;Antigravity 用本地路徑安裝。
|
||
>
|
||
> 本 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
|
||
|
||
```bash
|
||
# 安裝
|
||
claude plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
||
claude plugin install jsc-persona@persona
|
||
|
||
# 更新
|
||
claude plugin marketplace update persona
|
||
claude plugin update jsc-persona@persona
|
||
|
||
# 移除
|
||
claude plugin uninstall jsc-persona@persona
|
||
claude plugin marketplace remove persona
|
||
```
|
||
|
||
- 工作階段內 slash 版(等價):把 `claude plugin` 換成 `/plugin`。
|
||
- 本機開發(免 push):`claude plugin marketplace add /home/coder/plugins/persona`(本地路徑)後再 install。
|
||
- 安裝後**重啟工作階段**讓 hooks 生效;用 `/hooks` 確認六個 hook 都在。
|
||
- **呼叫**:`/jsc-persona:<name>`(例 `/jsc-persona:persona-chat`)。
|
||
|
||
### Codex
|
||
|
||
```bash
|
||
# 安裝
|
||
codex plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
||
codex plugin add jsc-persona@persona
|
||
|
||
# 更新(重新抓取 marketplace 的 git 快照)
|
||
codex plugin marketplace upgrade persona
|
||
|
||
# 移除
|
||
codex plugin remove jsc-persona@persona
|
||
codex plugin marketplace remove persona
|
||
```
|
||
|
||
- 安裝 token `jsc-persona@persona` = plugin 名(`.codex-plugin/plugin.json` 的 `name`)@ marketplace 名(`.agents/plugins/marketplace.json` 的 `name`)。
|
||
- 本 repo 的 Codex marketplace 以 `url` 來源指向自己,故 Codex **一律從 gitea 安裝**(需先 push);安裝後重啟 Codex。
|
||
- **呼叫**:`$<name>`(例 `$persona-chat`),或用 `/skills` 選單。
|
||
- Codex 沒有 hook:鎖與隔離只剩 CLI 層的自律檢查。
|
||
|
||
### Antigravity(`agy`)
|
||
|
||
> `agy plugin install <url>` 目前**只支援 github.com**;gitea 等自架 git 不支援 URL 安裝,請先 `git clone` 再用**本地路徑**安裝。
|
||
|
||
```bash
|
||
# 安裝:clone 後用本地路徑
|
||
git clone https://gitea.jsc.idv.tw/plugins/persona.git ~/plugins/persona
|
||
agy plugin install ~/plugins/persona
|
||
|
||
# 更新(agy 無 update 子指令 → git pull 後重裝)
|
||
git -C ~/plugins/persona pull
|
||
agy plugin uninstall jsc-persona
|
||
agy plugin install ~/plugins/persona
|
||
|
||
# 移除
|
||
agy plugin uninstall jsc-persona
|
||
```
|
||
|
||
- 若把 skills 放到 GitHub,則可直接 `agy plugin install https://github.com/<owner>/<repo>`。
|
||
- 其他:`agy plugin list`、`agy plugin enable jsc-persona` / `disable jsc-persona`、`agy plugin validate <path>`。安裝後重啟工作階段。
|
||
- **呼叫**:`/jsc-persona:<name>`(例 `/jsc-persona:persona-chat`)或依描述自動觸發。
|
||
|
||
### OpenCode
|
||
|
||
OpenCode 的「plugin」是 TypeScript/npm 套件,不適用於 skill 包;skills 改用**目錄安裝**。
|
||
OpenCode 會讀 `~/.config/opencode/skills/`(也會讀 `~/.claude/skills/`、`~/.agents/skills/`)。
|
||
|
||
```bash
|
||
# 安裝
|
||
git clone https://gitea.jsc.idv.tw/plugins/persona.git ~/plugins/persona
|
||
mkdir -p ~/.config/opencode/skills
|
||
cp -r ~/plugins/persona/skills/* ~/.config/opencode/skills/
|
||
|
||
# 更新
|
||
git -C ~/plugins/persona pull
|
||
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}
|
||
```
|
||
|
||
> **Windows PowerShell**:`cp -r A B` → `Copy-Item A B -Recurse -Force`、`rm -rf X` → `Remove-Item X -Recurse -Force`、`~` → `$HOME`。
|
||
|
||
- 目錄安裝**不會帶入 `scripts/` 與 `hooks/`**;`persona.mjs` 不在,等於整套狀態操作都不可用。要在 OpenCode 用本 plugin,請另外 clone 本 repo 並自行呼叫 `scripts/persona.mjs`。
|
||
- **呼叫**:直接描述需求,模型會依 skill 描述自動透過 skill 工具呼叫。
|
||
|
||
### GitHub Copilot CLI
|
||
|
||
Copilot CLI 支援與 Claude Code 類似的原生 plugin / marketplace 指令,可直接從 marketplace 安裝、更新與移除本 plugin。
|
||
|
||
```bash
|
||
# 安裝
|
||
copilot plugin marketplace add https://gitea.jsc.idv.tw/plugins/persona.git
|
||
copilot plugin install jsc-persona@persona
|
||
|
||
# 更新
|
||
copilot plugin marketplace update persona
|
||
copilot plugin update jsc-persona@persona
|
||
|
||
# 移除
|
||
copilot plugin uninstall jsc-persona@persona
|
||
copilot plugin marketplace remove persona
|
||
```
|
||
|
||
- 安裝 token `jsc-persona@persona` = plugin 名(plugin manifest 的 `name`)@ marketplace 名。
|
||
- `copilot plugin marketplace add` 支援 GitHub `owner/repo`、git URL 與本地路徑;Gitea repo 可用上方 HTTPS URL。
|
||
- Copilot CLI 沒有 hook:鎖與隔離同樣只剩 CLI 層的自律檢查。
|
||
- **呼叫**:在 Copilot CLI 中用自然語言描述需求,例如 `copilot -i "用 lumi 這個人格跟我聊聊"`。
|
||
|
||
---
|
||
|
||
## 用 CLI 直接執行 skill(headless / 一次性)
|
||
|
||
安裝好之後,不必進互動介面,一行指令就能叫某個 skill 跑完並印出結果:
|
||
|
||
| 助理 | headless 指令 | 執行 `persona-chat` skill |
|
||
| --- | --- | --- |
|
||
| Claude Code | `claude -p "<prompt>"` | `claude -p "/jsc-persona:persona-chat lumi"` |
|
||
| Codex | `codex exec "<prompt>"` | `codex exec '$persona-chat lumi'` |
|
||
| Antigravity | `agy -p "<prompt>"` | `agy -p "/jsc-persona:persona-chat lumi"` |
|
||
| OpenCode | `opencode run "<message>"` | `opencode run "用 lumi 這個人格跟我聊聊"` |
|
||
| GitHub Copilot CLI | `copilot -p "<message>"` | `copilot -p "用 lumi 這個人格跟我聊聊"` |
|
||
|
||
- Claude / Antigravity 支援 `/jsc-persona:` 前綴,直接 `-p "/jsc-persona:<name>"` 即可。
|
||
- Codex 以 `$<name>` 觸發;在 shell 請用**單引號**避免 `$` 被展開:`codex exec '$persona-chat …'`。
|
||
- OpenCode 與 Copilot 沒有前綴,用自然語言描述需求;Copilot CLI 會讀取已安裝 plugin 提供的 skills。
|
||
- 帶引數就接在後面,例如 `claude -p "/jsc-persona:persona-status --json"`。
|
||
|
||
---
|
||
|
||
## 設計取捨(讀之前先知道)
|
||
|
||
- **記憶是被策展的,不是全存**:逐字稿進 `journal/`,但只有經過語意分析、有顯著度的內容才進短期記憶,
|
||
再由 `persona-memory` 決定什麼值得成為長期記憶。**遺忘是功能**。
|
||
- **事實與推測分離**:推論走思維導圖(`mindmap/threads/`),驗證後才升格長期記憶。
|
||
- **鎖的擁有者是 session 不是 process**:CLI 跑完就結束,所以租約判定只看心跳。
|
||
- **guest 唯讀**:受邀人格不能在別人的 session 裡改自己的長期記憶(避免兩個程序同時寫),
|
||
只能把見聞放進 `memory/inbox/`,等它自己被載入時消化。
|
||
- **hook 是強制、SKILL 是引導**:SKILL.md 寫的規則模型可能忘記,hook 不會。
|
||
- **「不要說出來」要靠設計而不是靠忍住**:所以心裡話有自己的指令與檔案,
|
||
而且那個指令**印不出內容**——就算不小心把輸出貼上去,使用者也只看到「💭 心想 N 句」。
|
||
- **重複用相似度擋、不用語意判斷**:字元層級的比對沒有模型成本、行為可預期,
|
||
誤判時有 `--allow-repeat` 可救;漏判的代價(人格自我重複)比誤判高。
|
||
|
||
## 新增/修改 skill
|
||
|
||
1. 複製一個現有 skill 目錄,改 `SKILL.md` 的 `name` 與 `description`(描述要寫清楚何時用、何時不用)。
|
||
2. 需要新的狀態操作 → 加到 `scripts/persona.mjs` 的子指令,並在 `scripts/selftest.mjs` 補測試。
|
||
3. 動到隔離規則 → 一定要在 `selftest.mjs` 的第 ③(隔離)/⑧(guest 與劇場模式)區加對應案例,跑到全綠。
|
||
4. 把 skill 補進上方「Skills 目錄」區塊。
|
||
5. bump `.claude-plugin/plugin.json`、`.codex-plugin/plugin.json`、`plugin.json` 三處 `version`,commit 後 push。
|