diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..dd66d24 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "ai-video-admin", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev", "--", "-p", "3001"], + "cwd": "frontend", + "port": 3001, + "autoPort": false + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e69de29 diff --git a/backend/README.md b/backend/README.md index 7b5c0b2..06fb6b4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -68,7 +68,7 @@ uv pip install fastapi "uvicorn[standard]" sqlalchemy asyncpg greenlet python-do cp .env.example .env # CRUD 阶段只需 DATABASE_URL;语音再填模型 key # 起 Postgres:在 ai-video/ 下 docker compose up -d postgres -.venv/bin/uvicorn app:app --reload --port 8000 +uv run --with-requirements requirements.txt uvicorn app:app --reload --port 8000 ``` > pipecat 相关代码用**惰性导入**,所以阶段 A 不装 pipecat 也能启动并跑 `/api/*` 与 `/health`; @@ -82,7 +82,7 @@ api 服务挂了源码 + `--reload`,前端用 npm dev + HMR,改代码都即时 ```bash cd ai-video -docker compose up # 前台起 pg + api(:8000)+ ui(:3000),日志直出 +docker compose up # 前台起 pg + api(:8000)+ ui(:3030),日志直出 docker compose up -d # 后台起;看日志 docker compose logs -f api docker compose down # 停止全部 diff --git a/backend/app.py b/backend/app.py index 1254eb3..b2cda5b 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,6 +1,6 @@ """FastAPI 入口。挂载路由,放行前端跨域,启动时建表。 -启动: uvicorn app:app --reload --port 8000 +启动: uv run --with-requirements requirements.txt uvicorn app:app --reload --port 8000 路由分组(对齐 dograh 的 routes/ 结构): /health 健康检查 diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..df91287 --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1 @@ +certs/ diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..960d38a --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,66 @@ +# 本地 / 局域网 HTTPS 调试 + +语音预览要求麦克风可用,浏览器只在 **安全上下文**(localhost 或 https)下放行 +`getUserMedia`。本机用 localhost 就够;**要在局域网用 IP 给别的设备测,就得上 https**。 +这里用 mkcert 签本地受信任证书 + nginx 反代统一 TLS。 + +## 结构 + +``` +浏览器 ──https/wss──> nginx :443 (唯一 TLS 入口, mkcert 证书) + ├── /ws/ → 后端 :8000 (/ws/voice 信令、/ws/stream 裸流) + ├── /api/ → 后端 :8000 (assistants/credentials/...) + ├── /health → 后端 :8000 + └── / → 前端 :3000 (Next dev + HMR) +``` + +前端页面和信令 ws 同源(同 host 同端口),没有混合内容 / 跨源问题。 + +## 步骤 + +```bash +# 1) 装 mkcert(只需一次) +brew install mkcert nss + +# 2) 生成证书(本机 CA + localhost/LAN IP/ai-video.local 的证书) +./deploy/setup-certs.sh + +# 3) 起前后端(任选其一) +docker compose up -d # api:8000 + ui:3030 都发布到 localhost +# 或本地分别 npm run dev / uvicorn app:app --port 8000 + +# 4) 起 nginx(装一下:brew install nginx) +nginx -c "$(pwd)/deploy/nginx/ai-video.dev.conf" -g 'daemon off;' + +# 5) 访问 +# 本机: https://localhost 或 https://ai-video.local +# 局域网:https://<本机IP> (脚本结尾会打印) +``` + +## 前端怎么连后端 + +前端读环境变量 `NEXT_PUBLIC_API_BASE_URL`(compose 里已设)。走 nginx 后, +让它指向**同源**即可,ws 地址由它推导: + +``` +NEXT_PUBLIC_API_BASE_URL = https://<访问用的host> # 同源,不再写 :8000 +wsUrl = NEXT_PUBLIC_API_BASE_URL.replace(/^http/, 'ws') + '/ws/voice' +``` + +> 没有反代、直连后端时则是 `https://:8000`,但那样要给后端单独配证书、 +> 还有跨源,不推荐。统一走 nginx 最干净。 + +## 给别的设备(手机等)免警告 + +证书是 mkcert 本地 CA 签的,只有装了该 CA 的设备才信任: + +```bash +mkcert -CAROOT # 打印 CA 目录,里面的 rootCA.pem 拷到设备并信任 +``` + +LAN IP 不在证书 SAN 里会报名称不匹配——`setup-certs.sh` 已自动把探测到的 +en0/en1 IP 加进 SAN;换网络换了 IP,重跑脚本即可。 + +## 证书不入库 + +`deploy/.gitignore` 已忽略 `certs/`。私钥不要提交。 diff --git a/deploy/nginx/ai-video.dev.conf b/deploy/nginx/ai-video.dev.conf new file mode 100644 index 0000000..758eeaa --- /dev/null +++ b/deploy/nginx/ai-video.dev.conf @@ -0,0 +1,94 @@ +# AI Video Assistant —— 本地/局域网开发用 nginx 反代(统一 TLS 入口) +# +# 作用:浏览器只跟 nginx(:443)打交道,一张 mkcert 证书统管; +# 前端(:3000)和后端(:8000)在后面照常跑明文,不用各自配证书。 +# 前端页面与信令 ws 同源(同 host 同端口),没有混合内容/跨源问题。 +# +# 用法: +# 1. ./deploy/setup-certs.sh # mkcert 生成证书到 deploy/certs/ +# 2. 启动前后端(docker compose → ui:3030;本地裸跑 → ui:3000;后端均 :8000) +# 3. nginx -c $(pwd)/deploy/nginx/ai-video.dev.conf -g 'daemon off;' +# 4. 浏览器访问 https://<本机IP 或 ai-video.local> +# +# 注意:证书路径下面写的是绝对路径,换机器/换目录时改 __CERT_DIR__ 两行即可。 + +worker_processes 1; +events { worker_connections 256; } + +http { + # mac/homebrew 的 nginx 默认 mime.types 路径;Linux 一般是 /etc/nginx/mime.types + include mime.types; + default_type application/octet-stream; + sendfile on; + + # 前端上游:优先本地裸跑的 :3000,连不上自动落到 docker ui 发布的 :3030 + upstream ui_upstream { + server 127.0.0.1:3000; + server 127.0.0.1:3030 backup; + } + + # 80 → 全部跳 443 + server { + listen 80; + server_name _; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + server_name _; # catch-all:任何 host/IP 都匹配,LAN 调试省心 + + # __CERT_DIR__ —— mkcert 生成的证书(setup-certs.sh 会放到这里) + ssl_certificate /Users/wangx/Code/AI-VideoAssistant-Project/ai-video/deploy/certs/ai-video.pem; + ssl_certificate_key /Users/wangx/Code/AI-VideoAssistant-Project/ai-video/deploy/certs/ai-video-key.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + # ---- 语音信令 / 裸音频流 ws:/ws/voice、/ws/stream ---- + # 关键:Upgrade/Connection 头让 ws 握手成功;长超时防止长连接被掐;关缓冲实时透传。 + location /ws/ { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header 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 https; + + proxy_read_timeout 3600s; # 语音是长连接,默认 60s 会断 + proxy_send_timeout 3600s; + proxy_buffering off; # 流式音频不能攒着 + } + + # ---- 后端 HTTP 接口:/api/*(assistants/credentials/knowledge-bases)+ /health ---- + location /api/ { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + 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 https; + client_max_body_size 50M; # 知识库文件上传留余量 + } + + location /health { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + } + + # ---- 前端 Next dev(其余全部)---- + # Upgrade 头是给 Next 热更新(HMR)的 ws 用的。 + location / { + proxy_pass http://ui_upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header 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 https; + } + } +} diff --git a/deploy/nginx/ai-video.docker.conf b/deploy/nginx/ai-video.docker.conf new file mode 100644 index 0000000..36fec16 --- /dev/null +++ b/deploy/nginx/ai-video.docker.conf @@ -0,0 +1,75 @@ +# AI Video Assistant —— docker compose 用 nginx 反代(统一 TLS 入口) +# +# 与 ai-video.dev.conf 的唯一区别:proxy_pass 用 compose 的服务名(api/ui), +# 不是 127.0.0.1——容器之间靠 app-network 上的服务名互通。 +# 证书挂载到容器内 /etc/nginx/certs(见 docker-compose 的 nginx 服务)。 +# +# 这份文件被 nginx:alpine 容器当作 /etc/nginx/nginx.conf 整体加载。 + +worker_processes 1; +events { worker_connections 256; } + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + + server { + listen 80; + server_name _; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/certs/ai-video.pem; + ssl_certificate_key /etc/nginx/certs/ai-video-key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + # 语音信令 / 裸音频流 + location /ws/ { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header 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 https; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + } + + # 后端 HTTP 接口 + location /api/ { + proxy_pass http://api:8000; + proxy_http_version 1.1; + 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 https; + client_max_body_size 50M; + } + + location /health { + proxy_pass http://api:8000; + proxy_set_header Host $host; + } + + # 前端 Next dev(含 HMR 的 ws) + location / { + proxy_pass http://ui:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header 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 https; + } + } +} diff --git a/deploy/setup-certs.sh b/deploy/setup-certs.sh new file mode 100755 index 0000000..e53f622 --- /dev/null +++ b/deploy/setup-certs.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# 用 mkcert 生成本地受信任 TLS 证书,供 deploy/nginx/ai-video.dev.conf 使用。 +# +# mkcert 会建一个"本地 CA"并装进系统/浏览器信任库,之后它签的证书在本机零警告。 +# 局域网里其它设备(手机/别的电脑)要免警告,需把这个 CA 根证书也装到那台设备上 +# (见末尾提示)。 +# +# 用法: ./deploy/setup-certs.sh +set -euo pipefail + +CERT_DIR="$(cd "$(dirname "$0")" && pwd)/certs" +mkdir -p "$CERT_DIR" + +# 1) 确认 mkcert 已安装 +if ! command -v mkcert >/dev/null 2>&1; then + echo "✗ 未找到 mkcert。先安装:" + echo " brew install mkcert nss # nss 是给 Firefox 用的" + exit 1 +fi + +# 2) 安装本地 CA(幂等,已装过会跳过) +echo "▶ 安装/确认本地 CA(mkcert -install)…" +mkcert -install + +# 3) 探测本机局域网 IP(其它设备靠这个 IP 访问) +LAN_IP="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)" +if [ -z "$LAN_IP" ]; then + echo "⚠ 没探测到局域网 IP(en0/en1),证书将只覆盖 localhost。" + echo " 如需 LAN 访问,手动重跑:mkcert ... <你的IP>" +fi + +# 4) 签证书:覆盖 localhost / 回环 / 局域网 IP / 一个好记的本地域名 +HOSTS=(localhost 127.0.0.1 ::1 ai-video.local) +[ -n "$LAN_IP" ] && HOSTS+=("$LAN_IP") + +echo "▶ 为以下名字签发证书:${HOSTS[*]}" +mkcert -cert-file "$CERT_DIR/ai-video.pem" \ + -key-file "$CERT_DIR/ai-video-key.pem" \ + "${HOSTS[@]}" + +echo +echo "✓ 证书已生成:" +echo " $CERT_DIR/ai-video.pem" +echo " $CERT_DIR/ai-video-key.pem" +echo +echo "下一步:" +echo " • 本机访问: https://localhost 或 https://ai-video.local" +[ -n "$LAN_IP" ] && echo " • 局域网访问:https://$LAN_IP" +echo " • 别的设备要免警告,把本地 CA 根证书装到那台设备:" +echo " 根证书位置:\$(mkcert -CAROOT)/rootCA.pem → 拷到设备并信任" +echo +echo " ai-video.local 解析:在 /etc/hosts 加一行(可选)" +[ -n "$LAN_IP" ] && echo " $LAN_IP ai-video.local" diff --git a/docker-compose.yaml b/docker-compose.yaml index 0357e55..3f6142e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,12 @@ # # 核心服务(默认起):postgres + api + ui # docker compose up -d postgres api # 后端 + 库 -# docker compose up -d # 再带上前端 ui +# docker compose up -d # 再带上前端 ui → http://localhost:3030 +# +# 语音对话调试(参考 dograh quick start 的直连方案): +# docker compose up -d && make db-seed # 首次需灌种子(凭证+助手) +# 浏览器开 http://localhost:3030 → 助手 → 语音预览 +# (localhost 是 secure context,麦克风可用;WebRTC 媒体直连 api,WS 信令走 :8000) # # 可选服务(用 profile 推迟到需要时): # docker compose --profile data up -d # + redis / rustfs(后台任务、S3 录音存储) @@ -42,7 +47,8 @@ services: environment: # 容器内连库:用服务名 postgres,覆盖 .env 里的 localhost DATABASE_URL: "postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/postgres" - CORS_ORIGINS: "http://localhost:3000,http://127.0.0.1:3000" + # 3030 = docker ui 宿主端口;3000 = 宿主机裸跑 npm run dev 时的端口 + CORS_ORIGINS: "http://localhost:3030,http://127.0.0.1:3030,http://localhost:3000,http://127.0.0.1:3000" ports: - "8000:8000" depends_on: @@ -67,7 +73,8 @@ services: - ./frontend:/app - /app/node_modules ports: - - "3000:3000" + # 宿主机用 3030 访问(容器内 next dev 仍监听 3000),避开本地裸跑前端的 3000 + - "3030:3000" depends_on: - api networks: [app-network] @@ -117,6 +124,25 @@ services: - --min-port=49152 - --max-port=49200 + # ---- 可选(profile: tls):nginx 反代统一 TLS,局域网 https 调试语音预览 ---- + # 起前先生成证书:./deploy/setup-certs.sh(证书落在 deploy/certs/) + # docker compose --profile tls up -d + # 浏览器 → https://localhost 或 https://<本机IP> + nginx: + image: nginx:alpine + profiles: ["tls"] + ports: + - "80:80" + - "443:443" + volumes: + # 整份配置当作 nginx.conf 加载(容器内 proxy_pass 用服务名 api/ui) + - ./deploy/nginx/ai-video.docker.conf:/etc/nginx/nginx.conf:ro + - ./deploy/certs:/etc/nginx/certs:ro + depends_on: + - api + - ui + networks: [app-network] + volumes: postgres_data: redis_data: