登入

更新日誌

每個版本改了什麼——直接來自 changelog。

  1. Download generated media instead of opening a preview tab

    Request: Make Download Video and Photo Restoration download buttons save files locally.

    • Authorize downloads by generation ID, app and owner; exclude deleted, incomplete and moderation-removed results.
    • Sign a five-minute R2/S3 URL with attachment disposition; transfer directly from storage without proxying large videos through Vercel.
    • Keep preview URLs unchanged, preserve true file extensions, and report download preparation/errors in all nine languages.
    • Add signed-header, authorization, trusted-key, client no-new-tab and localization regressions.
  2. Suspend paired photo animation for payment review

    Request: Choose Option 1 after Waffo's review and withdraw two-person synthesis without deleting its implementation or history.

    • Gate paired animation, second-photo uploads and the style selector while retaining single-photo motion.
    • Update single-image upload guidance in all nine public locales.
    • Reject paired references and custom/retired animation prompts before any new job or credit hold; leave existing tasks and R2 results intact.
    • Add release-policy, rendering and service-boundary regressions and a resubmission reply template.
  3. Limit the public release to restoration and photo animation

    Request: Temporarily withdraw the audio-driven portrait tool for payment-provider review, retain its implementation and history, and deploy the revised website.

    • Restrict new Rekindle generation requests to the two currently offered models before provider calls or credit holds; keep historical result recovery unchanged.
    • Remove the disabled workspace tab, restored-photo handoff, hero slide, feature card, sample gallery, pricing benefits and SEO claims across all nine public locales.
    • Publish only active models and services in policy/legal copy; retain the six content restrictions, reporting and moderation disclosures.
    • Keep legacy code and copy behind an explicit release gate, document the approval/re-enablement process, and test both public rendering and direct-request rejection.
  4. Confirm Kie failures before refunding and localize History handoffs

    Request: Recheck failed tasks on each History visit within one hour, confirm final failure after an hour, and show localized wait/refund guidance rather than timeout errors.

    • Recheck provisional failures per visit, coalesce simultaneous lookups, and stop after the final failed response and refund commit atomically.
    • Do not refund known Kie jobs for local timeouts or early failure; retry interrupted settlement idempotently and preserve late-success R2 recovery.
    • Show pending versus finally failed/refunded outcomes in owner and admin history, with an optional API pending flag.
    • Bound browser waiting by wall-clock budgets, and translate each model's wait minutes, AI History destination and refund rule into all nine active languages.
  5. Update website payment disclosures to Waffo Pancake

    Request: Replace the legacy default processor wording in every language and check the rest of the website.

    • Update all nine website languages' Terms and Privacy payment disclosures to Waffo Pancake and refresh the legal revision date.
    • Replace outdated marketing and public documentation copy, including the real merchant/private-key variables and product mapping examples.
    • Remove the retired processor's admin switch; preserve historical order/provider identifiers and compatibility code for truthful reconciliation.
  6. Recover completed Kie jobs after local timeouts

    Request: A restored photo succeeded on Kie but timed out locally; recheck AI History and save successful results to R2.

    • Persist Kie task IDs before responding, claim dispatch atomically and avoid ambiguous create retries.
    • Register app/generation-bound signed callbacks, query the authenticated vendor API instead of trusting callback results, and use Next after() for result transfer.
    • Recheck running/failed Kie tasks on authorized history reads, reload status filters and store recovered results in R2 without changing previously refunded credits.
    • Keep provider/transfer errors retryable, bound network waits and replace the client timeout failure with a History handoff message.
    • Add provider, client polling, submission lifecycle, history and recovery/concurrency regression tests.
  7. Explain payment setup pending approval

    Request: Show a friendly English notice while payment integration is not ready.

    • Translate Waffo's unapproved-store checkout failure into a safe coming-soon message.
    • Show the notice inline and in an informational toast on homepage/pricing; the account billing

    page displays the same server message. Close the unused checkout tab and reset pending state.

    • Preserve real checkout errors and the existing Live configuration; no simulated payment success.
  8. Integrate Waffo Pancake payments and enable Live rollout

    Request: Use the official Pancake SDK and test credentials to exercise checkout and webhooks.

    • Add the server-side SDK provider, raw-body RSA verification, store/mode checks, USD amount

    validation, first/renewal payment handling, cancellation and payment-specific refunds.

    • Keep catalog pricing and existing transactional/idempotent credit grants. Open hosted checkout

    in a detached new tab; never grant credits from a success redirect.

    • Add test-product setup, restricted ngrok webhook gateway, sandbox checkout verification and

    unit/local PostgreSQL regressions. Credentials and captured payloads stay ignored and private.

    • Publish all nine catalog products, configure Production-only secrets and a Live webhook on

    the new Vercel project; retain local Test configuration and existing customer history.

  9. Complete Traditional Chinese website and console copy

    Request: Translate the Traditional Chinese homepage and inspect other pages, including admin.

    • Add complete Traditional Chinese landing, pricing, upload/gallery, legal/policy, docs and

    marketing template content instead of falling back to English.

    • Normalize mixed Simplified/Traditional console dictionaries and localize hardcoded admin

    search fields, table counts, dialogs, account/chat controls, previews and error pages.

    • Add catalog/placeholder regression tests and public/authenticated-local browser checks.
    • Keep technical model names and user-authored content unchanged; no production data migration.
  10. Fix marketing navigation and retire incomplete locales

    Request: Clicking homepage menu items repeatedly appends old fragments to the URL. Remove the unfinished Russian, Hindi and Indonesian website languages instead of translating them.

    • Use native anchors for header/footer fragment links, avoiding the router's cached canonical

    URL fragment concatenation. Keep Next.js links for regular page navigation.

    • Preserve locale prefixes on custom footer section links as well as header links.
    • Add browser regression checks for sequential/repeated clicks from existing/broken hashes,

    back/forward, keyboard access, cross-page navigation, mobile footer links, workspace retention and navigation without JavaScript.

    • Remove ru/hi/id from website language selectors, locale cookies, sitemap and hreflang links.

    Redirect existing prefixed URLs to English, preserving their path and query. Keep archived translation dictionaries and the separate mobile app unchanged.

    • Move the active AI model disclosure below the upload card for all three workspace tabs,

    retaining live model/tier updates and the content-policy link.

  11. Synchronize billing contract tests before GitHub delivery

    Request: Commit and push the verified Rekindle migration and fixes to GitHub; synchronize the billing tests and add late-payment/repeated-notification credit regression coverage.

    • Align stale order/subscription test matrices with the already documented transitions added

    in aed09cc; preserve the existing payment/credit transition rules.

    • Cover late confirmed payment on expired orders, prohibit reopening failed/canceled/refunded

    orders or downgrading paid orders, and cover trial/dunning cancellation transitions.

    • Clarify that expired orders are not terminal; expired subscriptions remain terminal.
    • Add isolated local PostgreSQL integration tests through the actual webhook route, event

    claims and fulfilment: late payments, sequential/concurrent duplicates, rollback and retry, crash recovery, amount/currency mismatches, terminal guards and app isolation.

    • Fix stale webhook-claim takeover: use an atomic age predicate instead of comparing a

    PostgreSQL microsecond timestamp with its millisecond-only JavaScript Date representation.

    • Exclude private configuration, database exports, credentials and generated build artifacts.
  12. Align production branding, pricing and Google avatars

    Request: Restore the local orange-red theme online, make /pricing match the homepage, and store Google login avatars in Cloudflare R2.

    • Carry NEXT_PUBLIC_BRAND_THEME into production releases; share pricing cards, tabs, catalog

    mapping and localized copy between both pages, preserving page metadata and FAQ structured data.

    • Cache Google profile images as private, app/user-scoped WebP objects; backfill existing Google

    image links on authenticated header reads. Preserve custom avatars and concurrent uploads.

    • Bound downloads/uploads, validate Google hosts and image decoding, suppress sensitive logs,

    and keep login usable on storage failures with an initial-letter image fallback.

    • Add unit tests and repeatable local/live UI checks plus a real Google-to-R2 browser smoke test.

    Minimal core seams: OAuth callback, web session display endpoint and optional storage abort signal.

  13. Deploy Rekindle to a separate Vercel project

    Request: Upload the new app to a new Vercel project, configure its environment variables, and leave the existing site and manual domain binding unchanged.

    • Deploy an allowlisted source snapshot with independent authentication secrets and a restricted

    runtime database role; exclude local env files, backups and database dumps from uploads.

    • Keep only the app's AI reconciliation cron; leave legacy payments and unrelated template

    automation disabled pending their own rollout.

    • Verify the real deployed password/session/credit/history flow using a cleaned-up temporary

    account, and document production environment configuration and manual domain-cutover steps.

  14. Import legacy data into an isolated Neon database

    Request: Preserve existing users, orders, credits and generation history while preparing the new deployment, without changing the old production database.

    • Add read-only consistent snapshot, guarded transactional mapping/import and verification scripts.
    • Preserve original records in a restricted archive; retain balances and expiry dates without

    replaying payments or granting credits again. Validate a separate local restore.

    • Support old password hashes with automatic Argon2id upgrade on successful login and preserve

    the registered Google callback path through the new auth handler.

    • Add mapping/password/callback tests and an isolated new-database authentication smoke check.
    • Document migration evidence, unavailable historical media, and the outstanding payment,

    source-delta and production cutover checks. No production domain switch is included.

  15. Use the Rekindle brand assets throughout the web app

    Request: Restore the legacy logo and favicon instead of the default Z mark.

    • Reuse the legacy Rekindle logo and favicon; add Apple touch and 192/512px install icons.
    • Update the header, footer, authentication and console branding, page metadata, manifest

    and social sharing card to Rekindle. Product brand values live in apps/photorevive/brand.ts.

    • Shared UI and App Router metadata files reference the product assets; auth and billing

    behavior are unchanged.

    • Fix local login's Unknown or disabled app: app_a by setting both public and server default

    app slugs to photorevive; document the paired variables in the environment template.

  16. 老照片修复迁移(十四):隐藏退款页,只保留 Privacy 与 Terms

    「隐藏」需要动四处,少一处都不算完:

    1. 路由本身 —— 只去掉链接不够:/legal/refund 仍可直接访问,而它挂的是模板占位的退款政策,公开可达比断链更糟(还会被搜索引擎收录)。已删除该路由。
    2. 页脚链接(MarketingFooter.tsx)。
    3. sitemap 条目(sitemap.ts)——否则搜索引擎会持续抓取一个 404。
    4. ⚠️ 移动端入口(mobile/app/(tabs)/profile.tsx:382)—— 这条容易漏:App 的「退款政策」行用 WebView 打开的正是同一个 web 路由(LegalPage 的 ?embed=1 模式就是为它做的)。只删网页会让 App 里点出 404。已一并移除。

    顺带清掉三个已死的占位模块: legal/privacy/content.ts、legal/terms/content.ts、legal/content.ts。上一条把两个法务页改为 markdown 渲染后,它们已无人引用;退款页删除后 legalDoc/LegalContent 也彻底没有消费者。这次选择删除而非保留——与之前「上游文件不删以免冲突」的处理不同,理由是:它们装着占位法务文本、就摆在真实法务页旁边,下一个想改隐私政策的人会很自然地打开 privacy/content.ts 改半天,然后发现页面纹丝不动。这个陷阱比合并冲突更值得规避。

    验证(实跑): /legal/refund → 404;/legal/privacy、/legal/terms → 200;sitemap 中 refund 消失、privacy/terms 保留;首页 legal/refund 链接数 0。构建路由表只剩 privacy + terms。mobile tsc 通过(我改了 profile.tsx)。web 390 passed;typecheck / eslint(0 error)/ build 全绿。

    报备(范围外): 页脚品牌仍是硬编码的 "zmlkit",版权行仍是 "© zmlkit · MIT-style template" —— 这是模板的默认值,出现在你的产品首页页脚。上一轮我给页头做了 brand 参数,页脚的品牌与版权尚未参数化。要改说一声。

  17. 老照片修复迁移(十三):隐私政策与服务条款换成旧站真实内容

    旧站内容在哪: content/pages/*.mdx,8 语言 × 2 文档共 16 个文件(en/zh/ja/ko/fr/pt/es/de),走 /privacy-policy、/terms-of-service。经检查是纯 markdown,无 JSX/import。

    两个不能直接照搬的问题:

    1. 结构表达不了。 zmlkit 的法务页是 LegalSection {heading, body: string[]},LegalPage 把每条 body 渲染成裸 <p> 纯文本。而旧站内容有 43 条列表、37 处粗体、6 个子标题、4 个链接(隐私)——塞进去会把 **粗体** 原样显示、列表全塌成段落。
    2. 免责横幅会误导。 每个法务页都顶着 mkt.legalDisclaimer:「This is a starter template, not legal advice. Replace the placeholders…」。旧站这两份是真实生效的文档,挂这条横幅是对用户的误导。

    做法: zmlkit 已有自研的 <Markdown />(无依赖、构造上防 XSS、docs 与 blog 都在用),其支持的语法 恰好覆盖旧站所需(h1–h6 / **粗体** / [链接](url) 协议校验 / - 列表)。故内容以 markdown 原样保留(apps/photorevive/legal-content.ts,脚本从 MDX 提取,12 语言、缺失 4 语言回退英文),用 <Markdown /> 渲染。

    模板改动(对上游友好): 给 LegalPage 加两个可选项,默认值完全保持模板原样:children(自定义 body,替代 sections)与 disclaimer(默认 true;真实文档设 false)。任何替换掉占位法务文本的下游项目都需要这两个接缝。「最后更新」日期改用旧站 frontmatter 的 created_at(2025-10-24),而非模板的硬编码日期。

    验证(实跑): /legal/privacy 与 /legal/terms 均 HTTP 200:旧站正文命中、粗体渲染为 <strong>、列表渲染为 <li>、链接可用、字面 ** 未出现、占位横幅消失、zmlkit 的 [Company Name] 占位消失、日期为 2025-10-24。/zh、/ja、/de 三语实跑均渲染本地化正文且无英文泄漏。390 passed;typecheck / eslint(0 error)/ build 全绿。

    留下的痕迹: 模板的 legal/privacy/content.ts 与 legal/terms/content.ts 现已无人引用。故意不删——上游文件,删除只会在合并模板更新时制造冲突(legal/content.ts 仍被退款页使用,必须保留)。退款页仍是模板占位内容,旧站没有对应文档,未动。

  18. 老照片修复迁移(十二):修 Tabs 高度溢出 + 登录按钮归入品牌色

    Tabs 高度:根因和我最初的推断不同,是实测量出来的。 用 Playwright 量真实 DOM:

    listH: 36px          ← TabsList 被钉死在 36px,我写的 h-auto 没生效
    triggers: 41 / 49px  ← 触发器比列表还高,溢出容器

    两边组件的设计前提完全不同:旧站 TabsTrigger 无任何高度约束(px-3 py-1.5 撑开);zmlkit 的是 h-[calc(100%-1px)],配合 TabsList 的固定 h-9 使用。我照抄旧站写法给 List 加了 h-auto——但 zmlkit 用的是 group-data-[orientation=horizontal]/tabs:h-9,variant 选择器特异性更高,且 tailwind-merge 视其为不同 key 而不去重,于是 h-9 胜出。列表底色只有 36px,选中药丸却是 41/49px,看起来就是「未选中区域高度不对」。

    修法: List 用同前缀的 group-data-[orientation=horizontal]/tabs:h-auto(同特异性,tailwind-merge 能识别冲突并丢掉 h-9);Trigger 加 h-auto 中和 h-[calc(100%-1px)](无 variant,直接去重)——回到旧站模型:高度由内容与 padding 决定。

    实测结果: 工作区 42+8(padding)+2(border) = 52 ✓ 精确吻合;定价 50+8 = 58 ✓。三个触发器等高,选中/未选中一致,1px 余量消失。

    登录按钮: AuthChip 有三处硬编码 indigo(登录按钮、头像悬停光环、头像占位底色)。改为 --accent / --accent-contrast——文字同样不能写死白色(暗色品牌橙上白字仅 2.6:1,见上一条)。实测按钮计算样式为 #e54e27 橙底 + #fff8ea 近白字,正是旧站配色。

    未处理(范围外,报备): 模板中仍有 51 处硬编码 indigo,分布在 31 个文件——(console) 37 处 / (marketing) 9 处 / components/marketing 4 处 / components/console 1 处。多为 hover 边框等细节,但 settings/profile、settings/orders 等是用户会用到的页面。这是一次独立的清理,未擅自扩大范围。

  19. 老照片修复迁移(十一):恢复旧站橙色配色 + 灯箱点遮罩关闭

    配色为什么变蓝了: 我用的是 shadcn 语义 token(bg-primary),而 zmlkit 把 primary 映射到自己的 --accent(indigo #6366f1)——配色自然跟着模板走了。旧站 theme.css 是 --primary: oklch(0.62 0.19 35)(注释写着「orange/coral hsl(20 90% 48%)」)。

    做法: zmlkit 有 NEXT_PUBLIC_BRAND_THEME 驱动的品牌预设机制([data-brand='…'],已有 emerald/amber/rose),这是官方指定的接缝。故新增 [data-brand='rekindle'] 预设并置 env,不覆盖模板的默认 indigo(覆盖会在 pull 模板更新时冲突)。

    ⚠️ 不能只改 --accent(实测数据): 把 oklch 转 sRGB 算 WCAG 对比度后发现——旧站暗色模式用的是更浅的橙(oklch(0.72 0.17 45))配深色文字(其 --primary-foreground: oklch(0.28 0.08 35)),得 5.7:1;而 zmlkit 的 --color-primary-foreground 硬编码白色,白字压在那个浅橙上只有 2.6:1——照搬橙色会比旧站还难读。故新增 --accent-contrast 变量,primary-foreground 改为跟随主题;默认值仍是白色,模板行为不变。--accent-fg(正文强调色)也按对比度选:浅色 5.2:1、暗色 8.3:1(品牌橙本身在浅底只有 3.7:1,不能直接当正文色)。

    顺带修: PlansCheckout.tsx(用户会用到的账单页)有 4 处硬编码 indigo(结算按钮、选中边框、聚焦环),品牌变橙后会残留蓝色。已改为跟随 --accent/--accent-contrast。/chat、/create 里也有硬编码 indigo,但那是模板演示页、本产品用不到,未动。

    文档: ui-conventions.md 的「primary → --accent(fg 固定白)」已成事实错误,改为「fg = --accent-contrast」并说明原因;web.env.example 原本完全没有 NEXT_PUBLIC_BRAND_THEME(功能存在但无文档),已补上并列出预设。

    灯箱:不是 bug,是模板的刻意设计。 用 Playwright 真实浏览器测出:点遮罩和 Escape 都不关闭(data-state 仍为 open),但关闭按钮正常。根因在 DialogContent——模板对三个 dismiss 事件全部 preventDefault,注释写明是「console-wide anti-misclick default:避免误触丢失半填的表单,调用方可传自己的处理器恢复」。这个默认值是对的,不能改(改了后台表单就危险了)。故按其文档给的逃生口,在 lightbox.ts 里为两个画廊显式恢复——它们展示的是图片/视频,没有可丢失的输入。

    验证: 新增 e2e/landing.spec.ts(真实 Chromium):匿名访客拿到产品而非登录墙且 5 个 section 齐备;前后对比灯箱关闭按钮与遮罩点击都能关;视频灯箱遮罩点击能关。3/3 通过。实跑确认 <html data-brand="rekindle">、发货 CSS 含 --accent: #e14d28(浅)/ #f97c3d(暗)+ 对应 --accent-contrast,且模板 :root 仍是 indigo。web 390 passed;typecheck / eslint(0 error)/ build 全绿。

    过程记录(一次自我纠正): 我最初推断「Radix 默认就会点遮罩关闭,应该已经能用」——推断是错的,真实浏览器立刻证伪。后续两次「加探针就好了/移除就坏了」的反复也说明:这类行为必须实测,不能靠读代码推。

  20. 老照片修复迁移(十):落地页设为站点首页,品牌与导航归位

    • (marketing)/[locale]/page.tsx 从模板的 block 驱动演示首页改为渲染 Rekindle 落地页。这个项目里产品即站点,/ 就该是产品,与旧站一致。改为 dynamic = 'force-dynamic'(原 revalidate = 600)——页面要读 session 决定在何处提示登录,ISR 与按访客状态渲染不兼容。JSON-LD 从 zmlkit 的 SoftwareApplication/DeveloperApplication 换成 Rekindle 的 MultimediaApplication,价格区间取自 PLAN_CATALOG.photorevive。
    • 删除上一轮的 /photo 路由并撤回其 LANDING_SLUGS 注册:/ 与 /photo 内容完全相同即重复内容,SEO 权重会被拆开。

    顺带发现并修复的三处(我上一轮引入 / 模板遗留):

    1. 我的落地页没有页头页脚:模板首页会渲染 MarketingHeader + MarketingFooter + JsonLd,而我上一轮的 /photo 三者皆无——是个没有导航的裸页面。已补齐。
    2. 品牌名硬编码 "zmlkit":MarketingHeader 左上角写死模板名,应显示旧站 landing.json 的 brand.title = Rekindle。
    3. 导航指向模板的演示页:header 与 footer 都链到 /showcase、/templates——这个产品没有这些页面。旧站导航是 6 个本页锚点(Generate/Features/How It Works/Gallery/Pricing/FAQ,逐语言翻译,存在 landing.json)。

    做法(对上游友好,不硬改模板行为): 给 MarketingHeader 加可选 brand / nav、给 MarketingFooter 加可选 productLinks,默认值完全保持模板原样;由首页传入产品自己的品牌与导航。这是任何下游产品都会需要的接缝,而非 photorevive 专用 hack。hrefFor 让 /#anchor 保持锚点、普通路径走 locale 前缀。i18n 提取器扩展为一并抓取 landing.json 的 brand 与 nav(12 语言)。

    验证: 匿名访客 GET / → HTTP 200,117KB:品牌显示 Rekindle(非 zmlkit)、6 个导航锚点齐全且页内 6 个 id 全部存在(generate/features/how-it-works/gallery/pricing/faq)、/showcase 与 /templates 已从全页消失、工作区在页面上、无登录表单、页脚存在。/zh → 中文全渲染(立即生成/效果展示/让老照片/照片修复/准备好修复您的回忆了吗)。web 390 passed;typecheck / eslint(0 error)/ build 全绿。

    留下的痕迹: 模板的 src/marketing/content/home/*.json 现已无人引用(首页不再走 block 渲染)。故意不删——那是上游的文件,删除只会在下次 git pull 合并模板更新时制造冲突。

  21. 老照片修复迁移(八):自查三条链路,修复 4 处我自己引入的缺陷

    结论:没有完全还原。 系统比对后发现 4 处缺陷,全部由我的 port 引入:

    1. 修复 → 转视频 / 说话 的交接彻底断了(主漏斗)。旧站 handleGoToVideo/handleGoToTalk 会把修复好的照片连同 restoredImageUrl 带到下一个 tab(跳过重传);我的实现只有 setActiveTab('animation'),什么都没带。用户修完照片点「照片转视频」会落到空白上传页,得手动下载再上传。
    2. runAnimate 里有死代码:restoredUrl && videoFiles.length === 0 永不成立(函数开头即 if (videoFiles.length === 0) return),而我的注释还在描述这个不存在的行为——注释在撒谎。
    3. VideoProcessingOverlay 整个漏了:旧站在生成期间给源照片盖扫描线遮罩 + 百分比(动画 tab 与说话 tab 各一处)。
    4. 动画照片卡样式不符:旧站 aspect-[3/4] + 左下角序号徽章 + 生成时隐藏删除按钮;我写成了 h-48、无徽章、仅 disabled。

    修复:

    • 新增 handoff.ts:carriedPhoto / pickedPhoto / resolveSourcePhoto。交接改为直接传存储 URL——修复结果本就在我们的存储里,无需像旧站那样经 /api/storage/download 把字节拉回来再传上去(旧站那么做只是因为它的上传路径必须要 File)。更快,且 zmlkit 本就没有该代理。
    • 交接同时清理目标 tab 的状态(旧站行为);新选照片会清掉 talkStoredUrl,否则会拿旧照片去生成。
    • 补回遮罩层与照片卡样式。
    • 积分不足:旧站客户端预检 user.credits < required 并显示「需要 X,你有 Y」。直接照抄有坑——余额是页面加载时的快照,生成几次后过期,会误挡实际有钱的用户。改为在服务端 credits/ai.ts 的 CREDITS_INSUFFICIENT 错误里带上 {required, available}(模板改动,对任何 app 都有用),客户端渲染同一条旧站文案但用权威实时数字。

    关于测试的一次自我纠正: 我先写了一个「测试」,它把组件逻辑重新实现一遍再断言自己的复刻品——组件真坏了也照样绿。这是测试表演,比没有更糟。删掉后改为把决策逻辑真正提取到 handoff.ts,让组件调用它,测试测的是组件实际执行的代码路径。仓库无 DOM 测试环境(无 jsdom/testing-library,且从不测组件),这是此处能做到的最真实的保障。

    验证: web 390 passed(41 文件,原 383,新增 7 例覆盖交接:carried 必须 reuse 不重传、picked 必须 upload 且绝不继承 storedUrl、两者都缺时报 missing 而非假装、修复结果必须与受损原图区分);typecheck / eslint(0 error)/ build 全绿;实跑 /photo 仍渲染 164KB。

    未能端到端实跑的部分: 交接需要一次真实的 Kie 修复才能走通,那会消耗你的厂商额度与真实积分,未经同意不做。

  22. 老照片修复迁移(六):补回说话照片的画质选择器,并让它名副其实

    取舍(只做明确正确的,不做投机的): ①画质选择器:做(线上正在骗用户,且我三期的 port 把它整个漏了);②修复/上色分开定价:不做(生产值本来都是 5,为从未用过的能力改架构是投机,且旧站那个能力靠客户端 scene 定价、本身可篡改);③clean-room:你指派了这次迁移,即为授权,记录在案;④账本 UI 标签:数据已保住,显示要改模板 API、价值低,仍留给你。

    发现: 三期的 PhotoRevive.tsx 漏掉了旧站的 720p/1080p 选择器(grep 无任何匹配)——这本身就是 1:1 的缺口。而旧站那个选择器是假的:请求不带 resolution、服务端省略参数恒按 720p 收 8 分/秒、Kie 的 avatar 任务根本没有 resolution 入参。选 1080p = 显示 16 分/秒、实收 8、产出仍是标准画质。

    • models.ts 新增 TALK_TIERS:standard → kling/ai-avatar-standard、pro → kling/ai-avatar-pro。真实的画质轴是模型,不是分辨率,所以 tier 直接映射模型。
    • audio.ts 的 CREDITS_PER_SECOND 改为按 tier 的费率表(standard 8 / pro 10),estimateTalkCredits(sec, tier)。
    • UI 补回选择器(保留旧站的双卡片布局与 talk.resolution / talk.creditsPerSecond 键),但标签换成诚实的 Standard / Pro——「720p」「1080p」命名的是一个从不存在的入参,继续用它等于对用户撒谎。新标签放 i18n-extra.ts(旧站没有的键)。
    • 提交时 model: TALK_TIERS[talkTier]——tier 就是模型,这是唯一真正改变产出的东西。

    结果对比: 旧站选 1080p:显示 16/秒 → 实收 8 → 标准画质。现在选 Pro:显示 10/秒 → 实收 10 → 确实更好的模型。用户付得比旧站宣称的少,且拿到真东西。

    验证:

    • 测试 383 passed(原 378):费率钉死改为 it.each 覆盖两个 tier(各自比对 AI_COSTS.perModel 与目录上限);新增「pro 必须比 standard 贵且模型必须不同」——钉住「选择器必须买到真东西」这个属性,防止假选项复活。
    • 实跑双 tier 计费:同一段 6 秒音频,kling/ai-avatar-standard → creditsCost=48(6×8)、kling/ai-avatar-pro → creditsCost=60(6×10),均由服务端探测得出。
    • 选择器在 talk tab 的「已上传照片」分支内(与旧站一致)+ Radix 非活动 tab 不渲染,故初始 HTML 中无此节点;改为核查发货产物:客户端 chunk 内含 Natural lip-sync / Sharper lip-sync & expression 与 kling/ai-avatar-standard + kling/ai-avatar-pro 两个模型。
    • typecheck / eslint(0 error)/ build 全绿。
  23. 老照片修复迁移(三):photo-upload 三条链路 UI 落地 + 端到端实跑验证

    关键发现(大幅降低难度): docs/ui-conventions.md 写明 zmlkit 本就以「CLI 复制式」引入 shadcn/ui(源码在仓库内,不违反 clean-room),且 shadcn 的 Tailwind token 通过 @theme inline 映射到 zmlkit 变量。旧站那套 bg-muted/text-muted-foreground/border-primary 的 class 因此可直接生效——视觉 1:1 无需逐个换算配色。

    • 按文档规定方式 pnpm dlx shadcn add progress radio-group textarea 补齐缺失基元(自动使用仓库既有的 radix-ui 统一包写法)。
    • apps/photorevive/ui/PhotoRevive.tsx:三个 tab(修复/上色、合影动画、说话照片)完整交互——拖放上传、前后对比滑块、动画风格选择、录音(到 15s 自动停)、试听、超长裁剪、进度模拟、结果下载。标签结构、Tailwind class、提示词、流程顺序与旧站一致。
    • apps/photorevive/ui/api.ts:收敛 /api/ai/uploads → /api/ai/generate → 轮询 /api/ai/generations/[id] 的契约。旧站的 scene 与客户端 costCredits 均已取消——zmlkit 由服务端按目录定价。轮询可中断(卸载/重置即停),瞬时读取错误吞掉重试(用户已付费的任务不能因一次网络抖动失败)。
    • apps/photorevive/audio.ts:录音/裁剪/时长/文本时长估算。客户端积分只是展示预估,代码与注释均写明服务端为准;配测试把预估与服务端 AI_COSTS + 目录上限钉死,任一侧改率另一侧不同步即测试失败。
    • i18n:81 键 × 12 语言,脚本从旧站 JSON 提取(避免手抄 664 条字符串出错),app 自持(apps/photorevive/i18n.ts),不动模板的 messages.ts。旧站只有 8 语言,缺的 4 语言(zh-Hant/ru/hi/id)按键回退英文而非露出裸 key。
    • i18n-extra.ts:旧站有 4 处硬编码英文字面量(如 toast.error('File too large. Max 10MB.'))从未走 i18n;英文逐字保留(英文用户零变化),但现在可翻译。与生成文件分离,重跑提取器不会覆盖。
    • 新页面 /photo((console)/(app)/photo):薄壳,价格由服务端读 AI_COSTS 注入,客户端永不作为定价事实源。
    • 顺带修正:register.ts 的 PHOTOREVIVE_MODELS 引用。

    端到端实跑验证(非仅 typecheck):

    1. 真实浏览器会话(注册 → cookie → GET /photo):142KB HTML 完整渲染——标题、三个 tab、拖放区、上传提示全部命中,无登录表单。
    2. 防篡改计费实证:客户端传 durationSec: 1(若被采信只需 8 分),服务端探测真实 6 秒 WAV(本地静态服务器提供),实收 creditsCost: 48(6×8)。
    3. 三条链路价格与旧站一致:修复 5、4s 动画 30、6s 说话 48。
    4. 必填守卫在真实 HTTP 下正确触发:缺 audioUrl / 缺 imageUrl / 未知模型 均 400,且扣费之前拒绝。
    5. 积分守恒:假 Kie key 使任务失败 → 三笔 hold 全部自动 refund;账目 917 + 48 + 5 + 30 = 1000,无损耗。

    验证: web 373 passed(39 文件);typecheck / prettier / build 全绿;/photo 出现在构建路由表。

    尚未完成: gallery 与落地页 blocks(迁移四期);1080p 选项的处理待决策(见一期记录)。

  24. 老照片修复迁移(二):photorevive 接入骨架 + 提示词与图片几何逻辑

    • 按 docs/build-new-app.md §4 建 web/src/apps/photorevive/,并在 apps/registry.ts(唯一允许下游改的模板相邻文件)注册。自定义代码全部收在该目录内,模板核心保持可合并。
    • register.ts:启动钩子校验 Kie key(缺失只 warn 不 throw——注册表隔离钩子异常,未配置的 App 不该拖垮同进程的其他 App)。
    • models.ts:三条链路 → 目录模型的映射(restore→seedream/4.5-edit、animate→bytedance/seedance-1.5-pro、talk→kling/ai-avatar-standard),即旧站发给 Kie 的原样 id,保证产出视觉一致。配 models.test.ts 防目录漂移:模型必须存在、必须路由到 kie、必需入参与计费必须齐备——否则上游改名只会在用户点击时才炸。
    • prompts.ts:修复/上色/合影动画/说话照片的提示词逐字迁移。提示词是模型输入而非 UI 文案,故不做 i18n——改写措辞会静默改变产出。
    • image.ts:1080p 下采样(上传前,老照片扫描件常 20MP+)、最近宽高比吸附、seedance 不支持的 3:2/2:3 → 4:3/3:4 回退。配 9 例测试,其中 2000x4000 → 540x1080 专门钉住原实现里「按宽缩放后仍超高、需改按高缩放且基于原始尺寸换算」的两段式逻辑。

    验证: web 363 passed(38 文件);typecheck 与 build 通过。

    尚未完成(迁移三期): photo-upload 的 UI 主体(三条链路的交互、录音/试听/裁剪、TTS、结果区)、gallery 与落地页 blocks、以及端到端实跑验证。

  25. 分享赚佣金:授权制佣金体系(与邀请积分正交)+ 管理端账本

    先确认的三个岔路(决策记录):

    1. 佣金比例存哪 —— 与 PRD §4 铁律「配置全部走 env、后台设置页只读」正面冲突。你选守铁律:比例按 APP_<SLUG>_COMMISSION_RATE_<TIER> 走 env,后台只读。
    2. 出金模型 —— 你选只记账、后台手工标记已结算(不做提现/余额/收款账户,避免 PII 与合规负担)。
    3. 计提规则 —— 「仅首购」与「续费也计」被同时勾选,互斥;不在钱的规则上替你猜,改为 env 开关 APP_<SLUG>_COMMISSION_ON=first_purchase|all_orders(默认首购,与既有 REFERRAL_REWARD_ON 同套路),两种都能要。

    分层: 邀请积分(人人可得、站内价值)与佣金(授权制、真钱)正交并存,同一人可两者都拿。

    • env 配置(apps.ts):COMMISSION_RATE_<TIER>/_DEFAULT 百分比 → 内部 basis points 整数运算(无浮点漂移,>100% 夹到 100%);COMMISSION_ON。任一比例>0 即启用计划,但仍需逐人授权。
    • 新表 affiliate(授权:status/note/authorized_by/revoked_*,唯一 (app_id,user_id))与 commission(账本:base_amount/rate_bps/amount/status,唯一 (app_id,order_no)),三方言 + pg/sqlite 迁移。
    • commissions/service.ts:accrueCommission(基数=实付金额;比例按套餐 tier,无 tier 用 DEFAULT;首购/全单范围;$0 试用不计;未授权不计;自邀防护)、commissionAmount 纯函数(四舍五入 + 封顶实收)、reverseCommissionForOrder(退款冲销,含已结算行)、settleCommissions(CAS 只动 pending)。
    • 计提挂载 grant.ts 三处(web 首购 / RevenueCat / 续费)+ reclaimRefund 冲销;全部 fire-and-forget,佣金故障绝不阻断履约。
    • 退款释放「首购名额」:冲销行不占位,否则推广者会因对方退款永久失去该用户的计佣机会。
    • 管理端 /admin/commissions(新页 + HandCoins 图标):按推广者邮箱/状态/App/日期区间筛选,三档汇总,勾选批量标记已结算;结算按每行所属 App 逐一复核权限(平台管理员跨 App 视图下防越权)。
    • 授权入口 /admin/users/[id] →「佣金推广权限」卡;无 env 比例时提示「授权也只会得到 0」。
    • 推广者视图 /settings/referral:待结算/已结算/好友数 + 每位好友的注册时间、是否购买、购买的会员类型、购买日期、为我赚了多少。邮箱仍脱敏(沿用既有隐私立场)。
    • 邀请积分后台开关:feature_toggles.referralCredits kill-switch(默认开,只有显式 false 关闭;env 定金额、后台定启用);关闭后邀请关系仍归属,佣金与邀请图谱照常。/admin/settings → 增长。
    • 新权限 app.commission.manage(独立于 orders.read——看谁该拿钱是更小的圈子);新审计动作 3 个。
    • 文档:coupons-referrals.md 新增 §3(分层对比、env、8 条计提规则、出金、界面、验收清单)、database-design/rbac-matrix/env 示例同步。

    验证: web typecheck + build;单测 35 文件 331 例全绿(含新增 5 例佣金金额数学:四舍五入、封顶实收、零基数/零比例、1 分订单不产生幻影佣金)。并对真实数据库跑通完整链路(造邀请人/被邀请人/订单,5 项全 PASS 后清理):未授权不计佣 → 授权后按 tier 比例计佣(999×20%=200 分)→ webhook 重投不重复 → 首购范围忽略第二笔 → 退款转 reversed。

    ⚠️ 升级动作: 需执行迁移(affiliate/commission 两表)+ 重跑 pnpm --filter web db:seed(新权限码,否则佣金入口静默不显示)。

    日期: 2026-07-15


  26. 安装 expo-clipboard;模板风格提示词支持一键复制

    背景: 上一条里我为「复制提示词」按钮考虑过 expo-clipboard,但因其为原生模块(需重做 dev build)而放弃,改用 selectable 文本。现按需求装上——且它与既有的邀请分享(profile.tsx → /api/user/referral → 系统 Share 面板,docs/coupons-referrals.md)同属一条产品线,后续「复制邀请码/邀请链接」可直接复用。

    • mobile/package.json 新增 expo-clipboard@~56.0.4(经 npx expo install 由 SDK 56 自动对齐,非手填版本号)。
    • template.tsx:风格提示词卡片右上角加「复制」按钮,点击后闪现「已复制」1.5s。定时器用 ref 持有并在 unmount 时清理——否则闪现期间离开该屏会对已卸载组件 setState。
    • i18n:mobile 12 语言新增 common.copy / common.copied。

    注意: expo-clipboard 是原生模块,需重新 dev build 后生效(可与 d5ad8a0 的 expo-system-ui 合并在同一次构建里)。

    验证: mobile tsc + expo lint(0 error,16 个既有警告数量未变)。

    日期: 2026-07-15


  27. 移动端模板携带风格提示词,「使用该模板」自动填充

    约定: 模板内容的 BODY = 该模板的风格提示词(即产生封面图那个效果的提示词)。用 body 而非 prompt: 标签,因为提示词是含逗号/冒号的长自由文本,而 tag 是短 key:value 芯片,塞进去会被解析破坏且后台难编辑。ContentItem.body 本就存在且 /api/content 已返回,只是模板一直没用它。

    • content/actions.ts:actionGo(action, prompt?) 把提示词透传给 create-* 目的地(chat/credits/gallery 无提示词,忽略)。
    • create.tsx:读取 prompt 参数作为提示词输入框初值(仅播种,用户可自由改写)。/create 是每次 push 新建的栈屏(非 tab),故每次进入都会重新初始化。
    • template.tsx:详情页展示风格提示词(selectable 可手动选中复制),CTA 携带提示词跳转。
    • 首页 TemplateVM 增加 prompt,CMS 模板取 c.body,内置模板各自内置英文风格提示词(提示词是模型输入而非 UI 文案,故不做 i18n)。
    • 顺带修正内置演示数据的不一致:内置模板原先全部硬编码跳 create-image,导致「音乐片段」也跳图片生成;现各自带 action,音乐 → create-audio,提示词与目的地一致。
    • 后台补齐可编辑入口(关键):banner|template 类型走 isHome 分支,原本根本不渲染 body 输入框——约定将无法填写。现在模板类型在首页卡片字段组内新增「风格提示词」文本框(含占位与说明),banner 不显示。
    • i18n:mobile 12 语言加 tpl.stylePrompt;web 12 语言加 3 个 form.stylePrompt* 键。

    验证: web typecheck + build、mobile tsc + expo lint(0 error)通过。实跑数据链路:向本地库插入一条带 body 的 type=template 内容,curl /api/content?type=template 确认 body 原样返回移动端(BODY REACHES MOBILE: True),随后删除验证数据并确认接口不再返回。

    日期: 2026-07-15


  28. 管理端手工赋予会员(月/年/终身/自定义起止)+ 会员派生单测

    设计取舍(修正上一条的「零新表」): 「付费会员从订单派生、不落库」这条原则对付费依然成立,但它的前提是「有一条支付记录可派生」。管理员赠送的会员根本没有支付——伪装成假订单会污染营收统计(paidAmount 进报表),伪装成假订阅会让用户账单页渲染「取消订阅」按钮并调用一个不认识它的 provider。因此新增 membership_grant 表专门承载无支付的权益:付费永远派生、绝不落库;赠予落库、绝不伪装成付费。

    • 新表 membership_grant(三方言 schema 一致 + postgres/sqlite 迁移;sqlite↔postgres 行类型对等断言自动覆盖):tier、starts_at、ends_at(NULL=终身)、status(active/revoked,撤销留痕不删行)、note/granted_by/revoked_by,索引 (app_id,user_id,status)、(app_id,status,ends_at)。
    • web/src/server/membership-grant.ts(新):grantMembership()(校验 tier 必须存在于该 App 阶梯,否则拒绝——否则授予了却没有 rank/label,永远满足不了任何 gate;校验 end>start)、revokeMembershipGrant()(幂等 CAS)、listUserGrants()、resolveGrantEnd()(月/年按自然月年推算,终身=null,自定义直取)。
    • deriveMembership() 新增第三来源 grants:仅 status='active' 且已到 starts_at(支持未来生效)且未过 ends_at 才计入;赠予 renews=false。与付费叠加而非互斥(按既有 rank/终身/到期晚的取胜规则),补偿 30 天不会缩短用户既有付费会员。/admin/memberships 名册同步纳入赠予会员。
    • 管理端 /admin/users/[id] 新增「赋予会员」卡(与「调整积分」并列——同属「不收钱发价值」):tier 选择、月度/年度/终身/自定义起止、备注,附赠予历史与撤销;server actions 全程审计(新增 user.membership.grant / user.membership.revoke)。
    • 新权限 app.membership.grant(APP_MEMBERSHIP_GRANT),默认给 app_admin,app_support 注释预留;rbac-matrix 文档同步。
    • web/src/server/membership.test.ts(新,26 例):钉住派生的微妙规则——终身、国内月卡叠加、canceled 期末前有效、past_due 宽限、撤销/未来生效/过期赠予、rank 取胜、meetsTier、resolveGrantEnd。已做变异测试验证有效性(破坏「未来生效不激活」与「月卡叠加」各被对应用例精准杀死)。
    • 文档:设计文档新增 §7「管理端赋予会员」并修正 §2 的取舍表述;database-design 补表结构。

    ⚠️ 升级动作(必读): 本次新增权限码,而 hasPermission 读的是 DB 的 role_permission 表 → 必须重跑 pnpm --filter web db:seed(幂等:权限 upsert、角色权限 onConflictDoNothing),否则无人持有 app.membership.grant,入口不显示。同时需执行数据库迁移(新增 membership_grant 表)。

    验证: web typecheck + pnpm --filter web build 通过;web 单测 34 文件 326 例全绿(含新增 26 例);core-domain/api-contract 无新增失败(既有失败 4 例与本次无关)。

    日期: 2026-07-15


  29. 会员(entitlement)体系:与积分正交的权益层 + 三端展示 + 管理端名册

    背景: 模板原本只有积分层(credit_ledger FIFO + 过期、AI 预扣/退回),subscription 实质只是「按周期发积分」的发放器(credits_per_cycle),没有会员等级(tier)概念,也没有终身。这对 AI 生成类够用,但记单词/学习类要的是「买了会员就解锁功能」,与消耗量无关。

    核心取舍: 新增会员层与积分层正交,但不新增表——会员状态从既有 subscription + payment_order 实时派生(模板有 pg/mysql/sqlite 三份 schema,零迁移才好合并 upstream;且支付回调只管写对订单,会员永远是其函数,不会漂移;退款置 refunded 后会员自动失效)。

    • docs/会员与积分体系设计.md:重写为 zmlkit 设计规范(原 Q&A 稿仅供参考)——双层模型、派生规则、三种周期表达、各渠道生效、权益校验、三端展示、加 App 配方、验收清单;docs/README.md 索引与 core-domain.md 互链。
    • core-domain/plans.ts:Plan 新增 tier?(授予的会员等级,不填=纯积分包)与 membershipDays?(仅 one_time 会员卡:null=终身,N=买断 N 天且重复购买顺延);新增 MembershipTier 阶梯(label/rank/color)+ getMembershipTiers/getTierMeta/tierRank/planGrantsMembership/isLifetimePlan;样例:app_a 的 pro 月/年加 tier 并新增 pro-lifetime(会员+积分),新增 app_edu_demo(纯会员、credits: 0,记单词类参考),app_cn_demo 新增 vip-cn-monthpass(国内微信/支付宝无自动续费 → 卖 30 天月卡)。
    • plans.test.ts:credits>0 不变量放宽为「金额>0 且(有积分或有 tier)」以容纳纯会员套餐;新增 one_time 会员卡 membershipDays 不变量。
    • api-contract:新增 MembershipStatus DTO 并接入 MeResponse;新增错误码 MEMBERSHIP_REQUIRED(402)——CREDITS_INSUFFICIENT 的权益层孪生,让客户端区分「买积分」与「开会员」两种付费墙。
    • web/src/server/membership.ts(新):纯函数 deriveMembership()(订阅窗口 / 终身卡 / 限时卡叠加 / canceled 期末前仍有效 / past_due 宽限 / 多会员按 rank 取胜)+ getMembership() + 权益校验 meetsTier() / requireMembership()。
    • web/src/server/membership-roster.ts(新):管理端跨用户名册,复用同一派生纯函数(候选集受付费用户约束)。
    • /api/user/me 返回 membership;settings/billing 顶部新增会员状态卡(tier 徽标 + 终身/续费于/有效至 + 开通时间),积分卡下移,层次改为「权益 → 钱包 → 套餐 → 流水」。
    • 移动端:auth/store 的 MeUser 加 membership(从 /api/user/me 顶层折叠进 user),Me tab 昵称右侧渲染 VIP 徽标(★ Pro/★ VIP,色取自 tier meta)。
    • 管理端 /admin/memberships(新页 + Crown 图标导航,权限沿用 APP_ORDERS_READ):按 App(平台管理员)/会员等级筛选、按到期日排序(最快/最晚到期、最新开通),7 天内到期标红、终身高亮、按 tier 统计人数。
    • i18n:extra.ts 12 语言各新增 14 个会员键。

    验证: web typecheck + pnpm --filter web build(/admin/memberships 路由已生成)、mobile tsc 均通过;core-domain/api-contract 测试无新增失败(既有失败 ai-models.test.ts LEGAL_MEDIA ×1、state-machines.test.ts ×3 与本次无关,stash 复验确认)。

    日期: 2026-07-15


  30. 安装 expo-system-ui,原生根背景跟随主题(消除暗色白闪)

    背景: JS 层主题本就跟随系统(useColorScheme + app.config userInterfaceStyle: 'automatic'),但缺 expo-system-ui,原生窗口根背景保持默认色,暗色模式下在启动首帧、滑动转场、回弹过度滚动时会闪白。

    • mobile/package.json 新增 expo-system-ui@~56.0.5(对齐 SDK 56),pnpm install --filter mobile。
    • mobile/app/_layout.tsx:新增 effect,随解析后的 scheme 变化调用 SystemUI.setBackgroundColorAsync(palettes[scheme].bg),把原生根背景刷成当前主题背景色(系统切换明暗或用户改 mode 时自动重跑)。

    验证: mobile tsc + expo lint(0 error)。

    日期: 2026-07-14


  31. 数据看板柱状图底部加基线细横线

    变更: admin/analytics/charts.tsx 的 DailyBars SVG 在柱子底部(日期标签之上)加一条 --border 色基线;vectorEffect="non-scaling-stroke" + strokeWidth=1 保证在 preserveAspectRatio="none" 横向拉伸下仍是 1px 发丝线。

    日期: 2026-07-13


  32. 移动端编辑资料页只读显示邮箱与手机号

    变更: mobile/app/edit-profile.tsx 昵称下方新增两个只读字段:邮箱(user.email,始终显示)、手机号(user.phone E.164,仅在已绑定时显示)。复用现成 auth.email / auth.phoneNumber 文案(12 语言已覆盖),无需新增 key;样式沿用 input 边框但降透明度、不可编辑,表明只读(修改走账号安全流程,不在此页)。

    日期: 2026-07-13


  33. 修复营销首页右上角头像不显示(web-me 未解析存储 key)

    变更: /api/auth/web-me 返回的 avatarUrl 之前是数据库里存的对象 key(浏览器无法直接加载),改为经 resolveStorageUrl() 解析成可显示 URL(公开 CDN 或签名),与 console 资料页一致。AuthChip 原样渲染该值,故修复后头像正常;null 时仍回退首字母占位。

    日期: 2026-07-13


  34. 移动端 / 小程序媒体本地缓存(打开过优先加载本地)

    核心(缓存键): 签名 URL 的 query 每次都变(X-Amz-Signature/Expires/时间戳),不能用完整 URL 当键;取去掉 query 后的 host+path 做键 —— 本模板每个对象 key 都内嵌唯一 id、从不覆盖写,故「同 path ⇒ 同字节」,key = hash(host+path) 稳定无碰撞。

    • 移动端 src/media/cache.ts(expo-file-system,cacheDirectory,临时文件下载完再 move、LRU 到 256MB)+ useCachedMedia() hook + <CachedImage>;接入 MediaPreview(图/视频/音频源)、gallery/ai-history/home 缩略图、create 结果、profile/edit-profile 头像。命中→本地 file://,未命中→远端 URL 先显示 + 后台下载(下次本地),源解析后不中途替换(播放器不重载)。
    • 小程序 src/lib/media-cache.ts(getFileSystemManager + USER_DATA_PATH,索引存 Taro storage + 内存热表 + 已验证集合避免列表逐帧 IO、LRU 到 120MB)+ <CachedImage>(同步命中)+ resolveMediaSrc()(命令式:<Video>/InnerAudioContext);接入 generate/gallery/ai-history/home/me/edit-profile。图片点开预览仍用远端 URL(previewImage 对 USER_DATA 本地路径支持不稳)。
    • 全程 best-effort:任何 fs/下载错误降级为远端 URL,绝不破坏 UI。
    • 文档:storage-design §3.1 客户端本地缓存(含小程序 downloadFile 合法域名过审提醒)。

    验证: mobile tsc + expo lint(0 error);miniapp typecheck + build:weapp 通过。

    日期: 2026-07-14


  35. API 交互文档生产环境默认仅管理员可见

    • 新增 env API_DOCS_PUBLIC(默认 false)与派生开关 apiDocsPublic = !isProd || API_DOCS_PUBLIC:开发/staging 恒公开;生产默认仅管理员,置 true 可恢复公开(API 即产品的项目用)。
    • 新增 server/openapi/access.ts canViewApiDocs():公开模式直接放行(不碰 cookie,/docs/api 保持 ISR);门控模式查 web 会话 + 与 /admin/api-docs 相同权限(APP_SETTINGS_READ 或 PLATFORM_APPS_CONFIG_READ)。
    • /docs/api 页非授权访问一律 notFound()(不暴露私有接口面);/api/openapi.json 由 force-static 改为请求时判定,非授权 404,公开模式带 s-maxage=3600 CDN 缓存、门控模式 no-store(防共享缓存把管理员响应泄给公网)。
    • sitemap 仅公开模式收录 /docs/api;webhooks 设置页的「API 文档」链接按访问者可见性降级为纯文本(避免普通用户点开 404)。
    • 文档:webhooks-openapi.md §8 曝光策略 + §9 新开关;env 示例补 API_DOCS_PUBLIC。

    日期: 2026-07-14


  36. 全后台弹框统一防误触(仅按钮可关闭)

    • components/ui/dialog.tsx DialogContent 中央默认拦截 onInteractOutside / onPointerDownOutside / onEscapeKeyDown —— 所有基于 shadcn Dialog 的后台弹框(webhooks/apikeys 创建、用户/分类管理、安全设置、组织、退款、发票、取消订阅等)一律只能通过 X / 取消 / 确认按钮关闭;调用方可传自己的处理器恢复原行为。
    • admin/content/ContentManager.tsx 本地 Modal 同步移除遮罩点击与 Escape 关闭(长文编辑误触即丢稿),保留滚动锁。
    • 有意保持可点击关闭的两处:ConfirmDialog(AlertDialog 本就无遮罩关闭,Esc 等价取消、无输入可丢)、AiMediaPreview 灯箱(纯预览,点击任意处关闭是惯例)。

    日期: 2026-07-14


  37. App 管理弹框防误触 + README 增补 Google 登录回调配置

    • admin/apps/AppManager.tsx 的 Modal 移除遮罩点击与 Escape 关闭(表单较长,误触静默丢弃输入),仅保留滚动锁;关闭只走表单的保存/取消按钮,onClose prop 一并删除。
    • README.md / README_CN.md 环境变量小节后新增「Google 登录:Authorized redirect URIs」:控制台建 Web application 类型 client、回调固定 {API_BASE_URL}/api/auth/oauth/google/callback 且须与 env 逐字符一致、JavaScript origins 留空、多 App 共用回调可复用同一 client、移动端走同一服务端回调无需在 Google 侧加深链。

    日期: 2026-07-14


  38. 成熟度大批次:小程序过审五件套 + 移动端体验五件套 + Web/跨端六件套 + 运维文档

    变更:(四个提交:f83387d / 29b6895 / d946ff6 / 本条文档提交)

    • 小程序:__usePrivacyCheck__ + requirePrivacyAuthorize 隐私门;getPhoneNumber → /api/user/phone/wechat-bind(wxa getuserphonenumber 免短信绑定);订阅消息全链路(notify() 扇出 + /api/user/wechat-mini/openid 登记 + 模板 env APP_<SLUG>_WECHAT_TMPL_* + 手势内 requestSubscribeMessage);generate 页(下单+轮询)与 chat 页(/api/ai/chat 新增 stream:false JSON 模式);首页/作品页分享卡片与朋友圈;11 个次级页面迁入 pages-sub 分包 + lazyCodeLoading。
    • 移动端:expo-local-authentication 应用锁(设置开关+前台唤醒锁,无硬件放行);expo-store-review 购后评分(每版本一次);Me 页邀请好友(referral 链接系统分享);expo lint(eslint-config-expo,0 error);一方埋点 track() → POST /api/analytics/events(client. 前缀入 user_event)。
    • Web/跨端:GET /api/user/export 数据导出(GDPR/PIPL)+安全页下载;admin 分析页计费健康 4 卡(滞留/催款/对账/金额不符);/showcase 落地页 CMS 案例网格(type='showcase',href: 外链);POST /api/billing/portal(Stripe Billing Portal)与 POST /api/billing/mobile/sync(RC REST 幂等回放,RN 轮询超时自动触发);运行时品牌主题 NEXT_PUBLIC_BRAND_THEME → html[data-brand](emerald/amber/rose 三预设)。
    • 文档:storage-design §8 国内外 CDN/存储选型(腾讯 COS+CDN / 阿里 OSS+CDN / Cloudflare R2,同一 S3 抽象换 endpoint 即可)+ env 示例;新增 backup-restore.md(RPO/RTO、PITR/pg_dump、对象存储复制、演练清单);monitoring.md 补 Uptime/状态页约定;api-contract 表同步(portal/mobile-sync 转正 + 4 个新端点)。
    • 验证:web typecheck+vitest 300+生产构建、mobile tsc+lint、miniapp tsc+build(分包)全过。

    日期: 2026-07-14


  39. 404/500/50x 默认页面成套(营销/console 双树 + 反代静态兜底)

    • 共享 components/ErrorFallback.tsx(客户端,Sentry 上报 + Try again + 逃逸链接用原生 <a> 整页导航;失败路径刻意零 i18n 依赖,CSS 变量带回退值)。
    • 500:新增 (marketing)/[locale]/error.tsx(回首页)与 (console)/error.tsx(回 dashboard)——在各自布局内渲染,不再动辄触发替换整个文档的 global-error;global-error.tsx 保留为最后防线。
    • 404:新增 (console)/not-found.tsx(console/admin 页 notFound() 此前落到 Next 无样式默认页);营销 404 升级为同套视觉(+文档入口)。
    • 50x 静态兜底:web/public/50x.html(自包含、noindex、明暗自适应、中英文案),供 nginx error_page 502 503 504 在应用进程挂掉时返回;deployment.md §2.3 附 nginx 配置示例与边界分工说明。
    • typecheck + 生产构建通过。

    日期: 2026-07-14


  40. README 补「赋予管理员权限」章节(中英双语)

    变更: 两个 README 在「启动服务端」与「启动移动端」之间新增章节,说明三种路径:① seed 引导首个超管(SEED_SUPER_ADMIN_EMAIL/SEED_SUPER_ADMIN_APP + db:seed,无密码账号走忘记密码流程,平台级、幂等);② CLI 提升已有账号(pnpm --filter web rbac:grant -- --email … --role … [--app …],列出 5 个内置角色);③ 后台用户详情页分配 App 级角色(审计留痕;平台级角色刻意仅限 CLI)。并指向 docs/rbac-matrix.md。

    日期: 2026-07-14