内容
# redactcat
**redactcat** 是一个已部署的 PII(个人可识别信息)检测和删除 API。它接受纯文本和单页 PDF 文件,并将它们通过多阶段的 AWS 检测流程 —— Comprehend 用于文本 PII,Textract 用于扫描 PDF,Rekognition 用于嵌入图像中的人脸,pyzbar 用于条形码和二维码。用户在删除前审查检测到的实体;任何时候都不会在数据库中存储 PII。
访问地址:`https://api.redactcat.com` · [交互式文档](https://api.redactcat.com/docs)
## 技术栈
| 层 | 选择 | 原因 |
|-------|--------|-----------|
| 框架 | FastAPI | 异步就绪,OpenAPI 文档开箱即用,一流的依赖注入 |
| ORM | SQLAlchemy 2.0 | `Mapped`/`mapped_column` 声明式语法,显式会话控制 |
| 迁移 | Alembic | 模式迁移,自动生成;在容器启动前运行,接受流量之前 |
| 认证 | PyJWT + bcrypt | passlib 与 bcrypt 5.x 不兼容;滚动自己的依赖表面最小化 |
| 验证 | Pydantic v2 | 与 FastAPI 共同设计;在请求边界强制执行字段级约束 |
| 包管理 | uv | 近乎即时的安装,锁定文件确定性,替换 pip + venv |
| 校验 | Ruff | 单一工具替换 flake8,isort 和 pyupgrade |
| 类型检查 | mypy + pydantic 插件 | 在调用边界静态捕获类型不匹配,独立于测试覆盖率 |
| 测试 | pytest + SQLite 内存 | 快速,完全隔离,CI 不需要外部服务 |
| PII 检测 | AWS Comprehend | 受管 NLP;同步 API 处理最多 100KB 内联,无需维护基础设施 |
| PDF 文本提取 | AWS Textract | 处理扫描的 PDF,其中文本不直接暴露;单页文档的同步 API |
| PDF 删除 | PyMuPDF | 在边界框坐标应用永久黑盒删除 |
| 人脸检测 | AWS Rekognition | 在嵌入的 PDF 图像中检测人脸;同步内联字节 API,无需维护基础设施 |
| 条码/二维码检测 | pyzbar + libzbar0 | 解码二维码和条形码,渲染为矢量或光栅内容在页面上 |
## 设计决策
**无状态 JWT + 旋转刷新令牌**
访问令牌是短寿命的 JWT(30 分钟),无服务器端存储。刷新令牌是不透明的字符串,存储在 `refresh_tokens` 表中。每个 `/auth/refresh` 调用都会旋转令牌对 —— 旧行被删除,新令牌对被颁发。注销是单个数据库删除。这避免了令牌黑名单,同时给予被盗刷新令牌仅一次使用窗口,在检测前。
**无状态文本扫描和删除**
文本通过内存传递 —— 任何时候都不会将 PII 写入数据库或 S3。`/text/scan` 返回源文本和检测到的实体;客户端可以直接将该响应正文发送到 `/text/redact`,在过滤后。客户端控制要删除哪些实体(按置信度阈值、实体类型等),并且可以从单个扫描中生成多个删除变体。
**最小有状态 PDF 扫描和删除**
PDF 使用会话模型:`/pdf/scan` 将源文件上传到 S3,安排一次清理 Lambda 约 1 小时后,并返回检测到的实体与边界框和 `expires_at` 时间戳。`/pdf/redact` 接受过滤后的实体列表并应用删除;它可以在 TTL 窗口内多次调用同一个作业 —— 每次调用都会生成一个不同的删除文件,并附有自己的预签名下载 URL。作业在 1 小时后过期(410 Gone)。Lambda 负责所有清理:它在到期时删除作业前缀下的所有 S3 对象和数据库行。只有 `Job` 行(job_id,user_id,s3_key)在调用之间持续存在 —— 实体数据和边界框通过 HTTP 有效负载传递,从不写入数据库。扫描响应形状与删除请求正文完全匹配;客户端过滤实体列表并将其发送回,无需重新整形。
单个 `/pdf/scan` 调用连续运行三个检测流程:
1. **文本 PII** —— S3 → Textract(文本 + 单词边界框)→ Comprehend(字符偏移处的 PII)→ 将偏移量映射回单词边界框。使用 Textract 而不是 PyMuPDF,以便正确处理扫描的 PDF,其中没有直接暴露的文本。
2. **人脸** —— 如果页面有嵌入图像,则将渲染的页面像素图(JPEG)发送到 Rekognition `detect_faces`;每个面部成为具有归一化边界框的 `REKOGNITION` 实体。
3. **条码/二维码** —— 将渲染的页面像素图(灰度)传递给 pyzbar `decode`;解码的符号成为具有解码文本和归一化边界框的 `PYZBAR` 实体。这会无条件运行,因为二维码和条形码通常是矢量图形,不会在嵌入的图像列表中出现。
所有三个源都返回边界框作为页面尺寸的归一化 0-1 分数。每个实体的 `source` 字段(`COMPREHEND`、`REKOGNITION` 或 `PYZBAR`)允许客户端区分检测源。
**长寿命 API 密钥**
处理端点(`/text/*`、`/pdf/*`)接受短寿命 JWT 或长寿命 API 密钥作为承载令牌。密钥以 `rcat_` 为前缀,使用 `secrets.token_urlsafe(32)` 生成,并存储为 SHA-256 哈希(确定性查找,与 bcrypt 不同)。每个用户一个密钥;`POST /users/me/api-key` 生成或旋转。帐户管理端点(`/users/me/*`、`/auth/*`)仅限 JWT —— 密钥不能管理自己。
**朴素 UTC 日期时间**
所有日期时间都在 UTC 中计算并存储为时区朴素(`datetime.now(UTC).replace(tzinfo=None)`)。SQLite 没有本地时区支持;PostgreSQL `TIMESTAMP WITHOUT TIME ZONE` 接受朴素值。UTC 约定在应用程序层强制执行 —— 没有混合偏移数据进入数据库。
**通过 404 进行跨用户隔离**
访问用户拥有的资源的端点在资源存在但属于另一个用户时引发 `404`(而不是 `403`)。这避免了向未经授权的调用者确认资源存在。
**生产环境中的 Neon PostgreSQL,本地环境中的 SQLite**
已部署的服务连接到 [Neon](https://neon.tech) —— 无服务器 PostgreSQL。连接字符串存储在 SSM 参数存储中,并在运行时注入到 App Runner 中。本地开发使用 SQLite(`DATABASE_URL` 默认值为 `sqlite:///./redactcat.db`),实现零配置设置。所有日期时间和模式决策都与 PostgreSQL 兼容;如果 Neon's 约束(无 VPC 放置,扩展到零冷启动)成为问题,迁移到 RDS 只需更改连接字符串。
**单个 `models.py` 和 `schemas.py`**
所有 ORM 模型和 Pydantic 模式都生活在各自的一个文件中。这避免了循环导入,使整个数据模型一目了然,并保持 `Base.metadata.create_all` 确定性。
**直接 bcrypt(无 passlib)**
passlib 的 bcrypt 后端对 bcrypt 5.x 初始化引发 `ValueError`(删除的 `__about__` 属性,严格的 72 字节强制执行)。直接使用 bcrypt 的 `hashpw`/`checkpw`。
## MCP 集成
将 RedactCat 作为 Claude Desktop、Cursor 或任何 MCP 兼容的 AI 客户端中的原生工具使用。
```bash
curl -sSL https://api.redactcat.com/mcp/install.sh | bash
```
安装脚本:
1. 下载 MCP 服务器到 `~/.redactcat-mcp/server.py`
2. 使用所需的依赖项创建隔离的 Python venv
3. 提示输入 API 密钥并将其保存到 `~/.redactcat-mcp/.env`(权限:600)
4. 打印要添加到 MCP 客户端的确切配置项
**公开的工具:**
| 工具 | 描述 |
|---|---|
| `scan_text` | 在纯文本中检测 PII(返回具有偏移量 + 置信度的实体) |
| `redact_text` | 用删除占位符替换检测到的实体 |
| `scan_pdf` | 在单页 PDF 中检测 PII(文本,人脸,条码);返回 job_id + 边界框 |
| `redact_pdf` | 生成删除的 PDF 并返回预签名的下载 URL |
| `get_usage_summary` | 检查当前令牌使用情况和剩余的每月预算 |
API 密钥存储在本地,从未通过 AI 的对话上下文发送。计划未来发布 `pip install redactcat-mcp` / `uvx redactcat-mcp` 分发;服务器脚本结构为包入口点,因此迁移只需要一个 `pyproject.toml`。
## API 参考
| 方法 | 路径 | 认证 | 描述 |
|--------|------|------|-------------|
| GET | /health | — | 健康检查 |
| POST | /auth/register | — | 注册并自动登录;返回访问 + 刷新令牌对 |
| POST | /auth/login | — | 使用凭证登录;返回令牌对 |
| POST | /auth/logout | ✓ | 删除刷新令牌行;使会话失效 |
| POST | /auth/refresh | — | 旋转刷新令牌;返回新的令牌对 |
| GET | /users/me | ✓ | 获取当前用户配置文件 |
| GET | /usage/summary | ✓ | 令牌使用情况,津贴和当前日历月份的剩余令牌 |
| GET | /usage/history | ✓ | 当前日历月份的所有使用情况事件,最新的第一 |
| PATCH | /users/me | ✓ | 更新电子邮件或密码 |
| DELETE | /users/me | ✓ | 删除帐户和所有活动会话 |
| POST | /users/me/api-key | ✓† | 生成或旋转长寿命 API 密钥;返回完整的密钥一次 |
| GET | /users/me/api-key | ✓† | 获取 API 密钥元数据(前缀,created_at,last_used_at);如果不存在,则 404 |
| DELETE | /users/me/api-key | ✓† | 吊销 API 密钥 |
| POST | /text/scan | ✓‡§ | 在文本中检测 PII 实体;返回源文本 + 实体列表 |
| POST | /text/redact | ✓‡ | 对文本应用删除;返回删除的字符串 |
| POST | /pdf/scan | ✓‡§ | 上传单页 PDF;运行 Textract(文本 PII),Rekognition(人脸)和 pyzbar(条码/二维码);返回 job_id + 带有边界框的实体 |
| POST | /pdf/redact | ✓‡ | 对 PDF 应用删除;返回预签名的下载 URL 和 expires_at;可以多次调用每个作业 |
| GET | /mcp/server.py | — | 下载 RedactCat MCP 服务器脚本 |
| GET | /mcp/install.sh | — | 下载一键式 MCP 安装脚本 |
† 仅限 JWT — API 密钥不能管理自己。
‡ 接受 JWT 或 API 密钥。
§ 扫描端点强制执行日历月令牌预算(50,000 个令牌,每月重置)。超出预算返回 `429 Too Many Requests`,并带有 `{"error": "token_limit_reached", "tokens_used": <n>, "tokens_allowed": 50000, "resets_in_days": <n>}`。删除端点不受限制 — 仅在扫描步骤中执行强制执行,其中会产生 AWS 成本。
`https://api.redactcat.com/docs` 上提供交互式文档。
### 文本工作流
```
POST /text/scan
主体: { "text": "我的名字是 John Doe,我的 SSN 是 123-45-6789" }
返回: { "text": "...", "entities": [{ "entity_type", "text", "start_offset", "end_offset", "confidence" }] }
POST /text/redact
主体: { "text": "...", "entities": [...], "replacement": "[REDACTED]" }
返回: { "redacted_text": "我的名字是 [REDACTED],我的 SSN 是 [REDACTED]" }
```
扫描响应可以直接发布到 redact —— 首先过滤 `entities` 数组以选择要删除的 PII。`replacement` 是可选的,默认为 `"[REDACTED]"`;传递空字符串可删除 PII 而不是替换它。
### PDF 工作流
```
POST /pdf/scan
主体: multipart/form-data — file (PDF, single page only)
返回: { "job_id": 1, "expires_at": "2026-01-01T01:00:00", "entities": [{ "source", "entity_type", "text", "start_offset", "end_offset", "confidence", "bboxes": [{ "left", "top", "width", "height" }] }] }
POST /pdf/redact
主体: { "job_id": 1, "entities": [...] } ← 过滤后的扫描响应
返回: { "download_url": "https://...", "expires_at": "..." } ← 预签名的 S3 URL(TTL 匹配 expires_at)
```
扫描响应可以直接发布到 redact —— 过滤 `entities` 数组以选择要删除的 PII。`source` 为 `COMPREHEND`、`REKOGNITION` 或 `PYZBAR`。边界框归一化(0-1 分数页面尺寸),无论检测源如何。约束:仅限单页 PDF,最大 10 MB。作业在 1 小时后过期(`expires_at`)。预签名 URL TTL 匹配 `expires_at` —— 作业窗口关闭后,所有 URL 都有效,直到作业窗口关闭,Lambda 删除底层 S3 对象。可以在同一个作业上多次调用 redact 调用,每个调用都会生成不同的输出文件。
## 基础设施和部署
基础设施使用 `infra/` 中的 Terraform 进行管理。所有 AWS 资源(ECR,App Runner,S3,IAM,SSM,Route 53)都在那里定义。查看 [infra/architecture.drawio](infra/architecture.drawio) 以获取完整的资源图。
**部署新版本**(手动):
```bash
docker build --platform linux/amd64 -t redactcat .
docker tag redactcat:latest <ecr_url>:latest
aws ecr get-login-password --region us-west-2 | \
docker login --username AWS --password-stdin <ecr_url>
docker push <ecr_url>:latest
aws apprunner start-deployment \
--service-arn <service_arn> \
--region us-west-2
```
将 `main` 分支合并后会自动触发 `.github/workflows/deploy.yml` —— GitHub 中没有存储 AWS 凭证。
**数据库迁移** 在容器启动时自动运行 —— `alembic upgrade head` 在 uvicorn 接受流量之前执行。多个 App Runner 实例同时启动是安全的;Alembic 使用数据库级锁,因此只有一个应用待处理迁移。对于具有现有表但没有 `alembic_version` 行的数据库,请删除并重新创建表,而不是标记 —— 自动部署留下了标记窗口。
**基础设施更改必须在部署代码之前应用** —— 任何添加新 IAM 权限或 AWS 资源都需要 `terraform apply` 才能避免部署代码调用服务时出现空白窗口。
**首次基础设施设置 — 在 `terraform apply` 后设置秘密:**
`terraform apply` 初始化 SSM 参数为占位符值。在应用程序启动之前,请设置真实值:
```bash
aws ssm put-parameter \
--name "/redactcat/JWT_SECRET" \
--value "<your-secret>" \
--type SecureString \
--overwrite \
--region us-west-2
aws ssm put-parameter \
--name "/redactcat/DATABASE_URL" \
--value "postgresql://<user>:<password>@<host>/<db>?sslmode=require" \
--type SecureString \
--overwrite \
--region us-west-2
```
后续 `terraform apply` 运行不会覆盖这些值。仅在基础设施被完全销毁并重建时再次需要。
## 快速入门
```bash
# 安装依赖
uv sync
# 复制并填写环境变量
cp .env.example .env
# 将迁移应用于本地 DB
uv run alembic upgrade head
# 启动开发服务器(需要 AWS_PROFILE 用于 Comprehend,Textract,S3)
AWS_PROFILE=<your-profile> uv run uvicorn app.main:app --reload
# 运行测试
uv run pytest
# Lint
uv run ruff check .
# 类型检查
uv run mypy app
```
### 本地数据库
```bash
# 生成迁移后编辑模型
uv run alembic revision --autogenerate -m "description"
# 应用待处理迁移
uv run alembic upgrade head
# 回滚所有迁移(空数据库)
uv run alembic downgrade base
# 重置完全 — 删除文件并重新迁移
rm redactcat.db && uv run alembic upgrade head
```
## 使用 Docker 进行运行
日常开发使用本地服务器 (`uv run uvicorn app.main:app --reload`)。Docker 用于在部署前在本地验证生产镜像。
```bash
# 构建
docker build -t redactcat .
# 运行本地
docker run --rm -p 8000:8000 --env-file .env redactcat
```
CI/CD 管道以 `linux/amd64` 为目标推送到 ECR —— 本地开发不需要平台标志。
## 环境变量
| 变量 | 需要 | 默认值 | 描述 |
|----------|----------|---------|-------------|
| `DATABASE_URL` | 否 | `sqlite:///./redactcat.db` | SQLAlchemy 连接字符串 |
| `APP_ENV` | 否 | `development` | `development` 或 `production` |
| `JWT_SECRET` | **是** | — | JWT 签名密钥(32+ 字符);应用程序不会启动而没有它 |
| `JWT_ALGORITHM` | 否 | `HS256` | JWT 签名算法 |
| `JWT_EXPIRE_MINUTES` | 否 | `30` | 访问令牌生命周期(分钟) |
| `REFRESH_TOKEN_EXPIRE_DAYS` | 否 | `30` | 刷新令牌生命周期(天数) |
| `S3_BUCKET` | **是** | — | S3 桶名,用于临时 PDF 作业存储;应用程序不会启动而没有它 |
| `AWS_PROFILE` | 是† | — | `~/.aws/credentials` 中的命名 AWS 配置文件;提供凭据和区域 |
†本地需要 Comprehend,Textract,Rekognition 和 S3。不用于生产环境 -- App Runner 使用实例 IAM 角色。设置 shell (`AWS_PROFILE=x uv run uvicorn ...);不会自动从 `.env` 注入。
## 项目结构
```
redactcat/
├── app/
│ ├── config.py # 设置(通过pydantic-settings使用环境变量)
│ ├── database.py # SQLAlchemy引擎、会话工厂、Base、get_db
│ ├── dependencies.py # 共享的FastAPI依赖项(get_current_user)
│ ├── main.py # FastAPI应用程序入口点 — 注册路由
│ ├── models.py # 所有SQLAlchemy ORM模型
│ ├── schemas.py # 所有Pydantic请求/响应模式
│ ├── mcp/
│ │ ├── server.py # 独立的MCP服务器脚本(作为下载提供;不被应用程序导入)
│ │ └── install.sh # 一键安装脚本(作为下载提供)
│ ├── routers/
│ │ ├── auth.py # 注册、登录、注销、令牌刷新
│ │ ├── health.py # 健康检查
│ │ ├── mcp.py # /mcp/server.py 和 /mcp/install.sh — 公共分发端点
│ │ ├── pdf.py # PDF个人身份信息扫描和编辑(有状态,支持S3)
│ │ ├── text.py # 文本个人身份信息扫描和编辑(无状态)
│ │ ├── usage.py # /usage/summary 和 /usage/history — 当前月份令牌报告
│ │ └── users.py # 用户配置文件(获取、更新、删除)和API密钥管理
│ └── services/
│ ├── auth.py # API密钥前缀常量和哈希助手 — 由dependencies.py和routers/users.py共享
│ ├── barcodes.py # pyzbar二维码和条形码检测,从渲染的页面像素图中检测
│ ├── detection.py # AWS Comprehend个人身份信息检测
│ ├── extraction.py # AWS Textract PDF文本提取 + 词bbox映射
│ ├── redaction.py # 文本编辑(字符串替换)和PDF编辑(PyMuPDF)
│ ├── rekognition.py # AWS Rekognition嵌入式图像中的人脸检测
│ ├── storage.py # S3上传、下载、预签名URL
│ └── usage.py # 使用事件记录 — 每个AWS调用的令牌成本,尽力插入数据库
├── tests/
│ ├── conftest.py # 夹具:引擎、数据库会话、TestClient
│ ├── test_auth_router.py # 身份验证端点测试
│ ├── test_barcodes_service.py # pyzbar条形码服务单元测试
│ ├── test_detection_service.py # Comprehend服务单元测试(botocore Stubber)
│ ├── test_health_router.py # 健康检查测试
│ ├── test_mcp_router.py # /mcp/server.py 和 /mcp/install.sh 端点测试
│ ├── test_migrations.py # Alembic升级/降级集成测试
│ ├── test_pdf_router.py # /pdf/scan 和 /pdf/redact 端点测试
│ ├── test_rekognition_service.py # Rekognition服务单元测试(botocore Stubber)
│ ├── test_redaction_service.py # PDF编辑服务单元测试
│ ├── test_text_router.py # /text/scan 和 /text/redact 端点测试
│ ├── test_usage_router.py # /usage/summary 和 /usage/history 端点测试
│ ├── test_usage_service.py # 使用事件记录服务单元测试
│ └── test_users_router.py # 用户配置文件和API密钥管理端点测试
├── infra/ # Terraform — ECR、App Runner、S3、IAM、SSM、Route 53
├── .github/workflows/
│ ├── ci.yml # 拉取请求上的 lint + test
│ └── deploy.yml # 构建、推送到ECR、在 main 上部署到 App Runner
├── .env.example # 环境变量模板
└── pyproject.toml # 依赖项和工具配置
```
## AI 辅助开发
使用 [Claude Code](https://claude.com/claude-code) 作为主要开发工具。所有架构、设计决策和代码审查均由作者完成。开发约定和架构约束在 `CLAUDE.md` 中有文档记录。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器