feat: 形象圖改為「依人格資料重繪」,並驗證 Wiki 同步
1. 不再直接使用網路上找到的圖片
舊版把裁下來的官方美術當圖示(photo 樣式)。現在改成:
找圖 → `icon headshot` 裁出**大頭照當底稿** → AI 用 Read 親眼看過 →
讀出髮型/瀏海/眼型/表情/髮飾/領口等特徵 → **由本工具重新繪製**。
* 底稿寫在 <人格>/.sync/headshot.png,**不是圖示、不同步、不發佈**。
* 產出的 SVG 不得有 <image>/base64/外連(selftest 會擋)。
* 移除 photo 樣式與 photoSvg;config 裡殘留的舊樣式會被忽略而非退回徽章。
2. 重繪引擎:五官與造型可參數化
新增 --features:hairstyle(5) / length(4) / fringe(4) / eyes(4) /
expression(4) / accessory(5) / side / collar(4) / ahoge。
渲染器新增 polygon 圖元(呆毛、緞帶、V 領、銳利眼角),SVG 與自寫柵格器
仍共用同一份圖形清單,兩邊必然一致。
另外調了臉部比例:眉毛用「髮色偏膚色」避免深髮角色眉毛與瀏海連成黑帶、
加了鼻子(否則嘴會被看成鼻子)、眼與嘴的縱向配置重排。
3. Wiki 形象圖必須同步(並且會被驗證)
新增 `sync verify`(不一致以非零結束)與 verifyIconInWiki();
`icon generate` 推完 Wiki 會自動回頭確認 icon.svg + icon.png 真的在遠端
且與本機一致。
修掉一個會讓驗證永遠失敗的 bug:Wiki 首頁內嵌了 `最後同步 ${now}`,
每次產生都不同 → 永遠 dirty、每次 push 都多一個 commit。改用圖示的
generated_at。porcelain 解析也從固定位移改為正規式。
已重繪兩個真實人格(皆為 portrait 樣式,並通過 Wiki 同步驗證):
ASUNA-01 底稿=《Unanswered//butterfly》(2026) 主視覺左側;金蜜色極長直髮、
中分瀏海、呆毛、紅褐杏眼、沉靜神情、紅上衣 V 領
YUI-01 底稿=AniList 官方角色圖;藍黑極長直髮、齊瀏海、圓大棕眼、燦爛笑容,
髮飾與配色採現行 Unital Ring 導航妖精造型(淡粉洋裝、藍花)
selftest 161 項全綠(新增第 ⑰ 節:特徵解析、換髮型/眼型/表情會畫出不同的圖、
polygon 兩邊一致、舊樣式不污染、SVG 無內嵌影像、Wiki 首頁無時間戳)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -462,7 +462,7 @@ export function wikiHome(slug, code) {
|
||||
.map((k) => `| ${k} | ${ident[k]} |`),
|
||||
`| 長期記憶 | ${longTerm.length} 則 |`,
|
||||
`| 關係人 | ${relations.nodes.length} 位 |`,
|
||||
`| 最後同步 | ${pl.nowIso()} |`,
|
||||
`| 圖示更新 | ${pl.loadConfig(slug).icon?.generated_at || "—"} |`,
|
||||
"",
|
||||
"## 頁面",
|
||||
"",
|
||||
@@ -610,6 +610,56 @@ export async function pullArea(slug, area, { code = null, owner = null, force =
|
||||
return { ok: true, area, code: theCode, changed: incoming, written };
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證某一區「本機 = 遠端」。
|
||||
* push 回報成功不等於遠端真的有東西(網路中斷、權限、非快轉都可能),
|
||||
* 形象圖這種一定要出現在 Wiki 的檔案更需要一個明確的檢查點。
|
||||
*/
|
||||
export async function verifyArea(slug, area, { code = null, owner = null } = {}) {
|
||||
const problem = giteaProblem();
|
||||
if (problem) return { ok: false, skipped: true, area, reason: problem };
|
||||
const theCode = code || personaCode(slug);
|
||||
if (!theCode) return { ok: false, skipped: true, area, reason: "沒有人格編號" };
|
||||
const theOwner = owner || (await resolveOwner());
|
||||
const { host } = giteaEnv();
|
||||
const dir = ensureClone(slug, area, repoUrl(host, theOwner, theCode, area));
|
||||
if (!git(["fetch", "--quiet", "origin"], dir).ok) {
|
||||
return { ok: false, area, reason: "fetch 失敗(連不上遠端)" };
|
||||
}
|
||||
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], dir).stdout || "main";
|
||||
const local = git(["rev-parse", "HEAD"], dir).stdout;
|
||||
const remote = git(["rev-parse", `origin/${branch}`], dir).stdout;
|
||||
// 再把工作副本疊上去,看看還有沒有沒推的差異
|
||||
if (area === "wiki") {
|
||||
pl.writeText(path.join(dir, "Home.md"), wikiHome(slug, theCode));
|
||||
pl.writeText(path.join(dir, "Icon.md"), wikiIconPage(slug, theCode));
|
||||
}
|
||||
const files = stageArea(slug, area, dir);
|
||||
const dirty = git(["status", "--porcelain"], dir)
|
||||
.stdout.split("\n").map((l) => l.replace(/^.{2}\s+/, "").trim()).filter(Boolean);
|
||||
git(["checkout", "--", "."], dir);
|
||||
git(["clean", "-qfd"], dir);
|
||||
return {
|
||||
ok: Boolean(local) && local === remote && dirty.length === 0,
|
||||
area,
|
||||
code: theCode,
|
||||
files: files.length,
|
||||
local,
|
||||
remote,
|
||||
pending: dirty,
|
||||
};
|
||||
}
|
||||
|
||||
/** 形象圖(SVG + PNG)是不是真的在 Wiki 上、而且和本機一致。 */
|
||||
export async function verifyIconInWiki(slug, opts = {}) {
|
||||
const res = await verifyArea(slug, "wiki", opts);
|
||||
if (res.skipped || !res.code) return res;
|
||||
const missing = ["icon.svg", "icon.png"].filter((f) => res.pending.includes(f));
|
||||
const dir = syncDir(slug, "wiki");
|
||||
const present = ["icon.svg", "icon.png"].filter((f) => fs.existsSync(path.join(dir, f)));
|
||||
return { ...res, icon_present: present, icon_pending: missing, ok: res.ok && present.length === 2 };
|
||||
}
|
||||
|
||||
/** 建立 Gitea 上的存取庫與 Wiki,並把兩區都推上去。 */
|
||||
export async function initRemote(slug, { code = null, owner = null, private_ = true } = {}) {
|
||||
const problem = giteaProblem();
|
||||
|
||||
Reference in New Issue
Block a user