Initial commit
This commit is contained in:
+124
@@ -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. 仅在用户明确选择后添加截图。
|
||||
@@ -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)。
|
||||
@@ -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。
|
||||
|
||||
## 数据保留
|
||||
|
||||
当前版本不自动删除反馈或附件。部署方应根据实际支持流程定义保留期限,
|
||||
并将数据库行和对应附件视为同一条记录处理。任何直接数据库维护都必须先
|
||||
停止写入、完成备份,并在隔离副本上验证。
|
||||
@@ -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. 不会自动附加对话、日志、文件、项目路径、模型配置或凭据。
|
||||
Reference in New Issue
Block a user