feat: 人格編號(ASUNA-01)與 Gitea 儲存,依更新頻率分檔案區/Wiki 區
1. 人格編號
- 編號 = 英文名全大寫 + 兩位索引,同名才遞增:ASUNA-01 / YUI-01 / ASUNA-02。
- 編號同時是新人格的本機目錄名與 Gitea 存取庫名稱(`create` 不給
--persona 就用編號當目錄名;既有小寫 slug 仍然合法,不會被鎖在門外)。
- 中文名由 skill 提議羅馬拼音、使用者確認後帶 `--romaji` 進來;
CLI 只吃拉丁字母,避免拼音在程式裡亂猜。
- 新增 `code show|assign|next`;`code assign --rename` 可把既有人格的
目錄名一併改成編號(會轉移鎖與 session 綁定)。
2. Gitea 儲存(scripts/persona-gitea.mjs)
- 每個人格一個私有存取庫,庫名 = 編號。本機仍是工作副本,hook 每輪
讀寫本機檔案,**同步失敗永遠不阻斷對話**。
- 依更新頻率分區:
檔案區(高頻)emotion / short-term / inner / said / inbox /
mindmap threads / journal → 每輪由 Stop hook 背景 push
Wiki 區(低頻)IDENTITY SOUL AGENTS USER / 長期記憶 / INDEX /
心智圖 / 關係圖 → 固化、改身分關係、release 時 push
- Gitea 的 wiki 只有根目錄 .md 會變成頁面(1.27 實測子目錄頁面 404),
所以 Wiki 區攤平成 `Memory-xxx.md`,原始路徑記在 `_paths.json`,
pull 時還原;Home 頁自動列出所有長期記憶連結。
- 新增 `sync status|init|push|pull`;載入時先 pull,兩邊都改過同一個
檔案就停下來不覆蓋本機,交由使用者決定。
- push 遇到 non-fast-forward 會對齊遠端後把工作副本重新疊上去再推。
- 認證走 http.extraHeader(GIT_CONFIG_* 環境變數),token 不寫進
.git/config 也不進 process 參數。
3. 順帶修正
- guard 的 `--persona` 解析原本只吃小寫,大寫編號會漏掉整個跨人格檢查。
- pull 原本會無差別覆蓋本機檔案,本機較新但還沒 push 的內容會被蓋掉;
改成只寫回「遠端真的改過的」與「本機缺少的」。
- `create` 原本吞掉 Gitea 首次推送的結果,失敗是靜默的。
新增 skill:persona-sync(編號、同步、既有人格遷移、衝突處理)。
selftest 116 項全綠(新增 ⑬,含「分區不重不漏」檢查;測試強制 PERSONA_GITEA=off)。
另對真實 Gitea 跑過端對端(建庫→兩區推送→改動→拉回→release),測試用存取庫已刪除。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+272
-13
@@ -14,9 +14,11 @@ import process from "node:process";
|
||||
import zlib from "node:zlib";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as pl from "./persona-lib.mjs";
|
||||
import * as gt from "./persona-gitea.mjs";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const TEMPLATE_DIR = path.join(HERE, "..", "skills", "persona-create", "templates");
|
||||
const SELF = path.join(HERE, "persona.mjs");
|
||||
|
||||
let QUIET = false;
|
||||
|
||||
@@ -48,6 +50,7 @@ function emit(payload, asJson, lines) {
|
||||
const FLAGS = new Set([
|
||||
"json", "quiet", "force", "takeover", "as-guest", "on", "off", "with-meta", "all",
|
||||
"with-journal", "gzip", "record", "load", "allow-repeat",
|
||||
"if-due", "no-gitea", "public", "rename",
|
||||
]);
|
||||
|
||||
function parseArgs(argv) {
|
||||
@@ -154,6 +157,15 @@ function requireMember(slug, sessionId, asGuest = false) {
|
||||
|
||||
const hostOf = (flags, session) => str(flags.persona) || pl.loadSession(session).host;
|
||||
|
||||
/**
|
||||
* 里程碑事件(記憶固化、身分/關係變更)之後,把 Wiki 區推上去。
|
||||
* 背景執行、失敗不阻斷:同步永遠不該卡住對話。
|
||||
*/
|
||||
function pushWikiLater(slug, session, flags) {
|
||||
if (flags["no-gitea"] || gt.giteaProblem() || !gt.personaCode(slug)) return;
|
||||
gt.pushInBackground(slug, "wiki", session, SELF);
|
||||
}
|
||||
|
||||
function renderTemplate(name, mapping) {
|
||||
let text = fs.readFileSync(path.join(TEMPLATE_DIR, name), "utf8");
|
||||
for (const [key, value] of Object.entries(mapping)) text = text.replaceAll(`{{${key}}}`, String(value));
|
||||
@@ -166,10 +178,25 @@ function renderTemplate(name, mapping) {
|
||||
|
||||
const commands = {};
|
||||
|
||||
commands.create = ({ flags }) => {
|
||||
commands.create = async ({ flags }) => {
|
||||
const session = requireSession(flags);
|
||||
const slug = str(flags.persona);
|
||||
if (!pl.validSlug(slug)) die("slug 只能是小寫英數與連字號(最長 48 字),例如 `lumi`、`shen-yu`。");
|
||||
// 人格編號 = 英文名全大寫 + 兩位索引(同名才遞增)。編號就是 Gitea 存取庫的名稱。
|
||||
let code = str(flags.code);
|
||||
if (code && !gt.validCode(code)) die(`編號 \`${code}\` 不合法,格式是「英文名全大寫-兩位數」,例如 \`ASUNA-01\`。`);
|
||||
if (!code) {
|
||||
const base = gt.normalizeRomaji(str(flags.romaji) || str(flags.persona));
|
||||
if (!base) {
|
||||
die(
|
||||
"需要 `--romaji <英文名>`:人格編號是英文名全大寫加索引(例:Asuna → `ASUNA-01`)。" +
|
||||
"中文名請先轉成羅馬拼音並跟使用者確認拼法,再帶進來。",
|
||||
);
|
||||
}
|
||||
code = gt.nextCode(base);
|
||||
if (!code) die(`\`${base}\` 的編號已經用到 99,請換一個英文名。`);
|
||||
}
|
||||
// 沒指定 --persona 就用編號當目錄名(一個識別走到底);指定了就沿用(相容既有人格)
|
||||
const slug = str(flags.persona) || code;
|
||||
if (!pl.validSlug(slug)) die("人格目錄名只能是英數與連字號(最長 48 字),建議直接用編號,例如 `ASUNA-01`。");
|
||||
if (pl.personaExists(slug) && !flags.force) {
|
||||
die(`人格 \`${slug}\` 已存在(${pl.personaDir(slug)})。要覆寫請加 --force。`);
|
||||
}
|
||||
@@ -191,12 +218,14 @@ commands.create = ({ flags }) => {
|
||||
pl.writeJson(pl.emotionPath(slug), pl.defaultEmotionState(parseDeltas(flags.baseline)));
|
||||
pl.writeJson(pl.configPath(slug), {
|
||||
persona: slug,
|
||||
code,
|
||||
romaji: gt.codePrefix(code),
|
||||
display_name: mapping.NAME,
|
||||
created_at: pl.nowIso(),
|
||||
created_by_session: session,
|
||||
origin: str(flags.origin) || "custom",
|
||||
source_work: str(flags.work),
|
||||
schema: 1,
|
||||
schema: 2,
|
||||
});
|
||||
pl.writeJson(pl.relationsJson(slug), { nodes: [], edges: [] });
|
||||
pl.writeText(
|
||||
@@ -206,8 +235,28 @@ commands.create = ({ flags }) => {
|
||||
pl.rebuildIndex(slug);
|
||||
pl.acquireLock(slug, session, { cwd: str(flags.cwd) || null });
|
||||
pl.bindHost(session, slug, { cwd: str(flags.cwd) || null });
|
||||
ok(`人格 \`${slug}\` 建立於 ${root},已取得載入鎖並綁定本 session。`);
|
||||
ok(`人格 \`${slug}\`(編號 \`${code}\`)建立於 ${root},已取得載入鎖並綁定本 session。`);
|
||||
say(` 下一步:補完 ${root}/IDENTITY.md 與 SOUL.md,再用 /jsc-persona:persona-chat 開始對話。`);
|
||||
// Gitea 上的存取庫名稱就是編號。身分還沒補完,這裡只開庫;內容之後由各時機自動 push。
|
||||
if (!flags["no-gitea"] && !gt.giteaProblem()) {
|
||||
try {
|
||||
const info = await gt.initRemote(slug, { code, private_: !flags.public });
|
||||
say(` 📦 Gitea:${info.repo.html_url}(${info.created ? "已建立" : "沿用既有"},${info.repo.private ? "私有" : "公開"})`);
|
||||
for (const key of gt.AREA_KEYS) {
|
||||
const res = info.results[key] || {};
|
||||
say(
|
||||
res.ok
|
||||
? ` ${gt.AREAS[key].label}(${gt.AREAS[key].why}):${res.changed ? `已推送 ${res.files} 個檔案` : "目前沒有內容"}`
|
||||
: ` ⚠ ${gt.AREAS[key].label}推送失敗:${String(res.reason).slice(0, 160)}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
say(` ⚠ Gitea 存取庫建立失敗(不影響本機使用):${err.message}`);
|
||||
say(" 之後可用 `sync init` 補建。");
|
||||
}
|
||||
} else if (!flags["no-gitea"]) {
|
||||
say(` ℹ 未同步到 Gitea:${gt.giteaProblem()}`);
|
||||
}
|
||||
};
|
||||
|
||||
commands.list = ({ flags }) => {
|
||||
@@ -221,6 +270,7 @@ commands.list = ({ flags }) => {
|
||||
}
|
||||
return {
|
||||
persona: slug,
|
||||
code: gt.personaCode(slug),
|
||||
identity: pl.identityBrief(slug),
|
||||
locked: status.locked,
|
||||
stale: status.stale,
|
||||
@@ -236,16 +286,17 @@ commands.list = ({ flags }) => {
|
||||
for (const r of rows) {
|
||||
const state = r.locked ? "🔒 已載入" : r.stale ? "⚠ 死鎖可接手" : "🔓 空閒";
|
||||
lines.push(
|
||||
`- \`${r.persona}\` ${state}` +
|
||||
`- \`${r.persona}\`${r.code && r.code !== r.persona ? `(編號 ${r.code})` : ""} ${state}` +
|
||||
(r.locked ? `(session ${r.owner_session}…, cwd ${r.owner_cwd})` : "") +
|
||||
`|guest ${r.guests}|長期記憶 ${r.long_term}|短期 ${r.short_term}` +
|
||||
(r.code ? "" : "|⚠ 尚無編號") +
|
||||
(r.identity ? `|${r.identity}` : ""),
|
||||
);
|
||||
}
|
||||
emit({ home: pl.personaHome(), personas: rows }, flags.json, lines);
|
||||
};
|
||||
|
||||
commands.load = ({ flags }) => {
|
||||
commands.load = async ({ flags }) => {
|
||||
const session = requireSession(flags);
|
||||
const slug = str(flags.persona);
|
||||
if (!pl.personaExists(slug)) die(`人格 \`${slug}\` 不存在。可用:${pl.listPersonas().join(", ") || "(無)"}`);
|
||||
@@ -264,9 +315,32 @@ commands.load = ({ flags }) => {
|
||||
die(`${err.message}\n 若確定那個程序已結束,可加 --takeover 接手。`);
|
||||
}
|
||||
pl.bindHost(session, slug, { cwd: str(flags.cwd) || null });
|
||||
// 載入時先把遠端拉回來(別台機器可能動過),衝突就停下來讓使用者決定
|
||||
const pulled = [];
|
||||
if (!flags["no-gitea"] && !gt.giteaProblem() && gt.personaCode(slug)) {
|
||||
for (const area of gt.AREA_KEYS) {
|
||||
try {
|
||||
pulled.push(await gt.pullArea(slug, area));
|
||||
} catch (err) {
|
||||
pulled.push({ ok: false, area, reason: err.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
pl.pruneShortTerm(slug);
|
||||
pl.rebuildIndex(slug);
|
||||
const lines = [`✔ 已載入人格 \`${slug}\`(exclusive,session ${session.slice(0, 8)}…,租約 ${lock.lease_seconds}s)`];
|
||||
for (const res of pulled) {
|
||||
if (res.conflicts?.length) {
|
||||
lines.push(
|
||||
`⚠ ${gt.AREAS[res.area].label}有衝突,**沒有覆蓋本機**:${res.conflicts.slice(0, 5).join(", ")}` +
|
||||
"。請告訴使用者:本機與 Gitea 都改過同一份資料,要保留哪一邊(`sync pull --force` 會以遠端為準)。",
|
||||
);
|
||||
} else if (res.ok && res.written?.length) {
|
||||
lines.push(`↓ ${gt.AREAS[res.area].label}從 Gitea 拉回 ${res.written.length} 個檔案。`);
|
||||
} else if (!res.ok && !res.skipped) {
|
||||
lines.push(`⚠ ${gt.AREAS[res.area]?.label || res.area}同步失敗(不影響本機):${String(res.reason).slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
if (lock.took_over_from) {
|
||||
const prev = lock.took_over_from;
|
||||
lines.push(
|
||||
@@ -279,11 +353,23 @@ commands.load = ({ flags }) => {
|
||||
emit({ persona: slug, lock, context }, flags.json, lines);
|
||||
};
|
||||
|
||||
commands.release = ({ flags }) => {
|
||||
commands.release = async ({ flags }) => {
|
||||
const session = requireSession(flags);
|
||||
const data = pl.loadSession(session);
|
||||
const slug = str(flags.persona) || data.host;
|
||||
if (!slug) die("本 session 沒有載入任何人格。");
|
||||
// 收工前把兩區都推上去(失敗不阻斷釋放,人格不能被鎖在網路問題裡)
|
||||
if (!flags["no-gitea"] && !gt.giteaProblem() && pl.personaExists(slug) && gt.personaCode(slug)) {
|
||||
for (const area of gt.AREA_KEYS) {
|
||||
try {
|
||||
const res = await gt.pushArea(slug, area, { message: `release: 對話結束 ${pl.nowIso()}` });
|
||||
if (res.ok && res.changed) say(` ↑ ${gt.AREAS[area].label}已推上 Gitea。`);
|
||||
else if (!res.ok && !res.skipped) say(` ⚠ ${gt.AREAS[area].label}推送失敗:${String(res.reason).slice(0, 120)}`);
|
||||
} catch (err) {
|
||||
say(` ⚠ ${gt.AREAS[area].label}推送失敗:${err.message.slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const released = pl.unbindSession(session);
|
||||
ok(`已釋放人格 \`${slug}\` 的載入鎖${released.guests.length ? `,並退出 guest:${released.guests.join(", ")}` : "。"}`);
|
||||
};
|
||||
@@ -557,6 +643,7 @@ commands.consolidate = ({ flags }) => {
|
||||
say(` 短期記憶已淘汰顯著度 < ${forget} 的項目,剩 ${keep.length} 筆。`);
|
||||
}
|
||||
ok(`長期記憶 \`${name}\` 已寫入(共 ${total} 則),INDEX.md 已重建。`);
|
||||
pushWikiLater(slug, session, flags); // 固化=Wiki 區(低頻設定)該更新了
|
||||
};
|
||||
|
||||
commands.prune = ({ flags }) => {
|
||||
@@ -572,6 +659,7 @@ commands.reindex = ({ flags }) => {
|
||||
const slug = hostOf(flags, session);
|
||||
requireOwner(slug, session);
|
||||
ok(`INDEX.md 重建完成(${pl.rebuildIndex(slug)} 則長期記憶)。`);
|
||||
pushWikiLater(slug, session, flags);
|
||||
};
|
||||
|
||||
commands.emotion = ({ flags }) => {
|
||||
@@ -672,6 +760,7 @@ commands.relation = ({ flags, positional }) => {
|
||||
});
|
||||
pl.renderRelations(slug);
|
||||
ok(`關係節點 \`${name}\` 已更新。`);
|
||||
pushWikiLater(slug, session, flags);
|
||||
return;
|
||||
}
|
||||
if (action === "edge") {
|
||||
@@ -685,6 +774,7 @@ commands.relation = ({ flags, positional }) => {
|
||||
});
|
||||
pl.renderRelations(slug);
|
||||
ok(`關係連線 ${str(flags.from) || "self"} → ${to} 已更新。`);
|
||||
pushWikiLater(slug, session, flags);
|
||||
return;
|
||||
}
|
||||
if (action === "render") {
|
||||
@@ -951,6 +1041,164 @@ commands.import = ({ flags }) => {
|
||||
emit({ ...result, source: bundle.persona, checksum_ok: checksumOk }, flags.json, lines);
|
||||
};
|
||||
|
||||
/**
|
||||
* 人格編號:英文名全大寫 + 兩位索引(同名才遞增),也就是 Gitea 存取庫的名稱。
|
||||
* 既有人格用 `code assign --romaji <英文名>` 補編號,加 `--rename` 連目錄名一起改成編號。
|
||||
*/
|
||||
commands.code = async ({ flags, positional }) => {
|
||||
const session = requireSession(flags);
|
||||
const action = positional[0] || "show";
|
||||
if (action === "next") {
|
||||
const base = gt.normalizeRomaji(str(flags.romaji));
|
||||
if (!base) die("需要 `--romaji <英文名>`(只能是拉丁字母與數字)。");
|
||||
const next = gt.nextCode(base);
|
||||
if (!next) die(`\`${base}\` 的編號已經用到 99。`);
|
||||
emit({ romaji: base, code: next }, flags.json, [`\`${base}\` 的下一個可用編號:\`${next}\``]);
|
||||
return;
|
||||
}
|
||||
const slug = hostOf(flags, session);
|
||||
if (action === "show") {
|
||||
requireMember(slug, session, Boolean(flags["as-guest"]));
|
||||
const code = gt.personaCode(slug);
|
||||
const state = gt.loadSyncState(slug);
|
||||
emit({ persona: slug, code, sync: state }, flags.json, [
|
||||
`人格 \`${slug}\` 編號:${code ? `\`${code}\`` : "(尚未指派,用 `code assign --romaji <英文名>`)"}`,
|
||||
code ? ` Gitea 存取庫:${state.repo_url || `${gt.giteaEnv().host}/${state.owner || "?"}/${code}`}` : "",
|
||||
].filter(Boolean));
|
||||
return;
|
||||
}
|
||||
if (action === "assign") {
|
||||
requireOwner(slug, session);
|
||||
const existing = gt.personaCode(slug);
|
||||
if (existing && !flags.force) die(`人格 \`${slug}\` 已有編號 \`${existing}\`。要重新指派請加 --force。`);
|
||||
let code = str(flags.code);
|
||||
if (code && !gt.validCode(code)) die(`編號 \`${code}\` 不合法(格式:ASUNA-01)。`);
|
||||
if (!code) {
|
||||
const base = gt.normalizeRomaji(str(flags.romaji) || slug);
|
||||
if (!base) die("需要 `--romaji <英文名>`(中文名請先轉羅馬拼音並跟使用者確認拼法)。");
|
||||
code = gt.nextCode(base);
|
||||
if (!code) die(`\`${base}\` 的編號已經用到 99。`);
|
||||
}
|
||||
const config = pl.loadConfig(slug);
|
||||
config.persona = slug;
|
||||
config.code = code;
|
||||
config.romaji = gt.codePrefix(code);
|
||||
config.schema = 2;
|
||||
pl.writeJson(pl.configPath(slug), config);
|
||||
const lines = [`✔ 人格 \`${slug}\` 的編號指派為 \`${code}\`。`];
|
||||
let current = slug;
|
||||
if (flags.rename && slug !== code) {
|
||||
if (pl.personaExists(code)) die(`目錄 \`${code}\` 已存在,無法改名。`);
|
||||
// 目錄名改成編號:先放掉自己的鎖 → 改名 → 重新取得鎖並重綁 session
|
||||
pl.releaseLock(slug, session);
|
||||
fs.renameSync(pl.personaDir(slug), pl.personaDir(code));
|
||||
fs.rmSync(path.join(pl.personaDir(code), gt.SYNC_DIRNAME), { recursive: true, force: true });
|
||||
const renamed = pl.loadConfig(code);
|
||||
renamed.persona = code;
|
||||
pl.writeJson(pl.configPath(code), renamed);
|
||||
pl.acquireLock(code, session, { cwd: str(flags.cwd) || null });
|
||||
pl.bindHost(session, code, { cwd: str(flags.cwd) || null });
|
||||
current = code;
|
||||
lines.push(` 目錄已改名:${pl.personaDir(code)}(.sync 快取已清掉,下次 push 會重新 clone)`);
|
||||
}
|
||||
if (!flags["no-gitea"] && !gt.giteaProblem()) {
|
||||
try {
|
||||
const info = await gt.initRemote(current, { code, private_: !flags.public });
|
||||
lines.push(` 📦 Gitea:${info.repo.html_url}(${info.created ? "已建立" : "沿用既有"})`);
|
||||
} catch (err) {
|
||||
lines.push(` ⚠ Gitea 存取庫建立失敗(不影響本機):${err.message}`);
|
||||
}
|
||||
}
|
||||
emit({ persona: current, code }, flags.json, lines);
|
||||
return;
|
||||
}
|
||||
die(`未知 action:${action}(可用 show/assign/next)`);
|
||||
};
|
||||
|
||||
/** 人格與 Gitea 的同步:檔案區=高頻活狀態,Wiki 區=低頻設定。 */
|
||||
commands.sync = async ({ flags, positional }) => {
|
||||
const session = requireSession(flags);
|
||||
const slug = hostOf(flags, session);
|
||||
requireOwner(slug, session);
|
||||
const action = positional[0] || "status";
|
||||
const area = str(flags.area) || "all";
|
||||
if (area !== "all" && !gt.AREA_KEYS.includes(area)) die(`--area 只能是 ${gt.AREA_KEYS.join("/")}/all。`);
|
||||
const areas = area === "all" ? gt.AREA_KEYS : [area];
|
||||
|
||||
if (action === "status") {
|
||||
const state = gt.loadSyncState(slug);
|
||||
const problem = gt.giteaProblem();
|
||||
emit({ persona: slug, code: gt.personaCode(slug), problem, sync: state }, flags.json, [
|
||||
`人格 \`${slug}\`|編號 ${gt.personaCode(slug) || "(無)"}|Gitea ${problem ? `⚠ ${problem}` : "✔ 已設定"}`,
|
||||
` 存取庫:${state.repo_url || "(尚未建立,跑 \`sync init\`)"}`,
|
||||
...gt.AREA_KEYS.map((key) => {
|
||||
const info = state.areas[key] || {};
|
||||
return ` ${gt.AREAS[key].label}(${gt.AREAS[key].why}):` +
|
||||
`最後 push ${info.pushed_at || "—"}|最後 pull ${info.pulled_at || "—"}|${info.files ?? "?"} 個檔案`;
|
||||
}),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const problem = gt.giteaProblem();
|
||||
if (problem) die(`Gitea 尚未設定:${problem}(設 GITEA_HOST 與 GITEA_TOKEN,或用 PERSONA_GITEA_* 覆寫)`);
|
||||
|
||||
if (action === "init") {
|
||||
let code = gt.personaCode(slug);
|
||||
if (!code) die(`人格 \`${slug}\` 還沒有編號。先跑 \`code assign --romaji <英文名>\`。`);
|
||||
const info = await gt.initRemote(slug, { code, owner: str(flags.owner) || null, private_: !flags.public });
|
||||
emit(info, flags.json, [
|
||||
`✔ 人格 \`${slug}\`(\`${info.code}\`)已對應到 Gitea:${info.repo.html_url}`,
|
||||
` ${info.created ? "存取庫已建立" : "沿用既有存取庫"}|${info.repo.private ? "私有" : "公開"}|Wiki ${info.wikiCreated ? "已建立" : "已存在"}`,
|
||||
...gt.AREA_KEYS.map((key) =>
|
||||
` ${gt.AREAS[key].label}:${info.results[key]?.ok ? `${info.results[key].files} 個檔案已推送` : `⚠ ${info.results[key]?.reason || "失敗"}`}`),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (action === "push") {
|
||||
const out = [];
|
||||
for (const key of areas) {
|
||||
if (flags["if-due"] && !gt.pushDue(slug, key)) {
|
||||
out.push({ area: key, ok: true, skipped: true, reason: "未到最小間隔" });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
out.push(await gt.pushArea(slug, key, { message: str(flags.message) }));
|
||||
} catch (err) {
|
||||
out.push({ area: key, ok: false, reason: err.message });
|
||||
}
|
||||
}
|
||||
emit({ persona: slug, results: out }, flags.json, out.map((r) =>
|
||||
r.skipped ? ` ${gt.AREAS[r.area].label}:略過(${r.reason})`
|
||||
: r.ok ? `✔ ${gt.AREAS[r.area].label}:${r.changed ? `已推送 ${r.files} 個檔案` : "沒有變更"}`
|
||||
: `✖ ${gt.AREAS[r.area].label}:${String(r.reason).slice(0, 160)}`));
|
||||
return;
|
||||
}
|
||||
if (action === "pull") {
|
||||
const out = [];
|
||||
for (const key of areas) {
|
||||
try {
|
||||
out.push(await gt.pullArea(slug, key, { force: Boolean(flags.force) }));
|
||||
} catch (err) {
|
||||
out.push({ area: key, ok: false, reason: err.message });
|
||||
}
|
||||
}
|
||||
pl.rebuildIndex(slug);
|
||||
emit({ persona: slug, results: out }, flags.json, out.map((r) =>
|
||||
r.conflicts?.length
|
||||
? `✖ ${gt.AREAS[r.area].label}:本機與遠端都改過 ${r.conflicts.slice(0, 5).join(", ")};` +
|
||||
"沒有覆蓋本機。確定要以遠端為準才加 --force。"
|
||||
: r.ok
|
||||
? `✔ ${gt.AREAS[r.area].label}:` +
|
||||
(r.empty
|
||||
? "遠端還是空的"
|
||||
: `遠端有 ${r.changed?.length || 0} 個檔案更新,寫回本機 ${r.written?.length || 0} 個` +
|
||||
`${(r.written?.length || 0) > (r.changed?.length || 0) ? "(含補回本機缺少的檔案)" : ""}`)
|
||||
: `✖ ${gt.AREAS[r.area].label}:${String(r.reason).slice(0, 160)}`));
|
||||
return;
|
||||
}
|
||||
die(`未知 action:${action}(可用 init/push/pull/status)`);
|
||||
};
|
||||
|
||||
commands.gc = ({ flags }) => {
|
||||
const removed = pl.gcRuntime();
|
||||
emit(removed, flags.json, [
|
||||
@@ -981,7 +1229,10 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
||||
用法:node persona.mjs <subcommand> [options]
|
||||
|
||||
人格與鎖:
|
||||
create --persona <slug> --session <id> [--name --creature --vibe --emoji --avatar --baseline --origin --work]
|
||||
create --romaji <英文名> --session <id> [--persona <目錄名> --code <ASUNA-01> --name --creature
|
||||
--vibe --emoji --avatar --baseline --origin --work --no-gitea --public]
|
||||
人格編號 = 英文名全大寫 + 兩位索引(同名才遞增),也是 Gitea 存取庫的名稱;
|
||||
不指定 --persona 就用編號當目錄名。
|
||||
load --persona <slug> --session <id> [--takeover]
|
||||
release --session <id> [--persona <slug>]
|
||||
list 列出人格與鎖狀態
|
||||
@@ -1010,7 +1261,15 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
||||
room post|read|script|list|theater --session <id> [--room --as --text --text-file --emotion --limit --on --off --with-meta]
|
||||
(post 會擋下「短時間內近似重複」與超過三句的發言;例外用 --allow-repeat / --force)
|
||||
|
||||
搬家:
|
||||
編號與 Gitea(存取庫名稱 = 人格編號):
|
||||
code show|assign|next --session <id> [--romaji <英文名> --code <ASUNA-01> --rename --force --public]
|
||||
sync status|init|push|pull --session <id> [--area files|wiki|all --if-due --force --message --owner]
|
||||
檔案區=高頻活狀態(情緒/短期記憶/心裡話/逐字),每輪對話後背景 push
|
||||
Wiki 區=低頻設定(IDENTITY/SOUL/長期記憶/心智圖/關係圖),固化或改身分時 push
|
||||
環境變數:GITEA_HOST / GITEA_TOKEN(或 PERSONA_GITEA_HOST / _TOKEN / _OWNER),
|
||||
PERSONA_GITEA=off 可整個關掉,PERSONA_SYNC_MIN_SECONDS 調 push 間隔
|
||||
|
||||
搬家(離線檔案):
|
||||
export --session <id> [--out <檔案> --with-journal --gzip --force] 匯出目前載入的人格
|
||||
import --session <id> --file <檔案> [--persona <新 slug> --force --load]
|
||||
|
||||
@@ -1021,7 +1280,7 @@ const HELP = `persona.mjs — jsc-persona 人格 / 記憶 / 情緒 / 關係圖 C
|
||||
全域旗標:--json(機器可讀)、--quiet(成功時不輸出;劇場模式必用)
|
||||
`;
|
||||
|
||||
function main(argv) {
|
||||
async function main(argv) {
|
||||
const sub = argv[0];
|
||||
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
||||
process.stdout.write(HELP);
|
||||
@@ -1032,7 +1291,7 @@ function main(argv) {
|
||||
const parsed = parseArgs(argv.slice(1));
|
||||
QUIET = Boolean(parsed.flags.quiet);
|
||||
try {
|
||||
command({ flags: parsed.flags, positional: parsed._ });
|
||||
await command({ flags: parsed.flags, positional: parsed._ });
|
||||
} catch (err) {
|
||||
if (err instanceof pl.LockError) die(err.message);
|
||||
throw err;
|
||||
@@ -1040,4 +1299,4 @@ function main(argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
process.exit(main(process.argv.slice(2)));
|
||||
process.exit(await main(process.argv.slice(2)));
|
||||
|
||||
Reference in New Issue
Block a user