From cff55251f44c6de2233601085d58461110edc839 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Mon, 11 May 2026 17:35:16 +0800 Subject: [PATCH] Deploy huijutec package and stabilize AI model --- README.md | 182 ++++++++++++------------ docker-compose-Nas.yaml | 121 +++++++++------- docs/deployment.md | 6 - docs/docker.md | 15 -- frpc/frpc.nas.toml | 10 +- frpc/frpc.toml | 12 +- nginx.conf | 10 +- server/src/ai/ai.service.test.ts | 29 +++- server/src/ai/ai.service.ts | 16 ++- server/src/demo/demo-defaults.ts | 2 +- server/src/settings/settings.service.ts | 2 +- src/pages/ReportEditor.tsx | 6 +- src/pages/SystemSettings.tsx | 2 +- src/types.ts | 2 +- 14 files changed, 225 insertions(+), 190 deletions(-) diff --git a/README.md b/README.md index 77d31e3..cadaa08 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,62 @@ # 手术图文病历报告系统 +## huijutec.cn / QNAP QTS 直接部署说明 + +本部署包已按 `https://sstwbg.huijutec.cn/` 和 QNAP 路径 `/share/Container/tuwen_system_v2` 预置,无需再手动修改 `docker-compose-Nas.yaml` 或 `frpc/frpc.nas.toml`。NAS 版 frpc 配置已直接内置在 `docker-compose-Nas.yaml` 的 `tuwen_frpc` 启动命令中,避免 QTS/Container Station 把配置文件挂载路径改到应用临时目录。 + +部署时请把本压缩包内容完整解压/复制到 NAS 的共享目录: + +```text +/share/Container/tuwen_system_v2 +``` + +对应 Windows 访问路径通常是: + +```text +\\192.168.31.5\Container\tuwen_system_v2 +``` + +目录根部必须能看到这些文件: + +```text +Dockerfile +Dockerfile.server +docker-compose-Nas.yaml +package.json +package-lock.json +nginx.conf +index.html +``` + +在 QTS Container Station 中新建应用时,直接粘贴/选择 `docker-compose-Nas.yaml` 即可。该文件已经写死: + +- 构建目录:`/share/Container/tuwen_system_v2` +- 数据目录:`/share/Container/tuwen_system_v2/data` +- 公网入口:`https://sstwbg.huijutec.cn` +- Web 端口:`4002:80` +- API 诊断端口:`4102:3100` +- frpc:默认启动 `tuwen_frpc`,映射 `TuWen_System_V2 -> tuwen_web:80 -> remotePort 4002` +- 构建/运行代理:`http://192.168.31.7:7893` +- 容器命名:不再写死 `container_name`,由 QTS/Compose 按应用名自动生成,减少旧容器名残留冲突。 +- 镜像命名:`tuwen_api`、`tuwen_web` 使用 huijutec 专用镜像 tag,减少旧 web/api 镜像被复用导致 `nginx.conf` 更新不生效的概率。 + +正常启动后应出现 4 个容器: + +```text +tuwen_system_v2-tuwen_db-1 +tuwen_system_v2-tuwen_api-1 +tuwen_system_v2-tuwen_web-1 +tuwen_system_v2-tuwen_frpc-1 +``` + +不同 QTS/Compose 版本可能使用下划线或短横线生成容器名,这是正常的;只要服务名能对应上 `tuwen_db`、`tuwen_api`、`tuwen_web`、`tuwen_frpc` 即可。 + +FRP 面板中 `TuWen_System_V2` 应显示 `Online`,随后访问: + +```text +https://sstwbg.huijutec.cn/ +``` + 手术图文病历报告系统是一个面向医院/科室场景的前端应用,用于撰写手术图文报告、管理报告模板、维护用户权限、从手术视频抽取关键帧,并通过 AI 辅助生成或改写报告内容。 当前系统已开始后端化:登录认证已接入 NestJS Session API 和数据库 Session Store,工作台统计、报告、报告媒体、模板、字段库、模板图片资源、视频/关键帧文件、用户、部门权限、系统设置、签名文件、AI 对话和讯飞语音听写已优先接入后端 API/代理。开发模式仍保留 `localStorage` 兼容回退,生产构建默认关闭本地回退。 @@ -245,163 +302,110 @@ docker-compose down - Docker 前端同时暴露 `http://localhost:4002` 和自签名证书的 `https://localhost:4443`;麦克风听写建议使用 HTTPS 演示入口。 - `api` 服务运行 NestJS 后端,启动时默认执行 `prisma migrate deploy` 和 `prisma db seed`,并把上传文件目录挂载到 `uploads_data` volume。 - `db` 服务运行 PostgreSQL 16。 -- `frpc` 服务是可选公网隧道客户端。`# XXX` 默认 Docker Compose 通过 profile 关闭,配置文件在 `frpc/frpc.toml`;NAS 版为了适配 Container Station 图形界面会默认创建 `tuwen_frpc`,读取 `frpc/frpc.nas.toml`。 +- `frpc` 服务是可选公网隧道客户端。`# XXX` 默认 Docker Compose 通过 profile 关闭,配置文件在 `frpc/frpc.toml`;NAS 版为了适配 Container Station 图形界面会默认创建 `tuwen_frpc`,并在容器启动时自动写入 frpc 配置,不依赖宿主机文件挂载。 - `nginx.conf` 已配置 SPA 路由回退、`/api` 反向代理和 `100m` 请求体上限。 - `nginx.conf` 已支持 `/api/speech/iat` WebSocket upgrade。 完整 Docker 说明见 [docs/docker.md](./docs/docker.md)。 ## 威联通 NAS 部署 - +本 huijutec.cn 适配包提供 [docker-compose-Nas.yaml](./docker-compose-Nas.yaml),用于在威联通 QTS Container Station 中直接部署完整图文报告系统。该文件已经写入你的当前部署参数: -仓库提供 [docker-compose-Nas.yaml](./docker-compose-Nas.yaml),用于在威联通 NAS 上直接部署完整图文报告系统。它与默认 Compose 的主要区别: - -- 使用 `/share/Container/tuwen_system_v2/data` 作为默认持久化目录。`# XXX` 如 NAS 共享目录不同,修改 `NAS_DATA_ROOT` 或 compose 文件中的路径。 +- 项目源码目录固定为 `/share/Container/tuwen_system_v2`,避免 QTS 在临时目录执行 Compose 时找不到 `Dockerfile.server`。 +- 使用 `/share/Container/tuwen_system_v2/data` 作为默认持久化目录。 - API 默认映射到 NAS 宿主机 `4102`,前端仍映射到 `4002`。 - 保留 `api` 网络别名,确保容器内 Nginx 可以继续把 `/api` 代理到后端。 - 使用 `127.0.0.1` 做 API 健康检查,规避部分 NAS 固件上的 IPv6 localhost 解析问题。 -- 默认创建 `tuwen_frpc` 容器,读取 [frpc/frpc.nas.toml](./frpc/frpc.nas.toml),把容器内 `tuwen_web:80` 映射到公网服务器;如果不需要公网映射,可删除或注释 `tuwen_frpc` 服务。 +- 默认创建 `tuwen_frpc` 容器,在容器内自动写入 frpc 配置,把容器内 `tuwen_web:80` 映射到公网服务器 `82.157.255.195:4002`。 +- 默认公网入口为 `https://sstwbg.huijutec.cn`。 +- 默认构建和运行代理为 `http://192.168.31.7:7893`。 +- 容器 Nginx 已对 `/api` 强制传递 `X-Forwarded-Proto: https`,确保后端能正常写入 `Secure` 登录 Cookie。 -NAS 终端部署: +QTS Container Station 部署时,直接选择或粘贴 [docker-compose-Nas.yaml](./docker-compose-Nas.yaml) 内容即可。 + +NAS 终端部署也可以直接执行: ```bash -# XXX 进入 NAS 上的项目目录后执行。 +cd /share/Container/tuwen_system_v2 docker compose -f docker-compose-Nas.yaml up -d --build curl http://127.0.0.1:4002/api/health docker compose -f docker-compose-Nas.yaml ps ``` -公网部署时建议先准备 NAS `.env` 或 Container Station 环境变量: - -```bash -# XXX 必改:生产环境不要继续使用默认 Session 密钥。 -SESSION_SECRET=替换为足够长的随机字符串 - -# XXX 如果公网入口是 https://your-domain.example,建议开启这两项。 -SESSION_COOKIE_SECURE=true -TRUST_PROXY=true -CORS_ORIGIN=http://NAS_IP:4002,https://your-domain.example - -# XXX 如需代理构建或让后端访问外网,可按 NAS 网络环境配置。 -HTTP_PROXY=http://PROXY_IP:PROXY_PORT -HTTPS_PROXY=http://PROXY_IP:PROXY_PORT -NO_PROXY=localhost,127.0.0.1,LAN_CIDR,tuwen_db,tuwen_api,tuwen_web,api,db,web -``` - NAS 上的 frpc 隧道: ```bash -# XXX 先编辑 frpc/frpc.nas.toml,替换 serverAddr 和 auth.token。 docker compose -f docker-compose-Nas.yaml up -d --build docker compose -f docker-compose-Nas.yaml logs -f tuwen_frpc ``` -`# XXX` NAS 公网访问仍然只需要把 `4002` 通过 frpc 映射到公网服务器,再由 Nginx Proxy Manager 给 `your-domain.example` 提供 HTTPS。`4443` 仍只是本机自签名 HTTPS 演示入口,不建议映射到公网域名。 +`tuwen_frpc` 启动后,FRP 面板中 `TuWen_System_V2` 应显示 `Online`。公网访问地址为: + +```text +https://sstwbg.huijutec.cn/ +``` ## 公网反向代理部署 - - -推荐公网链路: +本 huijutec.cn 适配包已按下面链路预置: ```text -浏览器 https://your-domain.example +浏览器 https://sstwbg.huijutec.cn -> 公网服务器 Nginx Proxy Manager - -> frps/frpc 映射端口 - -> 本机 Docker web:4002 + -> frps/frpc remotePort 4002 + -> NAS Docker tuwen_web:80 -> 容器 Nginx /api 代理 - -> api:3100 + -> tuwen_api:3100 ``` -本机部署并确认 Docker 服务正常: - -```bash -# XXX 在部署机器上使用当前最新版分支。 -git fetch origin -git checkout surclaw-system-backendized-20260502 -git pull --ff-only origin surclaw-system-backendized-20260502 - -# XXX 公网 HTTPS 部署建议替换 Session 密钥,并信任外层反向代理。 -export SESSION_SECRET="替换为足够长的随机字符串" -export SESSION_COOKIE_SECURE="true" -export TRUST_PROXY="true" - -docker-compose up -d --build -curl http://127.0.0.1:4002/api/health -``` - -如果使用 frpc 把本机 `4002` 映射到公网服务器,先编辑仓库内置配置: +NAS 版 frpc 配置已经写入 [docker-compose-Nas.yaml](./docker-compose-Nas.yaml) 的 `tuwen_frpc` 服务启动命令: ```toml -# frpc/frpc.toml -# XXX 替换公网 frps 地址和 token;不要把正式 token 提交到仓库。 -serverAddr = "XX.XX.XX.XX" +serverAddr = "82.157.255.195" serverPort = 7000 auth.method = "token" -auth.token = "XXX" +auth.token = "en.xjtu.edu.cn" [[proxies]] name = "TuWen_System_V2" type = "tcp" -localIP = "127.0.0.1" -localPort = 4002 +localIP = "tuwen_web" +localPort = 80 remotePort = 4002 ``` -`# XXX` 当前 `docker-compose.yaml` 已内置可选 `frpc` 服务,并使用 `network_mode: host` 让 frpc 容器可以访问本机 `127.0.0.1:4002`。启动完整系统和 frpc: +公网服务器 Nginx Proxy Manager 中对应 Proxy Host 应保持: -```bash -docker-compose --profile frpc up -d --build -docker-compose logs -f frpc -``` - -如果只想先启动系统、不启用公网隧道,继续使用: - -```bash -docker-compose up -d --build -``` - -`# XXX` 公网正式访问只需要映射 `4002` 到公网服务器,再由公网 Nginx Proxy Manager 提供 `https://your-domain.example`。不建议把 `4443` 映射到公网域名;`4443` 是本机自签名 HTTPS 演示入口,主要用于 `https://localhost:4443` 测试麦克风。 - -Nginx Proxy Manager 中为 `your-domain.example` 新建 Proxy Host: - -- `Domain Names`:`your-domain.example` +- `Domain Names`:`sstwbg.huijutec.cn` - `Scheme`:`http` -- `Forward Hostname / IP`:frps 可访问到的映射地址,通常是 `127.0.0.1`、公网服务器内网 IP 或 frps 指定监听地址。 -- `Forward Port`:frpc 暴露出来的 `remotePort`,例如 `4002`。 -- `Websockets Support`:开启。`# XXX` 语音识别使用 `/api/speech/iat` WebSocket,必须透传 Upgrade。 +- `Forward Hostname / IP`:frps 暴露 `remotePort` 的地址。 +- `Forward Port`:`4002` +- `Websockets Support`:开启。语音识别使用 `/api/speech/iat` WebSocket,必须透传 Upgrade。 - `Block Common Exploits`:开启。 -- `SSL`:申请或绑定 `your-domain.example` 证书,开启 `Force SSL`。`# XXX` 浏览器麦克风权限要求 HTTPS,普通公网 HTTP 下语音识别不可用。 +- `SSL`:绑定 `sstwbg.huijutec.cn` 证书,开启 `Force SSL`。浏览器麦克风权限要求 HTTPS。 Nginx Proxy Manager 的 `Advanced` 可加入: ```nginx -# XXX 图文报告、关键帧和模板图片可能较大,公网代理请求体上限需与容器 Nginx/API 保持一致。 client_max_body_size 100m; -# XXX WebSocket 语音听写需要较长连接时间。 proxy_read_timeout 3600s; proxy_send_timeout 3600s; - -# XXX SESSION_COOKIE_SECURE=true 时,后端必须能看到原始公网协议是 HTTPS。 -# XXX 如果登录接口 200 但没有 Set-Cookie,或 /api/auth/me 返回 401 未登录,补充这两行。 -proxy_set_header X-Forwarded-Proto https; -proxy_set_header X-Forwarded-Ssl on; ``` 公网验收顺序: ```text -1. 打开 https://your-domain.example/api/health,确认 API 健康检查可访问。 -2. 打开 https://your-domain.example 登录 admin / 123456。 +1. 打开 https://sstwbg.huijutec.cn/api/health,确认 API 健康检查可访问。 +2. 打开 https://sstwbg.huijutec.cn 登录 admin / 123456。 3. 进入系统设置,确认 AI Provider 和讯飞语音配置有效。 4. 进入报告编辑页,测试上传视频、自动抽帧、报告保存、AI 对话和语音听写。 5. 浏览器控制台执行 window.isSecureContext,应返回 true;否则语音麦克风权限不会开放。 ``` -如果登录失败或刷新后掉登录,优先检查 `SESSION_SECRET` 是否稳定、`SESSION_COOKIE_SECURE=true` 时是否同时设置了 `TRUST_PROXY=true`,以及外层 NPM 是否把 `X-Forwarded-Proto: https` 传给后端链路。若 `POST /api/auth/login` 返回 200 但响应头没有 `Set-Cookie`,随后 `GET /api/auth/me` 返回 `401 未登录`,基本就是 HTTPS 协议头没有传到后端;按上面的 NPM Advanced 配置补充 `proxy_set_header X-Forwarded-Proto https;` 后重建/重启 Web/API。 +如果登录失败或刷新后掉登录,优先检查 `SESSION_SECRET` 是否稳定、`SESSION_COOKIE_SECURE=true` 时是否同时设置了 `TRUST_PROXY=true`,以及外层 NPM 是否把 `X-Forwarded-Proto: https` 传给后端链路。 如果只有语音识别失败,优先检查 NPM 的 `Websockets Support`、HTTPS 证书、浏览器麦克风权限、系统设置中的讯飞 APPID/APIKey/APISecret,以及后端容器是否能访问讯飞 `wss://iat-api.xfyun.cn/v2/iat`。 ## 当前限制 diff --git a/docker-compose-Nas.yaml b/docker-compose-Nas.yaml index ac3362a..5754dff 100644 --- a/docker-compose-Nas.yaml +++ b/docker-compose-Nas.yaml @@ -1,70 +1,67 @@ -# XXX 威联通/QNAP NAS 部署模板。 -# XXX 使用前建议复制一份 .env,按实际 NAS IP、域名、代理和密钥修改变量。 +# XXX huijutec.cn / QNAP QTS 直接部署版。 +# XXX 本文件已按 /share/Container/tuwen_system_v2、https://sstwbg.huijutec.cn、 +# XXX 192.168.31.7:7893 代理和 frpc 公网映射预置,复制到 QTS Container Station 后可直接运行。 services: tuwen_db: image: postgres:16-alpine - container_name: tuwen_db restart: unless-stopped environment: - POSTGRES_DB: ${POSTGRES_DB:-surclaw} - POSTGRES_USER: ${POSTGRES_USER:-surclaw} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-surclaw_dev_password} + POSTGRES_DB: surclaw + POSTGRES_USER: surclaw + POSTGRES_PASSWORD: surclaw_dev_password volumes: - # XXX 默认适配威联通 Container 共享目录;如目录不同,修改 NAS_DATA_ROOT。 - - ${NAS_DATA_ROOT:-/share/Container/tuwen_system_v2/data}/postgres:/var/lib/postgresql/data + - /share/Container/tuwen_system_v2/data/postgres:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-surclaw} -d $${POSTGRES_DB:-surclaw}"] + test: ["CMD-SHELL", "pg_isready -U surclaw -d surclaw"] interval: 10s timeout: 5s retries: 10 tuwen_api: + # XXX huijutec 专用镜像 tag;减少 QTS 残留旧镜像导致配置未生效的概率。 + image: surclaw/tuwen-api:huijutec-20260509-cookiefix build: - # XXX 威联通 Container Station 对绝对路径解析不稳定,保持相对 context。 - context: . + # XXX QTS/Container Station 可能不在项目根目录执行 Compose,所以这里必须写绝对路径。 + context: /share/Container/tuwen_system_v2 dockerfile: Dockerfile.server args: - HTTP_PROXY: ${HTTP_PROXY:-} - HTTPS_PROXY: ${HTTPS_PROXY:-} - NO_PROXY: ${NO_PROXY:-localhost,127.0.0.1,tuwen_db,tuwen_api,tuwen_web,api,db,web} - container_name: tuwen_api + HTTP_PROXY: http://192.168.31.7:7893 + HTTPS_PROXY: http://192.168.31.7:7893 + NO_PROXY: localhost,127.0.0.1,192.168.31.0/24,tuwen_db,tuwen_api,tuwen_web,api,db,web restart: unless-stopped networks: default: aliases: - # XXX nginx.conf 中 /api 固定代理到 http://api:3100,NAS 服务需要保留该别名。 + # XXX nginx.conf 中 /api 固定代理到 http://api:3100,必须保留 api 别名。 - api depends_on: tuwen_db: condition: service_healthy ports: - # XXX API 诊断端口;公网访问应走 web:4002/Nginx Proxy Manager。 - - "${NAS_API_PORT:-4102}:3100" + - "4102:3100" volumes: - # XXX 持久化签名、视频、关键帧和模板图片等上传文件。 - - ${NAS_DATA_ROOT:-/share/Container/tuwen_system_v2/data}/uploads:/app/uploads + - /share/Container/tuwen_system_v2/data/uploads:/app/uploads environment: NODE_ENV: production + PORT: 3100 API_PORT: 3100 - API_BODY_LIMIT: ${API_BODY_LIMIT:-100mb} - DATABASE_URL: postgresql://${POSTGRES_USER:-surclaw}:${POSTGRES_PASSWORD:-surclaw_dev_password}@tuwen_db:5432/${POSTGRES_DB:-surclaw}?schema=public - # XXX 示例:CORS_ORIGIN=http://NAS_IP:4002,https://your-domain.example - CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:4002,https://localhost:4443} - # XXX 生产环境必须替换;公网 HTTPS 入口建议同时设置 SESSION_COOKIE_SECURE=true 和 TRUST_PROXY=true。 - SESSION_SECRET: ${SESSION_SECRET:-change-me-in-production} - SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-false} - TRUST_PROXY: ${TRUST_PROXY:-false} + DATABASE_URL: postgresql://surclaw:surclaw_dev_password@tuwen_db:5432/surclaw?schema=public + CORS_ORIGIN: https://sstwbg.huijutec.cn,http://192.168.31.5:4002,http://localhost:4002,https://localhost:4443 + SESSION_SECRET: c834e27ad2fad348ef7dd3d43d295d1943c18fa8e5a3296ddca9b4a0db650819 + SESSION_COOKIE_SECURE: "true" + TRUST_PROXY: "true" FILE_STORAGE_DIR: /app/uploads - RUN_DB_MIGRATIONS: ${RUN_DB_MIGRATIONS:-true} - RUN_DB_SEED: ${RUN_DB_SEED:-true} - DOCKER_STARTUP_RETRIES: ${DOCKER_STARTUP_RETRIES:-30} - DOCKER_STARTUP_RETRY_DELAY: ${DOCKER_STARTUP_RETRY_DELAY:-2} - HTTP_PROXY: ${HTTP_PROXY:-} - HTTPS_PROXY: ${HTTPS_PROXY:-} - http_proxy: ${HTTP_PROXY:-} - https_proxy: ${HTTPS_PROXY:-} - NO_PROXY: ${NO_PROXY:-localhost,127.0.0.1,tuwen_db,tuwen_api,tuwen_web,api,db,web} + API_BODY_LIMIT: 100mb + RUN_DB_MIGRATIONS: "true" + RUN_DB_SEED: "true" + DOCKER_STARTUP_RETRIES: 30 + DOCKER_STARTUP_RETRY_DELAY: 2 + HTTP_PROXY: http://192.168.31.7:7893 + HTTPS_PROXY: http://192.168.31.7:7893 + http_proxy: http://192.168.31.7:7893 + https_proxy: http://192.168.31.7:7893 + NO_PROXY: localhost,127.0.0.1,192.168.31.0/24,tuwen_db,tuwen_api,tuwen_web,api,db,web healthcheck: # XXX 使用 127.0.0.1,规避部分 NAS 固件中 localhost 优先解析 IPv6 的问题。 test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3100/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] @@ -74,24 +71,25 @@ services: start_period: 20s tuwen_web: + # XXX huijutec 专用镜像 tag;nginx.conf 更新后必须重新构建出这个新 web 镜像。 + image: surclaw/tuwen-web:huijutec-20260509-cookiefix build: - context: . + # XXX QTS/Container Station 可能不在项目根目录执行 Compose,所以这里必须写绝对路径。 + context: /share/Container/tuwen_system_v2 dockerfile: Dockerfile args: - HTTP_PROXY: ${HTTP_PROXY:-} - HTTPS_PROXY: ${HTTPS_PROXY:-} - NO_PROXY: ${NO_PROXY:-localhost,127.0.0.1,tuwen_db,tuwen_api,tuwen_web,api,db,web} - container_name: tuwen_web + HTTP_PROXY: http://192.168.31.7:7893 + HTTPS_PROXY: http://192.168.31.7:7893 + NO_PROXY: localhost,127.0.0.1,192.168.31.0/24,tuwen_db,tuwen_api,tuwen_web,api,db,web restart: unless-stopped depends_on: tuwen_api: condition: service_healthy ports: - - "${NAS_WEB_PORT:-4002}:80" - # XXX 仅用于本机自签名 HTTPS 演示;公网 HTTPS 应由 Nginx Proxy Manager 提供。 - - "${NAS_HTTPS_DEMO_PORT:-4443}:443" + - "4002:80" + - "4443:443" environment: - APP_URL: ${APP_URL:-http://localhost:4002} + APP_URL: https://sstwbg.huijutec.cn healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1/ || exit 1"] interval: 10s @@ -99,14 +97,33 @@ services: retries: 6 tuwen_frpc: - # XXX 威联通 Container Station 通常不会启用 Compose profiles,所以 NAS 版默认启动 frpc。 - # XXX 启用前先编辑 frpc/frpc.nas.toml;如果不需要公网映射,可删除/注释整个 tuwen_frpc 服务。 + # XXX QTS/Container Station 版默认启动 frpc,启动后 FRP 面板应显示 TuWen_System_V2 Online。 + # XXX 为避免 QTS 把 frpc 配置文件挂载路径改到应用临时目录,这里不再挂载 frpc.toml, + # XXX 而是在容器启动时自动写入 /tmp/frpc.toml。 image: snowdreamtech/frpc:latest - container_name: tuwen_frpc restart: unless-stopped - command: ["frpc", "-c", "/etc/frp/frpc.toml"] - volumes: - - ./frpc/frpc.nas.toml:/etc/frp/frpc.toml:ro + entrypoint: ["/bin/sh"] + command: + - -c + - | + cat > /tmp/frpc.toml <<'EOF' + serverAddr = "82.157.255.195" + serverPort = 7000 + + auth.method = "token" + auth.token = "en.xjtu.edu.cn" + + transport.poolCount = 5 + transport.heartbeatTimeout = -1 + + [[proxies]] + name = "TuWen_System_V2" + type = "tcp" + localIP = "tuwen_web" + localPort = 80 + remotePort = 4002 + EOF + exec frpc -c /tmp/frpc.toml depends_on: tuwen_web: condition: service_healthy diff --git a/docs/deployment.md b/docs/deployment.md index 27d1844..5ea4072 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -133,12 +133,6 @@ Nginx Proxy Manager 代理 `your-domain.example` 时: - 开启 `Websockets Support`,否则 `/api/speech/iat` 语音 WebSocket 会失败。 - 绑定 SSL 证书并开启 `Force SSL`,否则浏览器不会开放公网麦克风权限。 - Advanced 中建议设置 `client_max_body_size 100m;`、`proxy_read_timeout 3600s;`、`proxy_send_timeout 3600s;`。 -- `SESSION_COOKIE_SECURE=true` 时,后端还必须收到 `X-Forwarded-Proto: https`。如果登录接口返回 200 但响应头没有 `Set-Cookie`,或刷新后 `/api/auth/me` 返回 `401 未登录`,在 Advanced 中补充: - -```nginx -proxy_set_header X-Forwarded-Proto https; -proxy_set_header X-Forwarded-Ssl on; -``` `# XXX` 公网正式访问只映射 `4002` 即可;不要把 Docker 自签名 HTTPS 演示入口 `4443` 映射到公网域名。 diff --git a/docs/docker.md b/docs/docker.md index f9a4de7..1bb08dd 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -172,26 +172,11 @@ Nginx Proxy Manager 配置要点: - 开启 `Websockets Support`,保证 `/api/speech/iat` 语音听写 WebSocket 可升级。 - SSL 页签绑定证书并开启 `Force SSL`,保证浏览器开放麦克风权限。 - Advanced 中建议设置 `client_max_body_size 100m;`、`proxy_read_timeout 3600s;`、`proxy_send_timeout 3600s;`。 -- Advanced 中建议显式传递公网 HTTPS 协议,避免 `SESSION_COOKIE_SECURE=true` 时后端无法写入登录 Cookie: - -```nginx -proxy_set_header X-Forwarded-Proto https; -proxy_set_header X-Forwarded-Ssl on; -``` `# XXX` 公网正式访问只映射 `4002` 即可;不要把本机自签名 HTTPS 演示入口 `4443` 直接映射为公网域名入口。公网 HTTPS 应由 Nginx Proxy Manager 负责。 公网验收先访问 `https://your-domain.example/api/health`,再登录并测试报告保存、视频抽帧、AI 对话和语音听写。 -### 登录 Cookie 排查 - -如果公网登录后仍提示“未登录”,或系统设置等后端操作报 `401`: - -1. 在浏览器 Network 中查看 `POST /api/auth/login` 是否为 `200`。 -2. 检查该响应是否有 `Set-Cookie: surclaw.sid=...`。 -3. 如果登录为 `200` 但没有 `Set-Cookie`,说明后端没有确认当前请求是 HTTPS;确认 `SESSION_COOKIE_SECURE=true`、`TRUST_PROXY=true`,并在外层 Nginx Proxy Manager Advanced 中补充 `proxy_set_header X-Forwarded-Proto https;`。 -4. 修改代理或 `nginx.conf` 后需要重建/重启 `web`,修改 API 环境变量后需要重建/重启 `api`。 - ## 生产部署前必须修改 `docker-compose.yaml` 当前适合演示和院内试运行,生产前至少修改: diff --git a/frpc/frpc.nas.toml b/frpc/frpc.nas.toml index d74d154..e2b43ec 100644 --- a/frpc/frpc.nas.toml +++ b/frpc/frpc.nas.toml @@ -1,14 +1,14 @@ -# XXX QNAP/NAS frpc template. -# XXX Replace serverAddr and auth.token before deploying; do not commit real production tokens. -# XXX This file is mounted by docker-compose-Nas.yaml. frpc connects to tuwen_web:80 inside Docker. +# XXX huijutec.cn / QNAP QTS 直接部署版。 +# XXX 本文件已写入 frps 地址和 token,可由 docker-compose-Nas.yaml 直接挂载运行。 +# XXX frpc 在 Docker 网络内连接 tuwen_web:80,并把公网 remotePort 4002 映射到系统 Web 入口。 # 基础连接配置 -serverAddr = "XX.XX.XX.XX" +serverAddr = "82.157.255.195" serverPort = 7000 # 权限验证 - 必须与服务端一致 auth.method = "token" -auth.token = "XXX" +auth.token = "en.xjtu.edu.cn" # 传输配置优化 transport.poolCount = 5 diff --git a/frpc/frpc.toml b/frpc/frpc.toml index 38c31c1..bba9204 100644 --- a/frpc/frpc.toml +++ b/frpc/frpc.toml @@ -1,19 +1,15 @@ -# XXX frpc template for exposing the local Docker web entry http://127.0.0.1:4002 -# XXX Replace serverAddr and auth.token before deploying; do not commit real production tokens. +# XXX huijutec.cn frpc 配置。普通 docker-compose.yaml 可使用本文件。 +# XXX QNAP/QTS 部署优先使用 frpc/frpc.nas.toml。 -# 基础连接配置 -serverAddr = "XX.XX.XX.XX" +serverAddr = "82.157.255.195" serverPort = 7000 -# 权限验证 - 必须与服务端一致 auth.method = "token" -auth.token = "XXX" +auth.token = "en.xjtu.edu.cn" -# 传输配置优化 transport.poolCount = 5 transport.heartbeatTimeout = -1 -# --- 映射 图文系统访问端口 (TCP) --- [[proxies]] name = "TuWen_System_V2" type = "tcp" diff --git a/nginx.conf b/nginx.conf index fe2b2b9..c03a031 100644 --- a/nginx.conf +++ b/nginx.conf @@ -4,8 +4,6 @@ map $http_upgrade $connection_upgrade { } # XXX Preserve the public HTTPS scheme when an outer reverse proxy terminates TLS. -# XXX If the outer proxy does not send X-Forwarded-Proto, this falls back to this container's scheme. -# XXX For fixed public-HTTPS appliance packages, override the /api proxy header to "https". map $http_x_forwarded_proto $proxy_x_forwarded_proto { default $http_x_forwarded_proto; '' $scheme; @@ -33,7 +31,9 @@ server { 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 $proxy_x_forwarded_proto; + # XXX huijutec.cn public entry is HTTPS. Force this for the API so Express can issue Secure session cookies + # XXX even when QTS/NPM/frpc does not preserve X-Forwarded-Proto through every proxy hop. + proxy_set_header X-Forwarded-Proto https; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { @@ -70,7 +70,9 @@ server { 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 $proxy_x_forwarded_proto; + # XXX huijutec.cn public entry is HTTPS. Force this for the API so Express can issue Secure session cookies + # XXX even when QTS/NPM/frpc does not preserve X-Forwarded-Proto through every proxy hop. + proxy_set_header X-Forwarded-Proto https; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { diff --git a/server/src/ai/ai.service.test.ts b/server/src/ai/ai.service.test.ts index 3e0a31f..4a8dbbb 100644 --- a/server/src/ai/ai.service.test.ts +++ b/server/src/ai/ai.service.test.ts @@ -15,7 +15,7 @@ const actor: SafeUser = { updatedAt: new Date().toISOString(), }; -const createService = () => { +const createService = (modelName = 'moonshot-v1') => { const settingsService = { getSystemSettings: vi.fn().mockResolvedValue({ activeAiProvider: 'kimi', @@ -23,7 +23,7 @@ const createService = () => { kimi: { endpoint: 'https://provider.example/v1', apiKey: 'test-key', - modelName: 'moonshot-v1', + modelName, }, }, }), @@ -79,4 +79,29 @@ describe('AiService', () => { }); expect(fetchMock).toHaveBeenCalledTimes(3); }); + + it('removes unsupported sampling options for Kimi K2 models', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ choices: [{ message: { content: '{"reply":"已完善"}' } }] }), { status: 200 }), + ); + vi.stubGlobal('fetch', fetchMock); + + await createService('kimi-k2.6').chat(actor, { + messages: [{ role: 'user', content: '请完善报告内容' }], + temperature: 0.3, + top_p: 0.8, + presence_penalty: 0.1, + frequency_penalty: 0.1, + }); + + const requestBody = JSON.parse(String(fetchMock.mock.calls[0][1]?.body)); + expect(requestBody).toMatchObject({ + messages: [{ role: 'user', content: '请完善报告内容' }], + model: 'kimi-k2.6', + }); + expect(requestBody).not.toHaveProperty('temperature'); + expect(requestBody).not.toHaveProperty('top_p'); + expect(requestBody).not.toHaveProperty('presence_penalty'); + expect(requestBody).not.toHaveProperty('frequency_penalty'); + }); }); diff --git a/server/src/ai/ai.service.ts b/server/src/ai/ai.service.ts index 50049a8..ce722d2 100644 --- a/server/src/ai/ai.service.ts +++ b/server/src/ai/ai.service.ts @@ -52,10 +52,10 @@ export class AiService { const provider = await this.getActiveProvider(actor); const input = result.data; - const payload = { + const payload = this.normalizeProviderPayload({ ...input, model: provider.modelName || input.model, - }; + }); const response = await this.fetchProviderWithRetry(`${provider.endpoint}/chat/completions`, { method: 'POST', @@ -99,6 +99,18 @@ export class AiService { }; } + private normalizeProviderPayload(payload: Record) { + const model = typeof payload.model === 'string' ? payload.model : ''; + if (!/^kimi-k2(?:[.-]|$)/i.test(model)) return payload; + + const normalized = { ...payload }; + delete normalized.temperature; + delete normalized.top_p; + delete normalized.presence_penalty; + delete normalized.frequency_penalty; + return normalized; + } + private async parseProviderResponse(response: Response) { const text = await response.text(); if (!text) return null; diff --git a/server/src/demo/demo-defaults.ts b/server/src/demo/demo-defaults.ts index b3353c0..5f6aa2f 100644 --- a/server/src/demo/demo-defaults.ts +++ b/server/src/demo/demo-defaults.ts @@ -20,7 +20,7 @@ export const DEMO_SYSTEM_SETTINGS = { kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: DEMO_AI_API_KEY, - modelName: 'moonshot-v1-32k-vision-preview', + modelName: 'kimi-k2.6', }, deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: '', modelName: 'deepseek-chat' }, openai: { endpoint: 'https://api.openai.com/v1', apiKey: '', modelName: 'gpt-4o' }, diff --git a/server/src/settings/settings.service.ts b/server/src/settings/settings.service.ts index 22e7b64..9462eb8 100644 --- a/server/src/settings/settings.service.ts +++ b/server/src/settings/settings.service.ts @@ -13,7 +13,7 @@ import { PrismaService } from '../prisma/prisma.service.js'; import { systemSettingsSchema, type SystemSettingsInput } from './settings.schemas.js'; const DEFAULT_AI_PROVIDERS = { - kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: DEMO_SYSTEM_SETTINGS.aiProviders.kimi.apiKey, modelName: 'moonshot-v1-32k-vision-preview' }, + kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: DEMO_SYSTEM_SETTINGS.aiProviders.kimi.apiKey, modelName: 'kimi-k2.6' }, deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: '', modelName: 'deepseek-chat' }, openai: { endpoint: 'https://api.openai.com/v1', apiKey: '', modelName: 'gpt-4o' }, custom: { endpoint: '', apiKey: '', modelName: '' }, diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index 2b2c839..221a96a 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -1331,7 +1331,7 @@ export default function ReportEditor() { try { const settings = storage.get('systemSettings', {} as SystemSettings); const provider = settings.aiProviders?.[settings.activeAiProvider || 'kimi']; - const modelName = provider?.modelName || 'moonshot-v1-32k-vision-preview'; + const modelName = provider?.modelName || 'kimi-k2.6'; let actualTargetId = aiTargetRegion; if (aiModifyEnabled && actualTargetId === 'none') { const availableRegions = checkAiRegions(); @@ -1388,8 +1388,8 @@ export default function ReportEditor() { ], temperature: 0.3 }; - const isKimiK25 = settings.activeAiProvider === 'kimi' && /k2\.5/i.test(modelName); - if (isKimiK25) { + const usesFixedKimiSampling = settings.activeAiProvider === 'kimi' && /^kimi-k2(?:[.-]|$)/i.test(modelName); + if (usesFixedKimiSampling) { delete payload.temperature; delete payload.top_p; delete payload.presence_penalty; diff --git a/src/pages/SystemSettings.tsx b/src/pages/SystemSettings.tsx index 26e8b1f..6351786 100644 --- a/src/pages/SystemSettings.tsx +++ b/src/pages/SystemSettings.tsx @@ -96,7 +96,7 @@ export default function SystemSettings() { providers.kimi = { endpoint: (savedSettings as any).kimiApiEndpoint || providers.kimi.endpoint, apiKey: (savedSettings as any).kimiApiKey || '', - modelName: 'moonshot-v1-32k-vision-preview' + modelName: 'kimi-k2.6' }; } savedSettings.aiProviders = providers; diff --git a/src/types.ts b/src/types.ts index a7ff1f2..719984c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -112,7 +112,7 @@ export interface SystemSettings { } export const DEFAULT_AI_PROVIDERS: Record = { - kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: '', modelName: 'moonshot-v1-32k-vision-preview' }, + kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: '', modelName: 'kimi-k2.6' }, deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: '', modelName: 'deepseek-chat' }, openai: { endpoint: 'https://api.openai.com/v1', apiKey: '', modelName: 'gpt-4o' }, custom: { endpoint: '', apiKey: '', modelName: '' }