Initial commit

This commit is contained in:
lofyer
2026-08-24 17:27:41 +08:00
commit 5ff80caf7e
45 changed files with 11485 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
# 公共反馈 API
## Endpoint
```text
POST /api/v1/feedback
```
该接口不使用用户账户。服务端按来源 IP 和 `installationId` 的 keyed
digest 分别限流,并使用 `(productKey, clientRequestId)` 保证幂等。
生产环境必须使用 HTTPS。若请求包含 `Origin`,它必须与服务端
`PUBLIC_ORIGIN` 的 origin 完全一致。桌面应用的 Main 进程请求通常不发送
`Origin`。
## JSON 提交
```http
POST /api/v1/feedback HTTP/1.1
Content-Type: application/json
{
"schemaVersion": 1,
"productKey": "goodbuddy",
"category": "bug",
"title": "魔法笔记页签不可见",
"description": "平台功能中的魔法笔记页签被布局压缩,无法看到或点击。",
"contactEmail": "user@example.com",
"environment": {
"appVersion": "0.11.0",
"platform": "windows",
"architecture": "x64",
"locale": "zh-CN"
},
"installationId": "00000000-0000-4000-8000-000000000101",
"clientRequestId": "00000000-0000-4000-8000-000000000102"
}
```
## Multipart 提交截图
使用 `multipart/form-data` 时:
- `payload`:上述 JSON 的字符串形式
- `screenshot`:可选,最多一个 PNG、JPEG 或 WebP 文件
```sh
curl \
--fail-with-body \
--form 'payload={
"schemaVersion":1,
"productKey":"goodbuddy",
"category":"bug",
"title":"魔法笔记页签不可见",
"description":"平台功能中的魔法笔记页签被布局压缩,无法看到或点击。",
"installationId":"00000000-0000-4000-8000-000000000101",
"clientRequestId":"00000000-0000-4000-8000-000000000102"
}' \
--form 'screenshot=@capture.png;type=image/png' \
https://feedback.example.com/api/v1/feedback
```
服务端会验证实际解码格式,只接受单帧 PNG、JPEG 或 WebP,随后应用方向
信息、重编码为 PNG 并去除原文件元数据。
输入和重编码后的文件都必须满足 `MAX_SCREENSHOT_BYTES`。最大边长为
8192 像素,最大像素数为 3200 万。
## 字段
| 字段 | 必填 | 约束 |
| --- | --- | --- |
| `schemaVersion` | 是 | 当前固定为 `1` |
| `productKey` | 是 | 2–64 位小写字母、数字或连字符,产品必须启用 |
| `category` | 是 | `bug`、`feature`、`experience`、`other` |
| `title` | 是 | 去除首尾空白后 1–120 字符 |
| `description` | 是 | 去除首尾空白后 10–5000 字符 |
| `contactEmail` | 否 | 有效邮箱,最多 254 字符 |
| `environment.appVersion` | 环境存在时 | 1–64 字符 |
| `environment.platform` | 环境存在时 | `windows`、`macos`、`linux`、`unknown` |
| `environment.architecture` | 环境存在时 | `x64`、`arm64`、`unknown` |
| `environment.locale` | 否 | 2–32 字符 |
| `installationId` | 是 | 应用安装级随机 UUID,不应使用硬件 ID |
| `clientRequestId` | 是 | 每次新反馈生成的随机 UUID;重试时保持不变 |
请求对象采用严格 schema,未声明字段会导致请求失败。
## 成功响应
首次创建返回 HTTP 201:
```json
{
"reference": "GOODBUDDY-000001",
"duplicate": false
}
```
同一产品和 `clientRequestId` 的重试返回 HTTP 200,并返回原编号:
```json
{
"reference": "GOODBUDDY-000001",
"duplicate": true
}
```
## 错误
| HTTP | 含义 |
| --- | --- |
| 400 | 字段、JSON、multipart 或图片无效 |
| 403 | 请求 `Origin` 不允许 |
| 404 | 产品不存在或已停用 |
| 413 | 请求体超过限制 |
| 429 | IP 或安装实例超过提交频率 |
| 500 | 服务端内部错误 |
客户端应:
1. 为一次用户提交生成 `clientRequestId`,网络重试时复用。
2. 设置明确的连接与总超时。
3. 限制响应体大小,只接受 JSON。
4. 将非 2xx 响应作为失败处理,并向用户保留输入内容。
5. 仅在用户明确选择后添加截图。
+63
View File
@@ -0,0 +1,63 @@
# GoodBuddy 接入边界
本文件记录 Product Issue 已提供的服务端契约。GoodBuddy 桌面端接入应在
生产域名确定后实现。
## 推荐调用链
```text
About 页面反馈表单
→ shared Zod contract
→ preload 显式方法
→ Main IPC handler
→ Main HTTPS client
→ POST /api/v1/feedback
```
不要从 Electron renderer 直接跨域调用公开 API,也不要向 renderer 暴露
通用网络能力。
## Renderer
- 在“关于”页面内提供反馈入口,不跳转 GitHub Issue。
- 字段包括类型、标题、描述、可选邮箱和可选截图。
- 发送前显示会提交的环境字段和隐私说明。
- 截图需要主动选择或粘贴,支持预览和移除。
- 失败时保留所有输入,成功时显示服务端反馈编号。
- 同一次提交重试必须复用 `clientRequestId`。
## Preload 与 IPC
- 在 `src/shared` 定义并复用 Zod 输入/输出 schema。
- 使用现有 trusted sender 校验。
- preload 只暴露一个用途明确的反馈提交方法。
- IPC 输入必须限制字符串长度、附件数量和字节数。
- 不传递文件系统路径给服务端。
## Main HTTPS client
- 固定并校验生产 HTTPS origin。
- 使用正常系统 TLS 校验,不允许忽略证书错误。
- 设置连接和总超时,支持取消。
- 禁止或严格限制重定向,避免把反馈发送到非预期 origin。
- 限制响应体大小并按公共响应 schema 验证。
- 不记录请求正文、邮箱、图片、installation ID 或响应凭据。
## 标识
- `installationId`:应用首次生成并持久保存的随机 UUID,不使用硬件序列号、
用户名或设备指纹。
- `clientRequestId`:用户发起一条新反馈时生成;只有该次反馈的网络重试
复用。
- `productKey`:GoodBuddy 固定使用 `goodbuddy`。
## 明确不自动附加
- 对话和提示词
- 日志
- 文件、项目或工作区路径
- API key、Token、Cookie、SSH 信息
- 模型、知识库和 Runtime 配置
- 剪贴板或屏幕内容
服务端完整字段与响应见 [公共 API 契约](api.md)。
+152
View File
@@ -0,0 +1,152 @@
# 部署与运维
## 配置
Docker Compose 从项目根目录 `.env` 读取以下值:
| 变量 | 说明 | 默认值 |
| --- | --- | --- |
| `FEEDBACK_DOMAIN` | Caddy 申请证书并对外服务的域名 | 必填 |
| `APP_SECRET` | 会话、IP 和安装标识 digest 的主 secret,至少 32 字符 | 必填 |
| `SESSION_TTL_HOURS` | 管理员会话时长 | `12` |
| `PUBLIC_RATE_LIMIT_PER_HOUR` | 单 IP 每小时公开提交上限 | `20` |
| `PUBLIC_INSTALLATION_LIMIT_PER_HOUR` | 单安装实例每小时公开提交上限 | `8` |
| `MAX_SCREENSHOT_BYTES` | 输入及标准化后截图的字节上限 | `5242880` |
`.env` 包含 secret,不得提交到 Git 或复制到工单、聊天和日志中。建议在
密码管理器中保存一份加密副本,并在主机上执行 `chmod 600 .env`。
Compose 会从 `FEEDBACK_DOMAIN` 生成
`PUBLIC_ORIGIN=https://FEEDBACK_DOMAIN`。直接运行 Node 服务时,
production 环境仍必须显式提供只含 HTTPS origin 的 `PUBLIC_ORIGIN`。
## 网络与 TLS
- DNS A/AAAA 记录必须指向主机。
- 入站开放 TCP 80、TCP 443 和 UDP 443。
- 不要直接发布应用容器的 3000 端口。
- Caddy 自动申请并续期证书。
- `TRUST_PROXY=true` 仅用于当前 Caddy → app 的受控容器网络拓扑;改变
代理层级时必须重新检查真实客户端 IP 解析。
验证:
```sh
docker compose ps
curl --fail "https://feedback.example.com/health"
curl -I "https://feedback.example.com/admin/"
```
将示例域名替换为 `.env` 中的真实 `FEEDBACK_DOMAIN`。
## 管理员
创建或重置管理员:
```sh
read -rsp "Administrator password: " PRODUCT_ISSUE_ADMIN_PASSWORD
echo
export PRODUCT_ISSUE_ADMIN_PASSWORD
docker compose run --rm \
-e PRODUCT_ISSUE_ADMIN_PASSWORD \
app node dist/server/server/cli/create-admin.js \
--email admin@example.com \
--name "MesaLogo Admin"
unset PRODUCT_ISSUE_ADMIN_PASSWORD
```
密码必须为 12–128 字符。相同邮箱再次执行会更新显示名和密码、重新启用
管理员,并删除该管理员的所有既有会话。
## 备份
应用数据全部位于 `feedback-data` volume:
- `product-issue.sqlite` 以及可能存在的 `-wal` / `-shm`
- `attachments/<feedback-id>/screenshot.png`
为获得一致快照,备份时短暂停止 app:
```sh
umask 077
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "backups/${STAMP}"
docker compose stop app
docker compose cp app:/app/data/. "backups/${STAMP}/"
docker compose start app
tar -C backups -czf "backups/product-issue-${STAMP}.tar.gz" "${STAMP}"
```
同时安全备份 `.env` 中的生产配置,但应与数据归档分开加密保存。不要只
备份 SQLite 主文件而遗漏 WAL 或 attachments。
建议至少每日备份,并定期把加密副本同步到另一台主机或对象存储。定期在
隔离环境执行恢复演练,而不是只检查归档文件存在。
## 恢复
恢复会替换当前 volume 内容。先保存当前数据并确认选中的归档:
```sh
mkdir -p restore
tar -C restore -xzf backups/product-issue-YYYYMMDDTHHMMSSZ.tar.gz
docker compose stop app
docker compose run --rm --no-deps app sh -c \
'find /app/data -mindepth 1 -maxdepth 1 -exec rm -rf {} +'
docker compose run --rm --no-deps \
-v "$PWD/restore/YYYYMMDDTHHMMSSZ:/restore:ro" \
app sh -c 'cp -R /restore/. /app/data/'
docker compose start app
```
随后检查:
```sh
docker compose logs --tail=100 app
curl --fail "https://feedback.example.com/health"
```
登录后台并抽查产品、反馈、备注和截图。
## 升级
1. 创建并验证当前备份。
2. 获取经过审核的新版本源码。
3. 运行 `docker compose build --pull`。
4. 运行 `docker compose up -d`。
5. 检查 health、容器日志、管理员登录和一条测试反馈。
数据库迁移在应用启动时执行,采用可重复的 `CREATE TABLE/INDEX IF NOT
EXISTS`。未来出现结构变更时,仍应先备份再升级。
## Secret 轮换
更新 `.env` 中的 `APP_SECRET`,然后重建 app:
```sh
docker compose up -d --force-recreate app
```
轮换会让所有管理员会话失效,并使后续 IP/安装标识使用新的 digest。
数据库中的反馈、备注和截图不依赖该 secret 解密,因此仍可读取。
## 监控与故障排查
```sh
docker compose ps
docker compose logs --tail=200 app
docker compose logs --tail=200 caddy
docker compose exec app node -e \
"fetch('http://127.0.0.1:3000/health').then(async r=>console.log(r.status,await r.text()))"
```
应用关闭了常规逐请求访问日志,避免把公开提交者的原始 IP 持久写入应用
日志。拒绝和内部错误使用请求 ID 记录,不记录反馈正文、邮箱、截图内容、
Cookie、CSRF token 或 secret。
## 数据保留
当前版本不自动删除反馈或附件。部署方应根据实际支持流程定义保留期限,
并将数据库行和对应附件视为同一条记录处理。任何直接数据库维护都必须先
停止写入、完成备份,并在隔离副本上验证。
+75
View File
@@ -0,0 +1,75 @@
# 数据与隐私边界
## 公开提交会存储什么
| 数据 | 用途 |
| --- | --- |
| 产品、类型、标题、描述 | 分流和处理反馈 |
| 可选联系邮箱 | 在需要时联系提交者 |
| 可选应用版本、系统、架构、语言 | 判断影响范围和辅助复现 |
| `clientRequestId` | 网络重试幂等 |
| installation ID 的 keyed digest | 安装实例限流和滥用控制 |
| 来源 IP 的 keyed digest | 滥用调查和限流关联 |
| 可选截图 | 复现界面或视觉问题 |
| 创建和更新时间 | 排序与处理记录 |
服务器不会保存原始 installation ID 或原始 IP。digest 使用
`APP_SECRET` 生成。管理员状态修改和内部备注也会存入数据库。
## 默认不收集什么
GoodBuddy 客户端接入必须保持以下默认边界,除非未来为具体字段另行设计并
向用户清楚展示:
- 对话、提示词或模型回复
- 应用日志和崩溃日志
- 文件内容、项目内容和本地路径
- API key、Cookie、Token、SSH 凭据或主机配置
- 模型、知识库或 Runtime 的详细配置
- 剪贴板内容
- 屏幕截图
截图只能由用户主动选择或粘贴后随该次反馈上传。提交界面必须在发送前让
用户看到截图已包含,并允许移除。
## 截图处理
- 只接受单帧 PNG、JPEG、WebP,并验证实际解码格式。
- 服务端实际解码图片,不依赖扩展名。
- 应用方向信息后统一重编码为 PNG。
- 不复制原图 EXIF/XMP 等元数据。
- 原始上传字节不会作为附件保留。
- 标准化后的图片位于私有数据 volume,不由静态服务器公开。
- 只有具有有效管理员会话的同源接口可以读取截图,响应使用
`Cache-Control: private, no-store`。
截图可能仍然包含屏幕中肉眼可见的个人信息、文件名、消息或其他敏感内容。
客户端必须提醒用户检查画面。
## 管理访问
- 没有公开注册入口。
- 管理员只能通过主机 CLI 创建或重置。
- 密码使用带随机 salt 的 scrypt hash 保存。
- 会话 token 仅放在 HttpOnly、SameSite=Strict Cookie 中,数据库只保存
keyed digest。
- 管理写操作需要 CSRF token。
- 内部备注和可选联系邮箱只在管理后台展示。
## 传输、日志与保留
- 生产流量通过 Caddy HTTPS 传输。
- 常规应用访问日志关闭,避免持久记录公开提交者的原始 IP。
- 应用错误日志不得包含反馈正文、邮箱、截图、认证 Cookie 或 secret。
- 当前版本不会自动删除反馈。部署方负责公布和执行实际保留期限,并保护
备份中的同类数据。
## 客户端展示要求
在 GoodBuddy 或其他产品发送前,界面至少应说明:
1. 标题、描述和所选反馈类型会发送到 MesaLogo 反馈服务。
2. 邮箱是可选项。
3. 应用版本、操作系统和架构会在界面列明后发送。
4. 截图默认不上传,只有明确添加后才发送。
5. 不会自动附加对话、日志、文件、项目路径、模型配置或凭据。