Add HTTPS demo entry for microphone access

- Add a self-signed HTTPS Nginx entrypoint on Docker port 4443 so browser microphone APIs can run in demo mode.

- Keep the existing HTTP port 4002 unchanged while exposing container port 443 and generating the demo certificate during image build.

- Update CORS defaults and Compose environment for the HTTPS frontend origin.

- Clarify the report editor microphone message with localhost, HTTPS, and browser trusted-origin demo options.

- Document the browser HTTP microphone limitation, HTTPS demo URL, and Chrome/Edge insecure-origin workaround in README and docs.
This commit is contained in:
2026-05-02 03:34:31 +08:00
parent 2a86d9f5e4
commit b346b7e194
12 changed files with 81 additions and 11 deletions

View File

@@ -1,7 +1,7 @@
# Backend API development defaults.
API_PORT=3100
API_BODY_LIMIT="100mb"
CORS_ORIGIN="http://localhost:3001,http://localhost:4002"
CORS_ORIGIN="http://localhost:3001,http://localhost:4002,https://localhost:4443"
DATABASE_URL="postgresql://surclaw:surclaw_dev_password@localhost:5433/surclaw?schema=public"
SESSION_SECRET="change-me-in-production"
SESSION_COOKIE_SECURE="false"

View File

@@ -309,6 +309,8 @@ npm run test:e2e
API 默认 JSON/urlencoded 请求体上限为 `100mb`,由 `API_BODY_LIMIT` 控制Nginx 同步配置了 `client_max_body_size 100m`,用于承载迁移期报告 HTML、图片/关键帧和通用文件 Data URL 上传。
Docker 前端默认暴露 `http://localhost:4002`,并额外暴露自签名 HTTPS 演示入口 `https://localhost:4443`。浏览器麦克风 API 不能在普通局域网 HTTP 页面中由应用代码强行开启;语音听写演示优先使用 HTTPS 入口,或用 Chrome/Edge 的 `--unsafely-treat-insecure-origin-as-secure` 临时标记 HTTP 来源。
### `server/prisma/schema.prisma`
PostgreSQL 数据模型。当前覆盖 `Tenant``Department``User``UserSession``AppSession``Report``ReportMedia``ReportHistory``Template`、模板部门授权、`FileResource``SystemSetting``AuditLog`。Prisma 7 连接配置在根目录 `prisma.config.ts` 中。

View File

@@ -8,7 +8,15 @@ RUN npm run build
# Production stage
FROM nginx:alpine
RUN apk add --no-cache openssl \
&& mkdir -p /etc/nginx/certs \
&& openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/nginx/certs/surclaw-demo.key \
-out /etc/nginx/certs/surclaw-demo.crt \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
EXPOSE 443
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -223,6 +223,7 @@ docker-compose up -d --build
```text
前端: http://localhost:4002
语音 HTTPS 演示入口: https://localhost:4443
API: http://localhost:3002/api/health
数据库: localhost:5433
```
@@ -236,6 +237,7 @@ docker-compose down
部署说明:
- `web` 服务使用 Nginx 托管前端 `dist/`
- Docker 前端同时暴露 `http://localhost:4002` 和自签名证书的 `https://localhost:4443`;麦克风听写建议使用 HTTPS 演示入口。
- `api` 服务运行 NestJS 后端,并把上传文件目录挂载到 `uploads_data` volume。
- `db` 服务运行 PostgreSQL 16。
- `nginx.conf` 已配置 SPA 路由回退、`/api` 反向代理和 `100m` 请求体上限。

View File

@@ -21,7 +21,7 @@ services:
environment:
API_PORT: 3100
API_BODY_LIMIT: 100mb
CORS_ORIGIN: http://localhost:4002,http://localhost:3001
CORS_ORIGIN: http://localhost:4002,https://localhost:4443,http://localhost:3001
DATABASE_URL: postgresql://surclaw:surclaw_dev_password@db:5432/surclaw?schema=public
SESSION_SECRET: change-me-in-production
SESSION_COOKIE_SECURE: "false"
@@ -41,6 +41,7 @@ services:
restart: unless-stopped
ports:
- "4002:80"
- "4443:443"
depends_on:
- api

View File

