feat(gitea): clone —— 從 Gitea 匯入一個本機還沒有的人格

底層本來就走得通(`pullArea` 的 restore 會把本機缺少的檔案全部補進來),
擋住的是上層的雞生蛋:`sync` 先走 `requireOwner`,而 `requireOwner` 第一件事
就是「本機沒有這個人格就 die」。本機沒有它 → load 不了它 → sync pull 被擋 →
永遠拉不回來。所以照 `import` 的模式另開一個只驗 session、不驗 host 的入口。

* `clone --code <編號>`:兩區都拉回來(Wiki 區給身分與長期記憶,檔案區給活狀態),
  然後補上 `pullArea` 不管的那幾件事——驗 IDENTITY.md(`validateBundle` 明文的
  人格最低要件,Gitea 這條路上原本不存在)、補寫 config.code/來歷、
  `rebuildIndex()`、`renderRelations()`。拉回來不成人格就中止並清掉半成品。
* `clone`(不帶 --code):列出遠端有哪些人格、哪些本機還沒有。
  整個 codebase 原本沒有任何「列出 owner 底下的存取庫」的呼叫,新增
  `listRemotePersonas()`:分頁打 `GET /user/repos`(他人/組織走 `/users/<owner>/repos`),
  用編號格式過濾——存取庫名稱就是人格編號,所以那份清單就是遠端的人格清單。
* 本機已有同名人格時**預設不覆蓋**;`--force` 才蓋(沿用 `import` 的兩道保護:
  不得覆寫別人、不得覆寫正被其他程序載入的人格),`--persona` 可並存兩份。
* 加進 `OWNER_EXEMPT_SUBCOMMANDS`,否則已載入其他人格時會被 hook deny。
* 編號衝突:`nextCode()` 只掃本機,換機器會重複發號。新增
  `nextCodeAcrossMachines()`,發號前先問遠端已經用掉哪些編號;Gitea 連不上
  就退回本機答案並在輸出明講「只對過本機」。`create` 與 `code assign/next` 都改用它。
* 順手修正 `ensureRepo` 的建庫路由:`me` 取自 `resolveOwner()`,而它在有
  `PERSONA_GITEA_OWNER` 時只會把那個值原封不動還回來,於是組織永遠走成
  `/user/repos`(建到 token 本人底下)。改用不受該環境變數影響的 `giteaLogin()`。

測試:selftest 新增第 ㉒ 區,用 file:// 的裸倉庫當「假的 Gitea」跑完整往返
(推兩區 → 刪掉本機人格 → clone 回來 → 驗身分/長期記憶/活狀態/索引/關係圖),
並涵蓋前兩個修正(子資料夾與非 ASCII 檔名真的進了存取庫、push 覆蓋遠端的回報
與 Stop hook 只吵一次)。345 → 373 項全過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 09:35:29 +00:00
co-authored by Claude Opus 5
parent 5d27051293
commit 2b4b49d478
7 changed files with 509 additions and 23 deletions
+20 -1
View File
@@ -367,11 +367,29 @@ 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 --session <id> [--area files|wiki|all]
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
@@ -384,6 +402,7 @@ node scripts/persona.mjs import --session <id> --file ~/backup/lumi.persona.json
- 不帶 `state/lock.json``guests.json`(鎖屬於那台機器的那個程序);`journal/` 預設不帶。
- 換名匯入(`--persona <新 slug>`)可讓同一個人格並存兩份,`config.json` 會記下來歷。
- bundle 內的 `../` 逃逸路徑一律拒收;checksum 不符要 `--force` 才吃。
- 有 Gitea 的話不必經過檔案:`clone --code <編號>` 直接從存取庫拉一份回來(見上一節)。
## 劇場模式(多人格對話只顯示對話)