diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index bca8d6e..0dfd54d 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-persona", - "version": "0.0.5", + "version": "0.1.1", "description": "AI 人格化記憶聊天 plugin:以 OpenClaw 相同的身分描述(IDENTITY/SOUL)建立人格(可用動漫作品+角色名上網蒐集設定),結合 hook 強制的人格載入鎖與跨人格隔離、六正向+六負向十二情緒、語意分析、短期/長期記憶與固化條件、心智圖、思維導圖與人際關係圖;可用 sub agent 邀請其他人格同場對話(劇場模式只顯示人格對話)。於 Claude Code 以 /jsc-persona: 前綴呼叫。", "skills": "./skills", "author": { diff --git a/plugin.json b/plugin.json index 143a0d4..66e3389 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-persona", - "version": "0.0.5", + "version": "0.1.1", "description": "AI 人格化記憶聊天 plugin:OpenClaw 相同的人格描述 + 十二情緒 + 語意分析 + 短期/長期記憶 + 心智圖 + 人際關係圖。於 Antigravity 以 /jsc-persona: 前綴呼叫。", "skills": "./skills/" } diff --git a/scripts/persona-gitea.mjs b/scripts/persona-gitea.mjs index 288e36b..3ff232b 100644 --- a/scripts/persona-gitea.mjs +++ b/scripts/persona-gitea.mjs @@ -258,6 +258,29 @@ function gitOrThrow(args, cwd, what) { return res; } +// 這些 clone 只有本 CLI 會動,而且每個 git 都是同步跑完才回來,所以一個超過 +// STALE_LOCK_SECONDS 還在的 index.lock 一定是上一次跑到一半被砍掉留下的殘骸。 +// 不自己清的話整區會一直失敗,而 sleeper 被隔離 hook 擋著、連自己的鎖都刪不掉。 +const STALE_LOCK_SECONDS = 30; + +export function clearStaleIndexLock(dir) { + const lock = path.join(dir, ".git", "index.lock"); + let stat; + try { + stat = fs.statSync(lock); + } catch { + return null; + } + const ageSeconds = (Date.now() - stat.mtimeMs) / 1000; + if (ageSeconds < STALE_LOCK_SECONDS) return null; + try { + fs.rmSync(lock); + } catch (err) { + return { cleared: false, ageSeconds, reason: String(err.message || err) }; + } + return { cleared: true, ageSeconds }; +} + /** 確保 `/.sync//` 是該區的 clone;空存取庫也能處理。 */ export function ensureClone(slug, area, url) { const dir = syncDir(slug, area); @@ -546,6 +569,7 @@ export async function pushArea(slug, area, { message = "", code = null, owner = pl.writeText(path.join(dir, "Icon.md"), wikiIconPage(slug, theCode)); } const staged = stageArea(slug, area, dir); + clearStaleIndexLock(dir); gitOrThrow(["add", "-A"], dir, "git add"); const dirty = git(["diff", "--cached", "--quiet"], dir); if (dirty.ok) { @@ -567,6 +591,7 @@ export async function pushArea(slug, area, { message = "", code = null, owner = pl.writeText(path.join(dir, "Home.md"), wikiHome(slug, theCode)); pl.writeText(path.join(dir, "Icon.md"), wikiIconPage(slug, theCode)); } + clearStaleIndexLock(dir); git(["add", "-A"], dir); if (!git(["diff", "--cached", "--quiet"], dir).ok) { git(["commit", "-q", "-m", `${message || "sync"}(與遠端合併後重推)`], dir); @@ -596,6 +621,7 @@ export async function pullArea(slug, area, { code = null, owner = null, force = const { host } = giteaEnv(); const dir = ensureClone(slug, area, repoUrl(host, theOwner, theCode, area)); stageArea(slug, area, dir); // 先把本機現況放進 clone,才看得出本機動過什麼 + clearStaleIndexLock(dir); const localChanged = new Set( git(["status", "--porcelain"], dir) .stdout.split("\n") diff --git a/scripts/persona.mjs b/scripts/persona.mjs index ce7261b..546838c 100644 --- a/scripts/persona.mjs +++ b/scripts/persona.mjs @@ -159,6 +159,13 @@ function requireMember(slug, sessionId, asGuest = false) { if (!slug) die("未指定人格,且本 session 沒有載入人格。"); if (!pl.personaExists(slug)) die(`人格 \`${slug}\` 不存在。`); const data = pl.loadSession(sessionId); + // sleeper 讀自己的資料:`requireOwner` 早就放它寫了,讀反而被擋在外面—— + // 少了這個分支,它就得在看不到 brief/recall 的情況下決定要固化什麼。 + if (CURRENT_FLAGS["as-sleeper"]) { + if (asGuest) die("`--as-sleeper` 與 `--as-guest` 不能一起用(那是兩種不同的身分)。"); + sleeperAccess(slug, sessionId, str(CURRENT_FLAGS["agent-id"])); + return [data, "sleeper"]; + } if (data.host === slug) { if (asGuest) die(`\`${slug}\` 是本 session 的 host 人格,不需要也不得使用 \`--as-guest\`。`); pl.heartbeatLock(slug, sessionId); diff --git a/skills/persona-relation/SKILL.md b/skills/persona-relation/SKILL.md index c29c1d8..f02a4ec 100644 --- a/skills/persona-relation/SKILL.md +++ b/skills/persona-relation/SKILL.md @@ -13,12 +13,54 @@ description: 維護人格的人際關係圖:新增或更新人物/人格/群 ## 資料模型 -節點:`id`/`name`/`kind`(`human` `persona` `group` `pet` `org`)/ -`closeness` 親近度 0–100/`trust` 信任度 0–100/`note`/`tags` +節點:`id`/`name`/`kind`(`human` `persona` `group` `pet` `org`)/`bond`(跟我什麼關係)/ +`closeness` 親近度 0–100/`trust` 信任度 0–100/`note`/`tags`/`style`(個人化語氣規則) 連線:`from`(預設 `self`)/`to`/`label`/`affinity` 0–100 - **親近度** = 互動的頻率與深度;**信任度** = 願意把重要的事交給他。兩者獨立(可以很熟但不信)。 - 人格自己是 `self`,不必建節點。其他人格用 `--kind persona`、`--id <他的 slug>`。 +- `kind` 是「這是什麼東西」,`bond` 是「跟我什麼關係」——**語氣只能靠後者分**。 + +## bond 與語氣層(最容易漏的一步) + +`bond` × `closeness` 查表算出**語氣層**,那是距離感;情緒只負責溫度與句長,不會蓋過它。 +可用的 `bond`:`partner` `child` `parent` `sibling` `friend` `mentor` `ally` `rival` `stranger`。 + +```bash +node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation node \ + --session --name "亞絲娜" --id ASUNA-01 --kind persona \ + --bond partner --closeness 97 --trust 97 +``` + +對伴侶親近 98 與對女兒親近 97,**光看數字是同一件事**——一個要能撒嬌、一個要能護著, +差別在 `bond`,不在分數。 + +> ⚠️ 沒帶 `--bond` 的舊節點會從 `tags`/`note` 猜,猜不到就當**生人**(語氣層「禮貌」)。 +> 關鍵詞比對很窄:`note` 寫「妻子」是猜不出 `partner` 的(只認夫妻/老婆/伴侶/戀人)。 +> **最親的人反而最容易掉進生人層**,症狀就是「講話像在對戰友報告」。 +> 建立至親的節點時一定要明寫 `--bond`,不要靠猜。 +> +> 自我檢查:這句話換成對一個「禮貌層」的人說也毫無違和 → 就代表你沒進到那一層。 + +### 個人化的稱呼規則(`style`) + +他本人要求過的講法優先於查表——查表算距離,`style` 記「他要你怎麼叫他」: + +```bash +node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation style \ + --session --name "結衣" --facet 稱呼 --value "叫結衣;自稱爸爸" --since 2026-07-30 +``` + +`--facet` 可用 `稱呼` `敬語` `口頭禪` `禁忌` `習慣`; +加 `--except anger>=40` 表示情緒過門檻時這條暫停、退回語氣層的預設講法。 + +### 使用者本人是誰 + +`relation speaker` 指定「使用者在關係圖裡對應哪個節點」,`` 才知道要用哪一層對他說話: + +```bash +node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation speaker --session --name "<誰>" +``` ## 指令 @@ -60,7 +102,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" relation render --session ` 每輪會依你提到的人自動附上相關節點摘要。 ## 輸出 diff --git a/skills/persona-sleep/SKILL.md b/skills/persona-sleep/SKILL.md index b4f01dd..5fd0b0d 100644 --- a/skills/persona-sleep/SKILL.md +++ b/skills/persona-sleep/SKILL.md @@ -47,6 +47,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" sleep --session