一个可直接上传到 GitHub,并部署到 Vercel 的 Prompt 管理站。
技术栈:
- React 19
- TypeScript
- Vite
- Vercel Functions
- GitHub Contents API
- GitHub OAuth
核心目标:
- 游客只读浏览、筛选、复制 Prompt
- 管理员通过 GitHub OAuth 登录
- 只有白名单中的 GitHub 账号才能执行写操作
- 预览图上传到仓库
public/uploads/ prompts.json写回仓库data/prompts.json- 支持 JSON 导入 / 导出
- 无数据库,认证使用
HttpOnly Cookie + JWT
Vercel 静态托管和纯前端页面不能把用户上传的图片永久写进部署磁盘,因为运行环境是只读 / 临时的。
所以本项目的主方案是:
- 前端上传图片到
/api/upload-image - Vercel Function 调用 GitHub Contents API
- 图片 commit 到仓库
public/uploads/ - Prompt 数据 commit 到仓库
data/prompts.json - Vercel 监听 GitHub 新 commit 并重新部署
这样就满足了“图片和数据都真实存在于仓库目录,而不是浏览器缓存”。
补充说明:
- 图片刚上传成功后,到 Vercel 完成新部署之间会有一个短暂窗口期
- 这段时间静态路径
/uploads/xxx.png可能还没同步到当前部署 - 所以项目额外提供了
/api/image?path=/uploads/xxx.png作为回退读取 - 前端会优先加载静态路径,失败后自动回退到
/api/image
管理员认证方面,本项目不再使用共享密码,而是改成:
- 点击“GitHub 管理员登录”
- 跳转 GitHub OAuth 授权
- GitHub 回调到
/api/auth/github/callback - 服务端读取 GitHub 用户信息
- 只有
GITHUB_ADMIN_USERS白名单中的账号才发管理员 Cookie
这比共享密码更适合 GitHub + Vercel 的部署方式,也更容易撤权和审计。
.
├─ api/
│ ├─ _lib/
│ │ ├─ data-store.ts
│ │ ├─ env.ts
│ │ ├─ github.ts
│ │ ├─ http.ts
│ │ ├─ oauth.ts
│ │ ├─ session.ts
│ │ └─ upload.ts
│ ├─ auth/
│ │ └─ github/
│ │ └─ callback.ts
│ ├─ prompts/
│ │ ├─ import.ts
│ │ └─ save.ts
│ ├─ image.ts
│ ├─ login.ts
│ ├─ logout.ts
│ ├─ prompts.ts
│ └─ upload-image.ts
├─ data/
│ └─ prompts.json
├─ dev/
│ └─ server.ts
├─ public/
│ └─ uploads/
│ └─ .gitkeep
├─ shared/
│ ├─ contracts.ts
│ └─ prompt-schema.ts
├─ src/
│ ├─ components/
│ │ ├─ AdminPanel.tsx
│ │ ├─ LoginModal.tsx
│ │ ├─ PromptCard.tsx
│ │ ├─ PromptDetail.tsx
│ │ └─ PromptImage.tsx
│ ├─ lib/
│ │ ├─ api.ts
│ │ ├─ drafts.ts
│ │ └─ format.ts
│ ├─ App.tsx
│ ├─ index.css
│ └─ main.tsx
├─ .env.example
├─ .gitignore
├─ README.md
├─ eslint.config.js
├─ index.html
├─ package.json
├─ tsconfig.app.json
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts
npm install复制一份:
cp .env.example .envWindows PowerShell 可以直接手动新建 .env 文件。
npm run dev这个命令会同时启动:
- Vite 前端开发服务器
- 本地 API 开发服务器
dev/server.ts
Vite 会把 /api/* 代理到本地 API 服务,所以本地不需要额外跑 vercel dev。
| 变量名 | 必填 | 说明 |
|---|---|---|
SESSION_SECRET |
是 | JWT 签名密钥,必须足够长 |
GITHUB_OAUTH_CLIENT_ID |
是 | GitHub OAuth App 的 Client ID |
GITHUB_OAUTH_CLIENT_SECRET |
是 | GitHub OAuth App 的 Client Secret |
GITHUB_OAUTH_REDIRECT_URI |
建议 | OAuth 回调地址,推荐显式设置 |
GITHUB_ADMIN_USERS |
是 | 允许进入管理员模式的 GitHub 用户名,多个用逗号分隔 |
GITHUB_OWNER |
否 | GitHub 仓库 owner |
GITHUB_REPO |
否 | GitHub 仓库名 |
GITHUB_BRANCH |
否 | 默认 main |
GITHUB_TOKEN |
否 | Fine-grained token,需 Contents: Read and write |
DATA_PATH |
否 | 默认 data/prompts.json |
UPLOAD_DIR |
否 | 默认 public/uploads |
说明:
SESSION_SECRET + GITHUB_OAUTH_* + GITHUB_ADMIN_USERS用于管理员认证GITHUB_*写回变量全部配置后,才会启用真正的 GitHub 持久化- 如果没有
GITHUB_TOKEN,网站会自动进入“本地只读 / 临时编辑模式” - 即使没有
GITHUB_TOKEN,你仍然可以先把 OAuth 跑通,验证管理员登录链路
- 用户点击“GitHub 管理员登录”
- 前端跳转
GET /api/login?next=/?admin=1 - 服务端生成 OAuth state 和 PKCE code verifier,写入
HttpOnly Cookie - 浏览器跳转 GitHub 授权页
- GitHub 回调
/api/auth/github/callback - 服务端换取 access token,并调用
GET https://api.github.com/user - 判断当前 GitHub
login是否在GITHUB_ADMIN_USERS白名单中 - 如果允许,则签发管理员 session Cookie
- 后续所有写接口继续校验管理员 Cookie
GITHUB_ADMIN_USERS=tommydeng,another-admin只有这些 GitHub 用户名能进入管理员模式。
- 打开 GitHub
- 进入
Settings - 进入
Developer settings - 进入
OAuth Apps - 点击
New OAuth App
推荐填写:
Application name:Z-image Prompt AdminHomepage URL: 你的站点地址,例如https://your-site.vercel.appAuthorization callback URL: 你的回调地址,例如https://your-site.vercel.app/api/auth/github/callback
本地开发时,建议单独用一套本地环境变量:
GITHUB_OAUTH_REDIRECT_URI=http://localhost:5173/api/auth/github/callback注意:
- GitHub OAuth App 的回调地址必须和你实际发起授权时使用的地址一致
- 生产环境和本地开发通常需要分别配置各自的回调 URL
如果没有配置 GITHUB_TOKEN,项目仍然可以运行:
- 前台游客仍可浏览、搜索、复制 Prompt
- 管理员仍可通过 GitHub OAuth 登录
- 管理后台仍可在浏览器里临时编辑 Prompt / 分类
- 可导出 JSON
- 可导入 JSON 到当前浏览器草稿
但以下操作会被禁用:
- 保存到 GitHub
- 上传图片到仓库
- 服务端导入并写回
页面上会显示“本地临时模式”提示。
推荐使用 Fine-grained personal access token,不要直接使用大权限 classic token。
- 打开 GitHub
- 进入
Settings - 进入
Developer settings - 进入
Personal access tokens - 选择
Fine-grained tokens - 点击
Generate new token
仓库权限至少给:
Contents:Read and write
如果仓库是私有仓库,也要保证这个 token 能访问目标仓库。
GITHUB_OWNER=你的 GitHub 用户名或组织名
GITHUB_REPO=你的仓库名
GITHUB_BRANCH=main
GITHUB_TOKEN=你的 fine-grained token先把当前项目推到一个 GitHub 仓库。
- 登录 Vercel
- 点击
Add New... - 选择
Project - 选择你的 GitHub 仓库
- Framework Preset 选择
Vite - 保持默认构建命令即可
到项目的 Settings -> Environment Variables 添加:
SESSION_SECRET=xxxx
GITHUB_OAUTH_CLIENT_ID=xxxx
GITHUB_OAUTH_CLIENT_SECRET=xxxx
GITHUB_OAUTH_REDIRECT_URI=https://你的域名/api/auth/github/callback
GITHUB_ADMIN_USERS=你的github用户名,另一个管理员
GITHUB_OWNER=你的owner
GITHUB_REPO=你的repo
GITHUB_BRANCH=main
GITHUB_TOKEN=xxxx
DATA_PATH=data/prompts.json
UPLOAD_DIR=public/uploads然后重新部署。
部署完成后:
- 游客访问首页
- 前端读取
GET /api/prompts - 管理员点击 GitHub 登录并拿到
HttpOnly Cookie - 管理员修改内容后点击“保存到 GitHub”
- Function 调 GitHub Contents API commit 更新
- GitHub 仓库产生新 commit
- Vercel 自动触发新部署
返回当前最新 Prompt 数据。
返回字段包含:
datashaisAdminwritablegithubConfiguredauthConfiguredauthProvideradminUsersource
发起 GitHub OAuth 登录流程。
可带参数:
next
例如:
/api/login?next=/?admin=1
GitHub OAuth 回调接口。
作用:
- 校验 OAuth state
- 校验 PKCE
- 换取 access token
- 读取 GitHub 用户信息
- 判断是否在白名单中
- 写入管理员 session Cookie
清除管理员 Cookie。
管理员专用。
请求体:
{
"sha": "当前客户端看到的 data/prompts.json sha",
"data": {
"schemaVersion": 1,
"categories": [],
"prompts": []
}
}作用:
- 校验管理员会话
- 校验 schema
- 使用当前 sha 写回 GitHub
- 若 sha 不一致则返回冲突,让前端提示“请重试”
管理员专用。
请求体:
{
"mode": "merge",
"sha": "当前客户端 sha",
"incoming": {
"schemaVersion": 1,
"categories": [],
"prompts": []
}
}支持:
mergeoverwrite
管理员专用。
请求格式:
multipart/form-data
字段:
promptIdexistingImagePath可选image
限制:
- 只允许
PNG / JPG / WEBP / GIF - 图片大小
<= 2MB
这是为了处理“图片刚 commit 但当前部署的静态文件还没刷新”的窗口期。
前端显示策略:
- 先加载静态路径
/uploads/xxx.png - 如果当前部署还没有这张图,则自动回退到
/api/image
本项目采用最简单可行的冲突处理:
- 前端通过
GET /api/prompts获取当前sha - 保存时把这个
sha一起提交 - 服务端保存前再次检查最新
sha - 如果发现文件已经变化,则返回
409 - 前端提示用户刷新后重试
这可以避免多人同时编辑时直接覆盖彼此数据。
{
schemaVersion: number
categories: {
id: string
name: string
order: number
}[]
prompts: {
id: string
title: string
content: string
categoryId: string
tags?: string[]
imagePath?: string
createdAt: string
updatedAt: string
}[]
}当前实现中:
schemaVersion固定为1imagePath必须匹配/uploads/xxx.(png|jpg|jpeg|webp|gif)- 导入 JSON 时会严格校验字段与分类关联关系
GITHUB_TOKEN不会暴露到前端GITHUB_OAUTH_CLIENT_SECRET不会暴露到前端- 写接口全部要求管理员 Cookie
- Cookie 为
HttpOnly - 会话 token 使用
SESSION_SECRET进行 JWT 签名 - OAuth 流程带有
state和 PKCE 校验 - 图片上传做了类型与大小限制
原因:
- 当前 GitHub 账号不在
GITHUB_ADMIN_USERS
处理:
- 检查环境变量里填写的 GitHub 用户名
- 注意大小写不敏感,但不要写错账号
原因:
GITHUB_OAUTH_REDIRECT_URI和 GitHub OAuth App 配置不一致Client ID / Client Secret填错- 回调域名不是当前实际访问域名
处理:
- 重新核对 GitHub OAuth App 的 callback URL
- 重新核对 Vercel 环境变量
- 本地和线上最好分别配置各自的回调地址
原因:
GITHUB_TOKEN权限不足- Token 无法访问当前仓库
处理:
- 确认使用 Fine-grained token
- 确认仓库权限里有
Contents: Read and write - 确认
GITHUB_OWNER / GITHUB_REPO正确
原因:
- 你保存前,仓库里的
data/prompts.json已被其他人改过
处理:
- 刷新页面
- 重新加载最新数据
- 再次合并你的修改后重试
原因:
- 图片已 commit 到 GitHub,但当前 Vercel 静态部署还没切到新版本
处理:
- 前端会先走静态路径,再自动回退到
/api/image - 如果仓库是私有的,也可以依赖
/api/image读取最新图片 - 等待 Vercel 新部署完成后,静态路径会恢复正常
原因:
- 服务端限制为
<= 2MB
处理:
- 压缩图片
- 改成 webp
原因:
- 没有用
npm run dev - 只启动了纯
vite
处理:
- 用项目里的命令
npm run dev - 它会同时起前端和本地 API server
原因:
schemaVersion不等于1- 字段缺失
- Prompt 的
categoryId指向了不存在的分类
处理:
- 用导出的 JSON 作为模板来修改
- 保证分类先存在
正常部署到同一个 Vercel 项目时,不需要自己处理 CORS,因为前端和 /api 同域。
只有你把前端和 API 拆到不同域名时,才需要额外加 CORS 头。
你可以基于当前项目继续扩展:
- 改用 GitHub App 替代长期 PAT
- 引入 Cloudinary / S3 作为图片存储
- Prompt 历史版本对比
- 删除 Prompt 时自动清理仓库旧图
- 多管理员审计日志
- Cloudflare Access 或 Tailscale 作为第二层访问控制
这是一个适合个人 / 小团队内部维护 Prompt 资产的轻量工具站模板。
如果你的图片量级很大、更新非常频繁,建议把图片切到对象存储,把 GitHub 持久化主要用于 prompts.json 和配置文件。