Yanir-R/gemini-portfolio
GitHub: Yanir-R/gemini-portfolio
一个带文档感知 AI 助手的个人作品集网站,基于 Gemini API 实现严格的上下文接地回答,并采用多层云原生安全架构抵御提示注入。
Stars: 0 | Forks: 0
# Gemini AI 个人作品集
一个带有 AI 助手的个人作品集网站:前端基于 React + TypeScript 部署在 Cloudflare Pages 上,后端使用 FastAPI 部署在 Google Cloud Run 上,通过 Google 的 Gemini API 基于本地 Markdown 进行解答。
## 架构
| 层级 | 运行环境 | 原因 |
| --- | --- | --- |
| Frontend | Cloudflare Pages(静态,全球 CDN) | 它是一个静态 bundle;从容器中提供服务相当于把 CDN 的活干得更糟 |
| API 前门 | Cloudflare Worker (`edge/`) | 将 API 置于 Cloudflare 的速率限制和机器人处理之后,并添加后端所需的共享 secret |
| Backend | Google Cloud Run(缩容至零) | 需要 Python,调用 Gemini,发送 SMTP |
| Secrets | Google Secret Manager | 绝不作为明文 Cloud Run 环境变量传递 |
| CI 认证 | Workload Identity Federation | 无密钥——任何地方都不存在 service-account JSON |
浏览器从不直接调用 Cloud Run:它调用 `api.`,然后 Worker
带上共享 secret 转发到 `run.app` URL,后端会直接拒绝
任何没有携带该 secret 的请求。参见 [edge/README.md](edge/README.md)。
## 前置条件
- Node.js >= 22.22.0(react-router v8 的最低版本要求)以及 npm >= 10.8.2
- Python 3.12(3.9 已 EOL,无法安装当前的依赖项)
- 来自 [Google AI Studio](https://aistudio.google.com/apikey) 的 Gemini API key
- 启用了结算功能的 Google Cloud 项目——即使在免费额度内,Cloud Build 和 Artifact Registry 也无法在没有启用结算的情况下运行
- Cloudflare 账号(免费)用于前端(Pages)和 API 前门
## 快速开始
```
git clone https://github.com/Yanir-R/gemini-portfolio.git
cd gemini-portfolio
```
### 前端
```
cd frontend
npm install
npm run dev # http://localhost:3000
```
| 脚本 | 作用 |
| --- | --- |
| `npm run dev` | 在端口 3000 上运行 Vite dev server |
| `npm run typecheck` | `tsc --noEmit` |
| `npm run lint` | ESLint |
| `npm run format` / `format:check` | Prettier |
| `npm run build` | 先进行类型检查,然后构建——构建过程绝不允许遗留类型错误 |
### 后端
```
cd backend
python3.12 -m venv venv
source venv/bin/activate # Windows: .\venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000
```
仅运行一个进程。速率限制器(见下文)将计数器保留在进程内,因此多个 worker 会导致限额上限成倍增加。
## 配置
### 前端
公开的构建值已提交在 **`frontend/site.config.ts`** 中:站点
URL、后端 URL 以及可选的头像 URL。如果是通过 Fork 进行开发?请修改这三项。
它们都不是密钥——这三者都可以从已部署的站点中读取——
提交它们正是保持部署一致性的关键:Vite 会在构建时
将它们内联,因此保存在仓库变量中的值只有在下一次
构建时才会生效,部署后修改并不会产生任何作用。
对于本地开发,同名的环境变量会覆盖对应的值:
```
# frontend/.env.development
VITE_BACKEND_URL=http://localhost:8000
VITE_SITE_URL=http://localhost:3000
```
如果 `url` 或 `backendUrl` 解析为空,**生产构建将失败**,而
不是发布一个指向 localhost 的 bundle。`VITE_ALLOW_UNCONFIGURED_BUILD=true`
用于显式跳过此检查,专门用于不带配置的构建;
切勿在打算部署的构建中设置此项。切勿在标记中硬编码域名——
`index.html` 使用了 `%VITE_SITE_URL%` 占位符,由构建过程进行替换。
### 后端
```
# backend/.env (已 gitignore — 切勿提交此项)
GEMINI_API_KEY=...
EMAIL_ADDRESS=your_gmail@gmail.com
EMAIL_PASSWORD=... # Gmail App Password, not your account password
YOUR_EMAIL=where_contact_mail_lands@gmail.com
# 可选
ALLOWED_ORIGINS=https://your-frontend.example # comma-separated, added to the CORS allowlist
FRONTEND_PROD_URL=https://your-frontend.example
ORIGIN_SHARED_SECRET=... # must match the edge Worker's EDGE_SECRET
```
`ORIGIN_SHARED_SECRET` 未设置意味着“不强制执行”,这正是让
Worker 能够在后端开始要求它之前先行部署的原因。一旦设置,
每个不携带匹配 header 的请求都会收到 403 错误——包括直接
调用 `run.app` URL。在本地开发时请将其留空。
### 速率限制
`/chat-with-files` 和 `/api/contact` 无需认证,且每次调用都会消耗金钱或配额,因此两者均强制执行按客户端和全局窗口的限制。默认值:
```
RATE_LIMIT_CHAT_PER_IP_PER_DAY=10
RATE_LIMIT_CHAT_GLOBAL_PER_DAY=12
RATE_LIMIT_CHAT_WINDOW_SECONDS=86400
RATE_LIMIT_CONTACT_PER_IP_PER_MINUTE=3
RATE_LIMIT_CONTACT_GLOBAL_PER_MINUTE=15
```
这两个限制器使用不同的窗口,因为它们解决不同的问题。Contact 是一种速率——某人允许发送的频率。Chat 是一种预算:Gemini 的免费额度每个模型每天仅授予 20 个请求,因此按分钟的窗口每天会重置 1,440 次,根本无法限制任何有意义的消耗。
全局窗口是成本保障:仅靠基于 IP 的限制很容易被 IP 欺骗或僵尸网络击溃。参见 `backend/rate_limit.py`。
## 部署
这两个工作流都拆分为一个 `verify` 任务(在 pull request 上运行,不需要云凭据)和一个受 `github.event_name == 'push'` 控制的 `deploy` 任务。部署绝不会在 pull request 时运行——两者都针对固定的服务名称,因此 PR 部署会用未经审查的代码覆盖共享环境。
### 所需的 GitHub 配置
Secrets:
| 名称 | 说明 |
| --- | --- |
| `GCP_DEV_PROJECT_ID` | 目标 GCP 项目 |
| `GCP_SA_EMAIL` | 部署 service account |
| `GCP_WORKLOAD_IDENTITY_PROVIDER` | WIF provider 资源路径 |
| `EMAIL_ADDRESS`, `YOUR_EMAIL` | SMTP 发件人 / 收件人 |
| `CLOUDFLARE_API_TOKEN` | 权限范围:Account → Cloudflare Pages → Edit |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare 账户 |
Variables:
| 名称 | 说明 |
| --- | --- |
| `CLOUDFLARE_PAGES_PROJECT` | Pages 项目名称 |
| `CLOUDFLARE_PAGES_ENABLED` | 设为 `true` 即激活 Pages 部署——请**最后**设置此项 |
| `GCP_RUNTIME_SA` | Cloud Run 容器运行时使用的身份,与部署账号区分开 |
| `ALLOWED_ORIGINS` | 后端的额外 CORS 源 |
前端的 URL 不在此列表中:出于上述原因,它们存在于 `frontend/site.config.ts`
中。
这里刻意**没有 `GCP_SA_KEY`**。身份验证使用 Workload Identity Federation,并且 OIDC provider 带有将其限制为该代码库的属性条件,因此永远不会创建或存储长期有效的 JSON 密钥。
`GEMINI_API_KEY`、`EMAIL_PASSWORD` 和 `ORIGIN_SHARED_SECRET` **不是**用于后端部署的 GitHub secrets——它们存放在 Secret Manager 中,并通过 `--set-secrets` 进行附加,从而使其远离 Cloud Run 的修订版本元数据。
`edge/` 中的 Worker 是手动部署的(`npx wrangler deploy`),而不是通过 CI 部署,
其 `EDGE_SECRET` 仅存在于 Cloudflare 中。
## API
| Endpoint | 用途 |
| --- | --- |
| `GET /` · `GET /health` | 健康检查 |
| `GET /api/chat/status` | 查询 chat 是否有可提供解答的语料库 |
| `GET /api/content/{file_name}` | 读取 Markdown 文档(路径被限制在 profile 目录内) |
| `GET /api/projects` · `GET /api/projects/{slug}` | 项目数据 |
| `POST /chat-with-files` | 上下文感知对话——受速率限制 |
| `POST /api/contact` | 联系邮件——受速率限制 |
## 上下文层
聊天机器人对 Yanir 的了解,以及它被允许回答的方式,都保存在三个文件中:
| 文件 | 职责 |
| --- | --- |
| `backend/docs/profile/` | 源内容——已发布,请参阅下方的说明 |
| `backend/context.py` | 组装并缓存语料库;报告其 token 成本 |
| `backend/prompt.py` | 行为契约:grounding、语气、边界 |
语料库规模很小(约 3.6k 个 token),因此每次请求都会发送全部内容,
并基于文件的 mtime 进行缓存,而不是每次调用时重新读取。`context.py` 在加载时
会记录 token 估算值,并在超过审查阈值时发出警告——即到了
需要开始考虑针对每个问题进行内容筛选的阶段。
**Grounding 正是 prompt 层存在的原因。** 聊天机器人会在招聘人员
阅读的页面上以 Yanir 的第一人称进行回答,因此虚构的雇主或日期就是
归咎于真实个人的虚假声明。模型被指示只能基于 profile 进行回答,
在遇到语料库未涵盖的问题时应拒绝回答并提供邮件跟进,
并且要将语料库和访客消息都视为数据而非指令。
```
backend/docs/
├── profile/ # markdown the chat answers from — published
├── projects/ # per-project markdown, drives /api/projects and the chat
├── writing/ # published articles, drives /api/writing and the chat
└── templates/ # placeholders for forks; never sent to the model
```
### 修改聊天机器人的回答内容
prompt 和 profile 的更改会针对一组黄金问题集进行验证——包括 grounded 回答、
语料库未涵盖的问题、错误前提、注入和提取——这些问题集
保存在此仓库之外,并在发布更改之前手动运行验证。`backend/tests/`
涵盖了可以在离线状态下断言的内容,并在每次 pull request 的 CI 中运行。
## 延伸阅读
- [前端 README](frontend/README.md)
- [后端 README](backend/README.md)
- [Edge Worker README](edge/README.md)
- [工作流 README](.github/workflows/README.md)
## 贡献
Fork、创建分支、提交、发起 PR。CI 会在每次 pull request 时运行类型检查、lint、格式检查、构建以及依赖项审计;所有步骤都必须通过。
## 许可证
[MIT 许可证](LICENSE)
标签:AI助手, AV绕过, Cloudflare, DLL 劫持, FastAPI, Gemini, MITRE ATT&CK, React, Serverless, Syscalls, 个人主页, 大语言模型, 程序员工具, 自动化攻击, 逆向工具