@@ -74,11 +74,11 @@ AI 和语音密钥由后端 Settings API 保存并由代理使用,前端不再
docker-compose up -d --build
```
默认通过 Nginx 暴露 `http://localhost:4002`
默认通过 Nginx 暴露 `http://localhost:4002`,并额外暴露自签名证书的语音演示入口 `https://localhost:4443`
当前 Compose 服务:
- `web`:前端静态站点,暴露 `http://localhost:4002`
- `web`:前端静态站点,暴露 `http://localhost:4002``https://localhost:4443`
- `api`NestJS API暴露 `http://localhost:3002`
- `db`PostgreSQL 16暴露 `localhost:5433`
- `uploads_data`:后端文件持久化 volume。
@@ -88,7 +88,23 @@ docker-compose up -d --build
- `Dockerfile` 使用 Node 构建 `dist/`
- 运行阶段使用 `nginx:alpine` 托管静态文件。
- `Dockerfile.server` 构建并运行 NestJS API。
- `nginx.conf` 已配置 SPA 路由回退、`/api` 反向代理`100m` 请求体上限。
- `nginx.conf` 已配置 SPA 路由回退、`/api` 反向代理`100m` 请求体上限和自签名 HTTPS 演示入口
## 麦克风访问
浏览器不允许普通局域网 HTTP 页面调用麦克风代码无法绕过这个限制。Docker 演示环境建议使用:
```text
https://localhost:4443
```
首次打开会看到自签名证书提示,接受后即可让浏览器开放麦克风权限。如果必须用 `http://局域网IP:4002` 演示,只能在 Chrome/Edge 启动时加临时参数,把该 HTTP 来源标记为可信:
```bash
google-chrome --unsafely-treat-insecure-origin-as-secure=http://局域网IP:4002 --user-data-dir=/tmp/surclaw-chrome-demo
```
这是浏览器演示开关,不是系统安全方案;正式环境仍建议使用 HTTPS。
## 部署边界

View File

