Image Studio

把 Azure gpt-image-2 / 1.5 / MAI 等图像模型包成的设计师向 SaaS:access key 登录 + 积分计费(无支付)+ 隐藏管理后台。线上 design.mvp.restry.cn

当前 Tasks

🔥 进行中

  • src/lib/azure.ts 加 429 指数退避重试 + 读 Retry-After header;客户端友好文案(“目前画图较忙,稍后重试”),不要把 Azure 原话甩给用户 — (待派, 2026-05-13)
  • Azure gpt-image-2/gpt-image-15 配额扩容评估(S0 tier IPM 个位数,高峰真不够)— (待 resley 决定升 tier 还是限流, 2026-05-13)

⏸ 等决策/卡点

  • 仓库 owner 转移:bochub/image-studio (私有) → Restry 账号;当前 fries/Restry 都不是 owner,需 daddy 用 bochub 账号在 GitHub 网页 Settings → Transfer ownership 操作,转完 fries 把本地 remote 切到新地址 — (等 daddy, 2026-05-14)

✅ 最近完成(保留 7 天)

  • 线上 429 限流提示根因定位(azure.ts 只对 5xx 重试,429 直抛 + Azure 原话透传)— (2026-05-13)

概述

2026-04-23 ottor 在 deployer 上从零起项目,目标是把团队自有的 Azure 多模型画图能力(azure-imagengpt-image-2 / gpt-image-1.5 / MAI-Image-2e / Sora)包成一个对外可用的极简画图 SaaS:陌生人能看示例画廊 → 用 access key 登录 → 用积分调用模型出图 → 充值码加积分(不接支付,仅积分)。代码生成由本地 Claude Code (默认模型 claude-opus-4.6) 在 background 跑,每轮人审一次。

架构

