From cef648db9abff482f371067ba3a317515fa2c085 Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sun, 25 Oct 2026 13:42:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(exif):=20=E6=9E=84=E5=BB=BA=20EXIF=20?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=85=A8=E7=BA=BF=E5=A4=8D=E6=B4=BB=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=8B=A6=E6=88=AA=E5=BC=8F=20loader=20+=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=BB=93=E6=B8=85=E9=9A=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根因双层:①包装层 post-load 的 store.set 被 Astro 7 解析层静默丢弃 → 改拦截 baseLoader 写入内联增强(enrich 在 set 时机完成) ②内容层增量同步的持久化数据仓复用旧空条目 → rm node_modules/.astro 全量重提取(loader 注释已记此坑:改 loader 逻辑须清数据仓或换 loader 名) - 图库 19/21 张参数条实出(镜头/光圈/快门/ISO/焦距/时间) - 同时并入 op-gitea T3 交付:public/admin/ 三件(Sveltia 0.252 pinned 自托管)+ gitea [cors] 台账;其发现的 title omit 坑已在 config 修正 验证: build 28 页;dist/gallery data-exif 19 条含 ISO;E2E 30/30 PASS (staging 29 html) Spec: 2026-10-25_add-terminal-books-admin Co-authored-by: GLM-5.3 --- infra/gitea/README.md | 2 +- src/lib/exif.ts | 55 +++++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/infra/gitea/README.md b/infra/gitea/README.md index 0ec2e6d..25f0551 100644 --- a/infra/gitea/README.md +++ b/infra/gitea/README.md @@ -50,7 +50,7 @@ sqlite 直接热拷贝会撕裂,备份前 `docker compose down` 或用 `sqlite ## 已知坑 -- **[cors] 段已启用(2026-10-25,Sveltia CMS 试水)**:`ALLOW_DOMAIN = https://wait4x.online,http://127.0.0.1:18742`;本地源为 T3 试水临时项,试水收尾应移除;`HEADERS` 必须含 `Authorization`(token 认证跨源必需,默认仅 Content-Type,User-Agent)。依据:docs.gitea.com config-cheat-sheet#cors +- **[cors] 段已启用(2026-10-25,Sveltia CMS 试水)**:`ALLOW_DOMAIN = https://wait4x.online`(试水期的本地源已于收尾移除);`HEADERS` 必须含 `Authorization`(token 认证跨源必需,默认仅 Content-Type,User-Agent)。依据:docs.gitea.com config-cheat-sheet#cors - **不要开 `REQUIRE_SIGNIN_VIEW`**:它会把匿名 `/api/v1/version` 一并 403, 而验收与健康检查都裸 curl 该端点;私有性靠 private 仓 + `DISABLE_REGISTRATION` 保证。 diff --git a/src/lib/exif.ts b/src/lib/exif.ts index a3a107e..76d3a70 100644 --- a/src/lib/exif.ts +++ b/src/lib/exif.ts @@ -87,34 +87,49 @@ export async function resolvePhotoExif( /** * photos 集合加载器 = glob(JSON) + EXIF 构建期补全。 - * entry.data.image 经 z.image() 校验后已是 ImageMetadata(不含源文件路径), - * 因此从 JSON 原文重读 `image` 字符串,按“相对 JSON 文件”定位源图交给 exifr。 * - * 坑:dev 模式下单文件变更由内层 glob loader 的 watcher 增量处理,会绕过本包装层, - * 改图或改 JSON 后需重启 dev server 才能补全 EXIF;SSG 构建每次全量 sync,无此问题。 + * 实现教训(2026-10-25 双日调试定位):Astro 7 内容层在 load() 结束后 + * 会按 loader 的原始写入序列做 schema 解析——包装层在 baseLoader.load() + * 之后用 ctx.store.set 补写的字段会在解析阶段被静默丢弃(exif 全空但 + * 无任何报错的根因)。因此改为**拦截写入**:代理 baseLoader 看到的 + * store,在它 set 每个条目的那一刻异步增强,增强完成的 Promise 收集 + * 后在 load 返回前 await 完毕。 */ export function photosWithExifLoader(base = './content/photos'): Loader { const baseLoader = glob({ pattern: '**/*.json', base }); return { name: 'photos-with-exif', load: async (ctx) => { - await baseLoader.load(ctx); const root = fileURLToPath(ctx.config.root); - await Promise.all( - ctx.store.keys().map(async (id) => { - const entry = ctx.store.get(id); - if (!entry?.filePath) return; - const jsonPath = join(root, entry.filePath); - const raw = JSON.parse(await readFile(jsonPath, 'utf8')) as { - image?: string; - exif?: Partial; - }; - if (typeof raw.image !== 'string') return; - const imagePath = new URL(raw.image, pathToFileURL(dirname(jsonPath) + '/')); - const exif = await resolvePhotoExif(raw.exif, imagePath); - ctx.store.set({ ...entry, data: { ...entry.data, exif } }); - }), - ); + const origSet = ctx.store.set.bind(ctx.store); + const pending: Promise[] = []; + + const enrich = async (entry: Record) => { + const filePath = entry.filePath as string | undefined; + if (!filePath || !filePath.endsWith('.json')) return entry; + const jsonPath = join(root, filePath); + const raw = JSON.parse(await readFile(jsonPath, 'utf8')) as { + image?: string; + exif?: Partial; + }; + if (typeof raw.image !== 'string') return entry; + const imagePath = new URL(raw.image, pathToFileURL(dirname(jsonPath) + '/')); + const exif = await resolvePhotoExif(raw.exif, imagePath); + return { ...entry, data: { ...(entry.data as object), exif } }; + }; + + // Object.create 保留 store 其余方法/绑定;仅拦截 set + const proxiedStore = Object.create(ctx.store); + proxiedStore.set = (entry: Record) => { + pending.push( + enrich(entry) + .then((e) => origSet(e)) + .catch(() => origSet(entry)), + ); + }; + + await baseLoader.load({ ...ctx, store: proxiedStore }); + await Promise.all(pending); }, }; }