@@ -43,7 +43,7 @@
| 关键帧插入 | 真实集成 | 关键帧可点击插入或拖入图片占位符;上传成功后编辑器会把插入图片从 Data URL 替换为受控文件 URL。 |
| AI 辅助撰写 | 真实集成 | 前端调用 `/api/ai/chat`,后端使用全局共用 Provider Key 代理 OpenAI 兼容 `/chat/completions`;需要有效 Provider 配置、模型和网络。 |
| AI 差异确认 | 真实可用 | 使用 `diff` 生成左右差异,确认后写入 AI 区域。 |
| 讯飞语音听写 | 真实集成 | 前端使用麦克风采集音频并连接 `/api/speech/iat`;后端读取讯飞配置、生成鉴权 URL、补齐首帧 APPID/业务参数并转发 IAT 结果。需要浏览器权限、安全上下文(`localhost` 或 HTTPS、有效配置和网络。 |
| 讯飞语音听写 | 真实集成 | 前端使用麦克风采集音频并连接 `/api/speech/iat`;后端读取讯飞配置、生成鉴权 URL、补齐首帧 APPID/业务参数并转发 IAT 结果。需要浏览器权限、安全上下文(`localhost` 或 HTTPS、有效配置和网络Docker 提供 `https://localhost:4443` 演示入口。 |
| AI/语音密钥管理 | 真实集成 | AI Key 和讯飞 APIKey/APISecret 均由后端代理读取和使用;普通用户读取设置时不返回真实密钥。 |
| 系统设置 | 真实集成 | `SystemSettings` 优先调用 `/api/settings/system` 读取和保存抽帧、默认模板、AI Provider、语音配置“恢复演示出厂设置”会二次确认后调用后端 demo reset清空报告/审计并恢复默认用户、模板和演示配置。只有开发/显式回退模式下 API 不可用才回退本地缓存。 |
| 审计日志查看 | 真实集成 | 超级管理员和管理员可进入审计日志页,调用 `GET /api/audit-logs` 查看登录、报告、模板、用户、部门、设置和文件等操作;管理员只看本部门或自己相关日志。 |

View File

@@ -28,6 +28,7 @@ docker compose version
| NestJS API 本地服务 | `3100` | `npm run server:dev` 默认监听 |
| Docker API 暴露端口 | `3002` | 宿主机访问 Compose API |
| Docker 前端入口 | `4002` | 推荐的整套系统访问入口 |
| Docker 前端 HTTPS 演示入口 | `4443` | 自签名证书入口,推荐用于麦克风听写 |
| Docker PostgreSQL | `5433` | 宿主机访问 Compose 数据库 |
如果这些端口被占用,优先修改 `.env``docker-compose.yaml` 和对应文档,避免回退到 `3000` 等默认端口。
@@ -45,6 +46,7 @@ docker compose up -d --build
```text
http://localhost:4002
https://localhost:4443
```
健康检查:
@@ -56,7 +58,7 @@ curl http://localhost:3002/api/health
当前 Compose 会启动:
- `tuwen_web`Nginx 托管前端,宿主机端口 `4002`
- `tuwen_web`Nginx 托管前端,宿主机端口 `4002`;同时提供自签名 HTTPS 演示入口 `4443`
- `tuwen_api`NestJS API容器内监听 `3100`,宿主机端口 `3002`
- `tuwen_db`PostgreSQL 16宿主机端口 `5433`
@@ -149,7 +151,7 @@ VITE_API_PROXY_TARGET="http://localhost:3002"
完成启动后建议按顺序验证:
1. 打开 `http://localhost:4002` 或本地开发地址 `http://localhost:3001`
1. 打开 `http://localhost:4002`、语音演示地址 `https://localhost:4443` 或本地开发地址 `http://localhost:3001`
2. 使用 `admin / 123456` 登录。
3. 进入工作台,确认统计卡片能正常加载。
4. 进入“系统设置”,确认全局配置可保存。
@@ -190,7 +192,7 @@ Docker 方式应访问 `http://localhost:4002`。本地开发方式应确保 `np
检查占用:
```bash
ss -ltnp | grep -E ':3001|:3002|:3100|:4002|:5433'
ss -ltnp | grep -E ':3001|:3002|:3100|:4002|:4443|:5433'
```
本项目不应使用 `3000` 作为默认 API 端口。如果必须改端口,同步修改 `.env``docker-compose.yaml``vite.config.ts` 或 Nginx upstream并更新文档。

View File

@@ -85,7 +85,7 @@ AI 面板支持两种模式:
- 前端连接 `/api/speech/iat`,不再生成讯飞鉴权 URL也不读取 APPID/APIKey/APISecret。
- 浏览器采集麦克风音频,转换为 16k PCM 后发送音频帧。
- 启动前会检查浏览器是否支持 `navigator.mediaDevices.getUserMedia``AudioContext`;如果不是 `localhost` 或 HTTPS 等安全上下文,浏览器会禁止麦克风能力,页面会提示切换访问方式
- 启动前会检查浏览器是否支持 `navigator.mediaDevices.getUserMedia``AudioContext`;如果不是 `localhost` 或 HTTPS 等安全上下文,浏览器会禁止麦克风能力。Docker 演示环境可使用 `https://localhost:4443`,局域网普通 HTTP 只能通过 Chrome/Edge 演示启动参数临时标记为可信来源
- 后端读取 Settings API 中的 `xfSpeechConfig`,连接讯飞 IAT上游首帧由后端补齐 `common.app_id` 和默认 `business` 参数。
- 识别结果由后端转发回前端,并追加到 AI 输入框。

View File

@@ -18,6 +18,7 @@
- 系统设置、抽帧策略、AI Provider、语音参数和默认模板已优先接入 Settings API只有开发/显式回退模式下才保留本地缓存回退。
- Docker/Nginx 静态部署配置已存在。
- Docker/Nginx 与 NestJS API 已把请求体上限统一到 `100mb`,避免图文报告和文件 Data URL 上传触发默认 413。
- Docker Nginx 已额外提供自签名 HTTPS 演示入口 `4443`,用于浏览器麦克风听写权限;普通局域网 HTTP 仍受浏览器限制。
- 开发端口已调整为 `3001`
- 已补充 Vitest 测试框架和核心功能单元/组件测试。
- 已补充功能盘点,区分真实功能、外部集成、前端演示和预留项。
@@ -79,3 +80,4 @@
| 2026-05-02 | 将默认“腹腔镜胆囊切除术报告”后端 seed 与前端默认报告内容对齐,并把系统设置重置改为演示模式恢复出厂设置。 |
| 2026-05-02 | 修正报告草稿后端校验和保存失败提示,补充麦克风启动前置检查。 |
| 2026-05-02 | 增加 Nginx 和 NestJS 请求体上限配置,修复大图文报告保存 `request entity too large`。 |
| 2026-05-02 | 新增 Docker HTTPS 演示入口和麦克风访问说明,解决非安全上下文下语音听写不可启动的问题。 |

View File

@@ -33,3 +33,40 @@ server {
add_header Cache-Control "public, immutable";
}
}
server {
listen 443 ssl;
server_name localhost;
client_max_body_size 100m;
root /usr/share/nginx/html;
index index.html;
ssl_certificate /etc/nginx/certs/surclaw-demo.crt;
ssl_certificate_key /etc/nginx/certs/surclaw-demo.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:3100/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@@ -1123,7 +1123,7 @@ export default function ReportEditor() {
if (!mediaDevices?.getUserMedia) {
alert(window.isSecureContext
? '当前浏览器不支持麦克风采集,请更换新版 Chrome/Edge 后重试。'
: '麦克风需要在 localhost 或 HTTPS 环境下使用。请通过 localhost 访问,或配置 HTTPS 后重试。');
: '浏览器不允许在普通局域网 HTTP 页面中调用麦克风。请使用 http://localhost:4002、https://localhost:4443或用浏览器演示参数把当前 HTTP 地址标记为可信。');
return;
}
if (!AudioContextClass) {