域名design.mvp.restry.cn(mvp-deployer Caddy 路由)
端口3789(PM2 管理)
部署deployer 异步管线(POST /api/deploy 202+taskId → 轮询 /api/deploy/tasks/:id
项目目录~/projects/image-studio/(本地) / /opt/mvp-apps/image-studio/(线上)
技术栈Next.js 15 + Prisma + 后端 Postgres
Admin 入口/mgmt-7bx3k/login(隐藏路径,强密码 otbqvOyBDSy2Jt9i-h6A_cB1
用户入口/login(access key 单字段登录)
图片存储/var/lib/image-studio/images(项目外,不被部署覆盖;IMAGE_STORAGE_DIR env 注入)
Skill~/.openclaw/skills/azure-image-generation/(已脱敏,引用 ~/.credentials/.env

核心功能

  • 双登录:access key(普通用户) + 微信公众号(嵌入微信用,仍预留,未启用)
  • 积分计费:模型支持调倍率,Image 1.5 可设 1 积分/张、Image 2 可设 2-3 积分/张;管理端可改
  • 管理后台 /mgmt-7bx3k:Dashboard(使用情况)+ models / access-keys / topup-codes / 生成日志(含预览图 + 实际命中的 endpoint)
  • 公开首页画廊opennana-gallery + awesome-gpt-image-2 数据源融合后筛选过的 266 条 safe prompt(瀑布流 + 分类 tab + 弹窗预览,无搜索框,使用 Anthropic 橙色调),不抓 youmind(一开始尝试抓 youmind 27 类后改用开源数据源)
  • 生成排队GenerationJob 状态机 queued → running → succeeded/failed/blocked,前端轮询;含多 endpoint 轮询负载均衡(管理端可配多个同模型 endpoint,按 in-flight 分流:第 1 个忙就走第 2 个)
  • 限流:通过 caddy-reverse-proxy 网关层 caddy-ratelimit 模块做 4 档限流(auth 5/min、topup 10/min、generate 20/min、其他 60/min),见下文 04-23 安全章节
  • 手机端:移动端生成中加 shimmer 占位骨架(“图在显形”),不强制跳转历史
  • 图生图:用户端生成页面支持上传/选择参考图 + prompt 一起送进 /images/edits(Azure multipart),与纯文生图共用同一队列与积分扣费

凭据治理

完全跟随 deployer 04-23 的 secret v2 方案:

  • 项目读 process.env.IMAGE_STUDIO__*(在 ecosystem.config.js 中显式从 PM2 daemon env 拿,daemon 由 pm2-claw.service systemd unit 启动并 EnvironmentFile=/home/claw/.credentials/.env
  • 部署时 manifest.env 只传公开值(如 NEXT_PUBLIC_APP_URL),所有 8 个 secret 由 deployer 按 IMAGE_STUDIO__ 前缀自动从 ~/.credentials/.env 抽出去前缀写进 /opt/mvp-apps/image-studio/.env
  • 涉及 keys:DATABASE_URL / SESSION_SECRET / ADMIN_PASSWORD / WECHAT_APPID / WECHAT_APPSECRET / AZURE_OPENAI_ENDPOINT / AZURE_OPENAI_API_KEY / AZURE_OPENAI_API_VERSION

数据库迁移:SQLite → Postgres(04-23 当天 hotfix)

最初版本用 SQLite (file:./data/app.db),04-23 晚上踩了致命的部署覆盖坑

  • Bug 1(Prisma 路径解析):DATABASE_URL=file:./data/app.db 被 Prisma 解析成 schema.prisma 的相对路径 → 真库在 /opt/mvp-apps/image-studio/prisma/data/app.db
  • Bug 2(preserve-data 错位):deployer --preserve-data data 保的是项目根 data/真库在 prisma/data/ 没被保住
  • Bug 3(zip 漏 exclude):本地打包只 -x 'data/*',没 exclude prisma/data/*dev 库被压进 zip 直接覆盖线上库
  • 现象:每次重新部署用户 / access key / 历史图全没了

解法(同日完成)

  • 服务器侧:复用 mvp-deployer 那台的 mvp-postgres 容器(postgres:16-alpine0.0.0.0:5432,但 Azure NSG 拦了公网,只能 127.0.0.1:5432 访问)。建独立 db image_studio + 独立 user,密码进 ~/.credentials/.env
  • 本地侧:装 systemd postgresql-16 起 dev pg(同名 db / user / 独立密码),生产/开发库完全隔离,本地用 SSH tunnel 127.0.0.1:15432 → 服务器:5432 在需要时连 prod
  • Prisma schema provider sqlitepostgresql,重灌 266 条 safe prompts seed

04-23 部署历险一览

按时间序的关键踩坑(同 deployer v2 异步管线一同诞生,互相是对方的 smoke test):

  1. Next.js 15 standalone 不读 .envnext start 在生产模式下并不自动加载 .env,导致 ADMIN_PASSWORD / DATABASE_URL 全空 → 401。改为非 standalone + dotenv-cli 起或在 PM2 daemon env 注入
  2. Admin 登录 middleware 死循环 — middleware 把 /api/admin/session(登录接口本身)也拦截,要求带 admin-session-token cookie,鸡生蛋。修:豁免 /api/admin/session
  3. PM2 cwd 错位 — deployer pm2 reload 时 ecosystem.config.js 没设 cwd,导致 image-studio 进程 cwd 是 /opt/mvp-deployer/,读到 deployer 自己的 .env(postgres URL 串了),完全连不上自家 sqlite。修:deployer builder 强制写 cwd: '/opt/mvp-apps/<project>'
  4. response_format 参数 400 — gpt-image-2 不再接受这个字段,删 src/lib/azure.ts 第 10 / 43 行(commit 14f9981
  5. Azure key 在 git 历史泄露过 — 早期 admin/models/route.ts 硬编码过 key,建议轮换 + 把 key 收回 ~/.credentials/.env
  6. 图片 404 抢救 — 部署 zip 解压清掉了 public/prompts/<id>/N.jpg 用户原有图,扁平化搬到 /var/lib/image-studio/images(项目外目录),代码改读 IMAGE_STORAGE_DIR env 后部署不再覆盖
  7. Endpoint 输入框焦点跳出 — admin 表单组件定义在父 render 内,每次 setState 整个重建,输入完一个字符就丢焦。抽到组件外修复
  8. 首页改瀑布流 + IntersectionObserver 触底翻页(首屏 24,266 条全可滚动加载)+ 卡片标题/描述浮动到图上 + Anthropic 橙色
  9. Azure 偶发 500 (server processing error) — 服务器侧 gpt-image-2 偶发返 “The server had an error while processing your request”,本地无法复现。临时方案:客户端超时拉到 ≥120s + 最多 5 次指数退避重试,并在生成日志里记录最终命中的 endpoint,方便配合多 endpoint 轮询规避故障实例
  10. gpt-image-2 不传 quality 必 500 — 04-23 当晚对照实验确认:quality=low/medium/high 都正常返回 200,省略字段必 500(同 endpoint / key / prompt 复测三次稳定复现)。src/lib/azure.ts 默认 "medium" 是对的,UI 走的路径都会传,所以前端选项不影响;这条独立于上面 Azure 偶发 500,是参数契约问题
  11. AZURE_OPENAI_API_KEY vs AZURE_OPENAI_KEY 命名不一致 — 服务器 .envAZURE_OPENAI_API_KEY,代码读 AZURE_OPENAI_KEY,导致替换硬编码 key 后第一次部署直接连不上 Azure。改 src/lib/azure.ts 兼容两种名(commit 2ba8bc5

04-23 晚间安全收尾 + git 历史擦除

当天发现的硬编码 Azure key 等问题做了一次集中收尾(commit 2ba8bc5),随后因 repo 还在本地(未 push GitHub),直接 squash 成单个 e0e284c initial commit (history cleaned for security),把 15 个含泄漏 key 的 commit 全清掉,比走 key 轮换更彻底。落地项:

  • 5 个安全 header:HSTS / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy
  • Admin 密码改 crypto.timingSafeEqual 比较(防时序攻击)
  • Prompt 长度上限 10000 字(防注入面)
  • 充值码并发兑换走事务内二次校验
  • 自定义 404 页(src/app/not-found.tsx,Anthropic palette + “返回首页” 橙色按钮)
  • AppShell logo <Link href="/"> —— 之前登录后无法点 logo 回首页
  • Caddy 限流方案落到 deploy/rate-limit.md(不直接动服务器配置,留给 review 后再上);正式上线在 04-25 那一轮
  1. gpt-image-2 必须传 quality — 直连 curl A/B 实测:quality=low ✅ 200/118s、quality=medium ✅ 200/86s、不传 quality ❌ 500/92s。早期 UI 删 output_format/background/moderation/compression 时把 quality 透传链路也撞坏,必须强制 low/medium/high 之一

2026-04-25 大爆发:拉新 + 充值 + 反馈 + test 环境

ottor 一整天对 image-studio 做了第二轮重构,从”内部画图 SaaS”演化为”对外引流付费产品”。9+ 批次提交,覆盖六大模块;同时拆出独立 test.design.mvp.restry.cn 测试环境。

1. 账号体系重构(脱离 access key)

  • 账密注册/登录:User 表加 username / passwordHash(unique + bcrypt),/api/auth/random-name 生成”动物名+2位数字”形式(最终规范:总长 ≤5 字,词根 1-3 字 + 2 位数字,例:猫77 / 狐狸42 / 熊猫08,密码用 crypto.randomInt 16 位 4×4 dash 分隔,去掉易混淆字符)
  • 注册 UX:眼睛图标查看密码 + 二次确认 modal(amber-500 警示横幅 + ring-4 + 不可关)+ navigator.credentials 自动写浏览器密码
  • 微信公众号 OAuth 仍保留:未认证个人订阅号 snsapi_userinfo 没权限拿 nickname → 早期出现 Mock User 是因为 WECHAT_APPSECRET 解密失败回退 mock;同时 client component 把 NEXT_PUBLIC_APP_URL=http://localhost:3000 固化进 bundle 导致 redirect_uri 串号,部署时必须 --env NEXT_PUBLIC_APP_URL=https://design.mvp.restry.cn
  • 微信浏览器:未配 APPID 时禁止 mock=1 自动登录,强制走密码表单
  • 小程序登录方案归档(未实施):凭据存 ~/.credentials/image-studio-miniprogram.envMINIPROGRAM__ 前缀 chmod 600),完整方案 docs/miniprogram-qrlogin-plan.md,因审核 7-10 天放弃

2. Onboarding + 充值 B 流程

  • Onboarding:User 加 hasSeenOnboarding,4 步介绍(欢迎 / 怎么生成 / 进阶玩法 / 进充值页)+ 圆角小一些的设计 token
  • 充值 B 流程(管理员审核):放弃 A 方案(自助提单事后核对)以减少防刷复杂度。AppSetting 配 payUrl(微信支付码主区)+ contactUrl(客服微信码 → 右下角 Headphones 浮动按钮弹窗)+ flow_note(默认 “管理员审核后到账,通常 10 分钟到半小时”)+ topup_enabled 总开关
  • 6 档定价(最终方案,¥9-499):体验 ¥9/25 积分(≈5 张 image2)、入门 ¥29/100、标准 ¥49/180、进阶 ¥99/400、高级 ¥199/850、旗舰 ¥499/2300。SQL UPDATE 直接覆盖 topup_price_tiers(migration 用 ON CONFLICT DO NOTHING 不会覆盖已有空行,这是教训)
  • 尺寸友好名1024x1024 → 正方形(社交头像/封面)、1536x1024 → 横屏(壁纸/Banner)、1024x1536 → 竖屏(手机屏/海报);质量挡保留 low/medium/high/auto + 中文副标

3. 站内通知 + 反馈 + 公告 + 拉新

Batch模块关键设计
Notification 表通知充值审核结果自动写通知;用户端顶栏铃铛 + 红点 + 30s 轮询 + ding 音效 + toast
Feedback反馈入口右下角浮按(topup/login/onboarding 隐藏)+ 类型/内容/附图 + 单用户每日 ≤3;admin 审核通过事务 += feedback_reward_credits(默认 1,可配置)
Announcement公告群发targets: all / active30d / paid / userlist;level: info / warning / event;warning/event 强弹 modal
Invite拉新首充审核通过才结算(不在注册/出图触发);邀请人 15%、被邀请人 10、封顶 1000 积分;自引用 User.invitedBy + Invite 审计;档位计算:旗舰 ¥499 → 邀请人 ≈345 积分

4. Admin 后台 6 组侧栏

重构成两级导航:概览 / 用户与权限 / 财务 / 内容 / 增长(反馈/公告/邀请)/ 系统(审计/设置)。

新增 /mgmt-7bx3k/generations “加入示例库”按钮:admin 点完弹 modal 填 title/desc/category/featured/safe → 复制图到 public/prompts/{no}/{idx}.png + 写 Prompt + PromptImage + AuditLog(416 条 prompt 库里 179 条默认 safe=false 因为分类空/人像/时尚类,不是审查”不适合”)。

子代理清理低质 prompt 25 条(粤语吐槽 / @grok 互动 / 推特发布通告 / 重复 showcase / 日文炫耀贴):Prompt 表 446 → 421。no=28 由 daddy 强制点名删除。

5. 多轮安全审计 + Caddy 限流

全天三轮 Claude Code review(报告分别在 /tmp/review-2026-04-25.md/tmp/review-2026-04-25-round2.md),落地 P0:

  • crypto.randomInt 替换 Math.random 防 PRNG 预测
  • password-login 加 dummy bcrypt + 统一错误信息 + middleware PUBLIC_PATHS 精确 Set
  • 注册 NFKC + Unicode 白名单 + UTF-8 字节长度校验 + P2002 race-safe;登录共享 normalizeUsername helper
  • topup approve 双花用 updateMany where status=pending 幂等锁 → 重复点 409
  • generate 负余额:updateMany + balance gte 守卫 + 整个流程进事务
  • WeChat open redirect:new URL(state, req.url).origin === origin 校验,免疫 \ // 等所有解析变种
  • Feedback / promote-to-prompt TOCTOU 复制 topup 模式

caddy-reverse-proxy 加新限流:/api/auth/login/api/auth/password-login/api/auth/register 共享 5/min;/api/auth/check-ref 30/min;feedback 10/min;topup 10/min;generate 20/min;其他兜底 60/min。

6. Test 环境 test.design.mvp.restry.cn

  • 端口 3790;DB 隔离(同 PG 上 image_studio_test 库);存储 /var/lib/image-studio-test/images;Azure key 复用
  • deployer 流程铁律:默认只发 test,prod 走显式触发不自动同步
  • 配置基线同步:AppSetting (13) / AIModel (2) / AIEndpoint (3) 三张表 pg_dump --data-only → test TRUNCATE CASCADEpsql -f 灌入;Prompt 库不带(外键复杂)
  • /api/auth/check-ref 当时不在 PUBLIC_EXACT,访客调用 401 → 前端 setRefCode(null) → 注册时 ref 丢失,邀请关系全 NULL(commit 9678bcc 修复)
  • 邀请链接、metadataBase 硬编码 prod 域名 → 改读 NEXT_PUBLIC_APP_URL,兜底 req.url origin

7. 关键踩坑

  • deployer 缺 --hostdesign.mvp.restry.cn 被替换成默认 image-studio.mvp.restry.cn,TLS 证书失效 → 站点打不开。所有部署必须显式 --host
  • ?schema=public 串到 postgres 库unrecognized configuration parameter "schema";Prisma 写 URL 时手动 strip
  • pm2 restart --update-env 不可靠,env 缓存死掉 → 必须 pm2 delete && pm2 start
  • deployer 读 .env 权限:手动 chown root:root chmod 600claw 用户读不到,fallback 用空 env → 连到 prod 库;改 chown claw:claw
  • --preserve-env 对新项目无效:第一次部署需先 push .env 到服务器 /opt/mvp-apps/<project>/.env

8. Git + Review 化

  • 9 commit 分批落(安全 / 账密 / 充值 / 通知 / promote / UI 杂项 / 侧栏 / NFKC + race / test 修复)
  • 推到新建私有 GitHub repo(之前一直只本地 git)

2026-04-26 续:Admin prompts 页 + 自建 analytics + WeChat 推送修复

Admin prompts 管理页

/mgmt-7bx3k/prompts:抽 src/components/GalleryCard.tsx 给首页 LandingClient + admin 共用(之前误整页复用 LandingClient 被否决,“共用是只共用卡片组件”)。Admin 嵌侧栏 + 搜索 + category + safe 三态筛选 + Grid hover 三按钮(编辑/safe/删)+ 24/页分页 + 编辑抽屉(title/desc/promptText/category/author/safe)。API GET/PATCH/DELETE /api/admin/prompts[/no],updateMany 防并发,DELETE 级联。

自建访问统计(30 分钟自撸 vs 51.la)

51.la 接入失败 — sdk.51.la/js-sdk-pro.min.js 这个 CDN 域名从服务器 + 国内大部分网络 curl 直接 ERR_CONNECTION_CLOSED/timeout,不是浏览器拦截而是该域名本身在国内连通性挂了。同日切自撸:

  • Prisma 加 PageView 表(path / referer / userAgent / ipHash(日盐 SHA256 截 16 字符)/ ip(原始)/ country / city / sessionIdshu_sid cookie,30 分钟滚动)/ userId? / createdAt + 4 索引)
  • POST /api/track:匿名/登录都可,IP 取 x-forwarded-for 第一段(Caddy 反代已塞),fire-and-forget
  • <AnalyticsTracker /> 客户端组件:usePathname 监听路由变化每次打点
  • /mgmt-7bx3k/analytics:今日/7天/30天 tab + PV/UV 4 大数字卡 + recharts 双线日趋势 + Top 20 页面 + Top 10 来源 + 登录 vs 游客分布 + 最近访问表(时间/用户/IP/国家·城市/路径/UA)
  • IP 地理:机会式回填,首次访问该页时后台异步跑 ip-api.com batch 查最近 7 天去重 IP 写回 country/city
  • 侧栏新增「数据分析」分组

教训:Next.js 15 不允许 server component 用 dynamic(..., { ssr: false }),recharts 直接 'use client' 即可;Postgres migration 不能写 SQLite DATETIME 语法(首次 prod 跑 fail,需先清 _prisma_migrations 那条 failed 标记再重跑)。

WeChat 推送 9 天全挂复盘

发现:本周 7/7 cron 推送生成成功但全部投递失败,向前追溯 4/18 起连续 9 天微信推送 0 送达,last_status: oklast_delivery_error 长期未清。

两个 bug 叠加

  1. 上游 iLink ret=-2(4/22 起每天必现):Tencent iLink sendmessage 返回 ret=-2 errcode=None,可能是 context_token 过期(妈妈那条 DM 30 天+无新消息刷新)或 iLink 服务端校验异常
  2. 跨 event loop 复用 aiohttp session(真代码 bug):cron/scheduler.py:418asyncio.run(coro) 在 worker thread 跑 fallback,进 gateway/platforms/weixin.pysend_weixin_direct 后短路用 live_adapter._send_session.send(...)。但 _send_session 是 gateway 启动时在主 event loop 里建的,此刻在 asyncio.run() 起的新 loop 里跑 — aiohttp 的 timeout context 跨 loop 失效,抛 Timeout context manager should be used inside a task。fallback 路径永远不会成功,把 Bug 1 的失败用一个误导性错误覆盖掉

修法(A)send_weixin_directlive_adapter._send_session._loop is current_loop 检查 — loop 不一致就跳过 live adapter 短路、走 standalone 路径在当前 loop 上建新 aiohttp session。重启 gateway 生效。Bug 1 (ret=-2) 是独立问题,待手动测一条确认是否还在

修法(B,2026-04-27 ottor):进一步给 gateway/platforms/weixin.py_send_text_chunkPUSH_WINDOW_EXPIRED_RET = -2 常量,ret=-2 现在和 -14 一样会触发”清掉 context_token 重试一次”;如果重试后仍 -2,日志写 “recipient’s 48h proactive-push window has expired”,不再是模糊的 “unknown error”。验证当天 00:42 实际错误就是 ret=-2(两次都是),加重试 + 让妈妈在微信回一句话重开 48h 窗口即可恢复。

2026-04-27 增长 + 留存模块(test 上线 + Github 同步)

ottor-laptop 一天里把”新人礼 + 每日签到 + admin 三页面”做完,合并 commit 32c55e0 推到 github.com/bochub/image-studio(私有)(仓库本来就在远端,但当天的所有 growth/retention 改动是首次提交)。Daddy 邀请 Restry 为 collaborator(write)—— 同邀请也发给了 bochub/wiki / bochub/project-management / bochub/wukong-host

1. 新人礼 +10 积分(三维度风控)

  • WeChat OAuth 注册即送,unionid + IP + browser fingerprint 三维度,任意命中两维度即拒,全审计
  • fingerprint:同步 djb2 hash(UA + screen + lang + timezone + cores),写 shu_fp cookie,避开 WeChat 重定向前的异步竞态(async 指纹库会丢)
  • unionid 在 prisma.$transaction 里二次校验防 TOCTOU
  • 新表 WelcomeBonusLog(IP / UA / unionid / fingerprint / decision / reason)
  • admin 后台 /mgmt-7bx3k/welcome-bonus-log 查审计
  • 风控参数全走 admin 配置(额度、冷却期、启用开关),不硬编码

2. 每日签到(连续递增 + 断签清零)

  • 新表 CheckIn,按 Asia/Shanghai 时区切日,@@unique([userId, date]) 防并发双签
  • 曲线可在 admin 配置(默认 [1,2,3,4,5,6,7]),断签从头算
  • AppShell header 加签到 popover,未签到带 amber 小红点

3. Admin 后台增长 / 留存 / 审计三区块

  • 增长配置:新人礼额度 / 风控阈值 / 启用开关
  • 留存配置:签到曲线 / 启用开关
  • 审计日志:欢迎奖发放历史、签到打卡历史

4. test 环境部署链路

  • deployer --build "pnpm install --frozen-lockfile && pnpm prisma generate && pnpm prisma migrate deploy && pnpm build" —— 关键是 prisma migrate deploy 自动跑新 migration 建 WelcomeBonusLog + CheckIn
  • --preserve-env 抓服务器现有 10 个 env vars 全量回传,不冲掉密钥
  • 实际 PG 是 Postgres 不是 SQLite(之前 04-23 已迁完);deployer 部署 82s 完成
  • 验证:test 站 //mgmt-7bx3k/mgmt-7bx3k/welcome-bonus-log 全 200

5. Admin 用户列表展示 gap

WeChat OAuth 拿到的 nickname + headimgurl + unionid 已写 WechatAccount 表、displayName 用昵称;但 admin /mgmt-7bx3k 用户列表 API 没返回 avatarUrl、UI 也没渲染头像 —— 待补三处小改:API 加 wechatAccount.avatarUrl 字段、列表第一列加头像、详情页显示。

2026-04-29 — 「Design Studio」品牌升级 + WeChat Pay 接入

1. 多 vertical 化(launcher + pack + general)

顶部品牌从 “PackSmith” 抽象为 “Design Studio”(Layout/login/footer/launcher 全改),只有 /pack/* 子路由保留 PackSmith 命名。

  • lib/verticals.ts 注册表:launcher / + /pack(PackSmith — source=xiaofang,120 张包装样本,注入包装 prompt 模板) + /general(source ≠ xiaofang,238 张通用图)
  • /general UX 大改:复用主分支 landing-client.tsxopenModal/closeModal lightbox;卡片改 3:4 竖版,5 列响应式
  • /gallery + /generate 新增 ✏️ hover 编辑按钮 → /generate?editFromGen=&imgId= 自动预填 sourceImage(点图本身仍是 lightbox)

2. WeChat Pay JSAPI 接入(走 wx-gateway)

  • Payment 表 + migration 20260430000000_add_payment
  • lib/wx-gateway.ts:HMAC + payCreate / payJsapiPrepay / payStatus / verifyWebhook 4 个 wrapper
  • /api/checkout/start:渠道路由(personal_qr vs wxpay)
  • /api/wx/payment-webhook:HMAC 验签 + 事务内 addCredits 幂等(idempotent on Payment.status
  • /topup UI:保留个人码在上,下方加 #07C160 微信支付按钮,3 道守卫(UA 必须微信内 / 必须有 openid / 渠道开关)

3. 架构转向 — 网关托管 checkout 页(重要)

JSAPI 痛点:每个业务 app 都要单独在公众号后台登记「JS 接口安全域名」+「支付授权目录」。改为网关侧 host 一个 /pay/checkout/[paymentId] 公共页,业务 app 只 POST /pay/create + returnUrl 拿回 checkoutUrl 跳转过去;微信支付授权目录只需登记 https://wx.mvp.restry.cn/pay/ 一次,所有租户复用。

  • 网关 commit 1b70f25 + image-studio commit c5ef489
  • wx-gateway-integrate skill payment.md v2 重写,把 redirect 流程立为唯一推荐路径
  • 安全约束:?paid=1 仅 UI 提示,不可信;webhook 是唯一信任源;returnUrl host 必须匹配 app.appBaseUrl

4. notify_url 路径化(架构 bug 修)

微信 reject 含 ?notify_url → 网关把 ?app=X/pay/wxpay/notify/<app> 路径式(commit 48986bb)。

5. 商户配置(design-studio-prod)

mchId 1591949631,wxpayAppId wx225bf76b06064faa(造悟者公众号),apiV3Key + privateKey 已就位。当前商户号在微信侧 NO_AUTH 状态(账户审核问题,与代码无关),等审核通过开收。

6. mvp-deployer 踩坑(沉淀进 deployer

  • --preserve-env 在 SSH key 缺失时静默 fallback 空 env → 必须 base64 抓服务器 .env 然后多 --env KEY=VAL 注入
  • deploy.py 不支持 --post-deploy;postDeploy 阶段会自动跑 prisma migrate deploy,不需要手动指定

7. 流量

Day-1 在 design.mvp.restry.cn(CST 5/1):5 PV / 4 sessions / 1 logged-in / 0 generations。

相关页面

  • deployer — 部署平台 + secret v2 + inventory 接口(一同诞生)
  • azure-imagen — 上游模型(gpt-image-2 04-23 上线)
  • caddy-reverse-proxy — 限流方案落在 Caddy caddy-ratelimit 模块
  • shutiao-world — 同部署器上的另一个项目
  • echo — 同 deployer 上 04-25 上线的公众号写作工作台

2026-05-13 — 线上 429 限流提示根因 + Azure 重试缺口

线上 https://design.mvp.restry.cn/generate 持续提示 “We are currently servicing too many requests at the moment. Please wait and try again later.”(用户原话即 Azure 报文透传)。

根因src/lib/azure.ts:78-83 重试逻辑只对 5xx 重试429 直接 throw

if (response.status >= 500 && response.status < 600 && attempt < MAX_ATTEMPTS) {
  // retry...
}
// 429 跳过这里 → 直接 throw err.error.message

两层问题:

  1. Azure 容量真不够(根因):gpt-image-2 / gpt-image-15 部署在 resley-east-us-2-resource,S0 tier 默认 IPM 个位数,高峰/连发触发 429
  2. 代码层无兜底(放大):429 不重试 + 错误文案直接透传 Azure 原话

待补:429 走指数退避 + 客户端友好文案(“目前画图较忙,稍后重试”),不要把 Azure 原话甩给用户。

项目记忆刷新(5-13 整理):本地 ~/projects/image-studio、GitHub bochub/image-studio(Restry 账号),当前分支 dev(5/6 起 daddy 说”以后都用 dev”);pack 同步、main 已落后;Stack 是 Next.js 15 + React 19 + Prisma 6 + PostgreSQL,AI 走 Azure OpenAI(不是 Gemini)。两套部署:prod design.mvp.restry.cn:3789,PackSmith 分支 pack.mvp.restry.cn:3792

2026-05-14 — .memory/PROJECT.md 落项目根目录 + 仓库归属确认

承接 5-13 复盘做了两件:

  1. 项目级 memory 落盘~/projects/image-studio/.memory/PROJECT.md(5.2KB,8 节)—— 基础 / 部署(host claw@163.228.243.161,PM2 image-studio :3789,/opt/mvp-apps/image-studio,PG 127.0.0.1:5432 db/user image_studio)/ 微信 / 踩坑 / 当前 429 问题(同上 5-13 根因)/ 凭据索引(~/.credentials/.envIMAGE_STUDIO__ 前缀,详见 deployer Path B)/ 部署 API https://deploy.mvp.restry.cn / 远程地址。同 nexora-loop 5-14 同日做的 PROJECT_MEMORY.md 一个模式——让任何接手 agent cat .memory/PROJECT.md 即可对齐,不依赖 hermes vault。

  2. 仓库归属bochub/image-studio 私有仓库(不是公开),当前登录的 GitHub 账号 Restry / zhendl 都不是 owner——4-27 历史邀请的 collaborator 而已。要转给 Restry 账号必须 daddy 用 bochub 账号登录 GitHub Settings → Transfer ownership 触发,Restry 这边收邮件确认接收;转完 fries 这边 git remote set-url origin https://github.com/Restry/image-studio.git 切走。日志 / pm2 进程目录 / .env 凭据这套链路完全不动,是 GitHub 仓库归属层的事。已加进 ⏸ 卡点等 daddy。