From 595c8da595eabd554ab2156f90522814ec07f584 Mon Sep 17 00:00:00 2001 From: Zhu Junhao <2661664053@qq.com> Date: Fri, 4 Sep 2026 15:30:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9E=84=E5=BB=BA=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E5=AE=B9=E5=99=A8=E5=8C=96=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 22 ++++++ .gitattributes | 6 ++ README.md | 53 +++++++++++++-- compose.yml | 25 ------- docker-compose.yml | 131 ++++++++++++++++++++++++++++++++++++ server/.dockerignore | 6 ++ server/Dockerfile | 37 ++++++++++ server/docker-entrypoint.sh | 25 +++++++ web-ui/.dockerignore | 7 ++ web-ui/Dockerfile | 21 ++++++ web-ui/nginx.conf | 33 +++++++++ 11 files changed, 335 insertions(+), 31 deletions(-) create mode 100644 .env.example create mode 100644 .gitattributes delete mode 100644 compose.yml create mode 100644 docker-compose.yml create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile create mode 100644 server/docker-entrypoint.sh create mode 100644 web-ui/.dockerignore create mode 100644 web-ui/Dockerfile create mode 100644 web-ui/nginx.conf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..93d25ba --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# 复制为 .env 后必须替换以下三个敏感值;不要把真实 .env 提交到 Git。 +POSTGRES_PASSWORD=replace-with-a-strong-database-password +APP_MASTER_KEY=replace-with-at-least-32-random-characters +APP_ADMIN_PASSWORD=replace-with-a-strong-admin-password + +# 百炼知识库未启用时可以保留为空;模型 API Key 仍通过页面配置并加密保存。 +DASHSCOPE_API_KEY= + +# 非敏感运行参数。 +POSTGRES_DB=smart_factory_agent +POSTGRES_USER=smart_factory +APP_ADMIN_USERNAME=admin +APP_RUN_TIMEOUT=60m +SESSION_COOKIE_SECURE=false +MANUAGENT_DATA_ROOT=/srv/manuagent/data +# 默认值即部署要求的 0.0.0.0:5173;普通 Linux 部署无需修改。 +FRONTEND_PORT=5173 + +# 可选镜像标签,便于私有镜像仓库或版本升级时覆盖。 +AGENT_RUNTIME_IMAGE=smart-factory-agent-runtime:0.1.0 +BACKEND_IMAGE=manuagent-backend:0.1.0 +FRONTEND_IMAGE=manuagent-frontend:0.1.0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8740312 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# 容器内执行的 Shell 脚本必须使用 LF,避免 Windows 检出时生成 CRLF 导致解释器无法识别。 +*.sh text eol=lf + +# Docker 与 Nginx 配置统一使用 LF,便于在 Linux 容器内直接加载。 +Dockerfile text eol=lf +*.conf text eol=lf diff --git a/README.md b/README.md index b80fdb3..cea8447 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ ## 本地启动 ```bash -docker compose --profile build-only build agent-runtime +docker compose -f docker-compose.yml build agent-runtime docker compose up -d postgres mvn -q -f server/pom.xml spring-boot:run -npm --prefix client install -npm --prefix client run dev +npm --prefix web-ui install +npm --prefix web-ui run dev ``` -打开 ,本地默认账号为 `admin / admin123`。 +本地开发服务器默认使用 Vite 配置的端口。容器化部署固定从 访问。 模型连接只能在“模型配置”页面新增并持久化到 PostgreSQL;API Key 会使用 `APP_MASTER_KEY` 环境变量提供的主密钥加密后保存。`dashscope_key.txt` 仅供百炼知识库使用,不参与模型配置。 @@ -29,12 +29,53 @@ npm --prefix client run dev $env:APP_MASTER_KEY = '<使用独立生成的高强度密钥>' ``` +## Docker Compose 部署 + +部署包含 Nginx 前端、Spring Boot 后端、Agent Runtime 镜像和 PostgreSQL。Agent Runtime +不是常驻 API 服务:`agent-runtime` 服务完成镜像构建后会以状态码 0 退出,后端再通过 Docker +Socket 为每个 Agent Session 动态创建隔离容器。 + +1. 复制环境变量模板并替换其中的三个必填密码/密钥;生产环境建议使用密码管理系统生成随机值。 + + ```bash + cp .env.example .env + ``` + +2. 确认 `MANUAGENT_DATA_ROOT` 是 Docker 宿主机上的绝对路径。这个路径会以完全相同的路径挂载到 + 后端容器,供动态 Agent Runtime 继续挂载项目材料、工作目录和产物。Linux 默认值为 + `/srv/manuagent/data`。 + +3. 构建并启动完整服务: + + ```bash + docker compose -f docker-compose.yml up -d --build + docker compose -f docker-compose.yml ps + ``` + +4. 打开 ,使用 `.env` 中的 `APP_ADMIN_USERNAME` 和 + `APP_ADMIN_PASSWORD` 登录。查看日志或停止服务: + + ```bash + docker compose -f docker-compose.yml logs -f backend + docker compose -f docker-compose.yml down + ``` + +默认端口映射为 `0.0.0.0:5173:80`。若 Windows 的动态端口排除范围占用了 5173,可仅在本机 +验证时临时执行 `$env:FRONTEND_PORT = '15173'`;Linux 服务器部署应保留默认的 5173。 + +PostgreSQL 数据保存在 `postgres-data` 命名卷中;项目材料、Agent 工作区、快照和生成文件保存在 +`MANUAGENT_DATA_ROOT`。`docker compose down` 不会删除它们,只有显式增加 `--volumes` 才会删除 +PostgreSQL 卷。后端挂载 Docker Socket 等价于授予其管理宿主机容器的高权限,应只在受信任的 +Docker 主机上运行,并限制 5173 端口的网络访问范围。通过 HTTPS 反向代理部署时,请将 +`SESSION_COOKIE_SECURE` 设为 `true`。 + ## 验证 ```bash mvn -q -f server/pom.xml test -npm --prefix client run test -- --run -npm --prefix client run build +npm --prefix web-ui run test +npm --prefix web-ui run build +docker compose -f docker-compose.yml config --quiet ``` 产品、数据库与验收设计见 [docs](docs/)。内置 Skill 与资源由 Flyway 种子迁移写入数据库,无需额外导入文件。 diff --git a/compose.yml b/compose.yml deleted file mode 100644 index dfe0b74..0000000 --- a/compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -services: - agent-runtime: - image: smart-factory-agent-runtime:0.1.0 - build: - context: sandbox - profiles: ["build-only"] - - postgres: - image: postgres:17-alpine - environment: - POSTGRES_DB: smart_factory_agent - POSTGRES_USER: smart_factory - POSTGRES_PASSWORD: smart_factory - ports: - - "127.0.0.1:54330:5432" - volumes: - - smart-factory-pg:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U smart_factory -d smart_factory_agent"] - interval: 3s - timeout: 3s - retries: 20 - -volumes: - smart-factory-pg: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ceb1bcf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,131 @@ +name: manuagent + +services: + postgres: + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-smart_factory_agent} + POSTGRES_USER: ${POSTGRES_USER:-smart_factory} + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + secrets: + - postgres_password + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - manuagent-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 10s + + # 此服务负责构建并登记 AgentScope 使用的 Runtime 镜像。 + # 它成功退出后,后端会通过 Docker Socket 按 Session 动态创建真正执行任务的 Runtime 容器。 + agent-runtime: + image: ${AGENT_RUNTIME_IMAGE:-smart-factory-agent-runtime:0.1.0} + build: + context: ./sandbox + dockerfile: Dockerfile + command: ["/bin/true"] + restart: "no" + networks: + - manuagent-network + + backend: + image: ${BACKEND_IMAGE:-manuagent-backend:0.1.0} + build: + context: ./server + dockerfile: Dockerfile + restart: unless-stopped + init: true + depends_on: + postgres: + condition: service_healthy + restart: true + agent-runtime: + condition: service_completed_successfully + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-smart_factory_agent} + SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-smart_factory} + # Spring Boot 将 secrets 目录中的点分文件名作为配置属性加载,避免把密码写进普通环境变量。 + SPRING_CONFIG_IMPORT: optional:configtree:/run/secrets/ + APP_DATA_ROOT: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data} + APP_DASHSCOPE_KEY_FILE: /run/secrets/dashscope_key + APP_ADMIN_USERNAME: ${APP_ADMIN_USERNAME:-admin} + APP_SANDBOX_IMAGE: ${AGENT_RUNTIME_IMAGE:-smart-factory-agent-runtime:0.1.0} + APP_SANDBOX_NETWORK: manuagent-network + APP_RUN_TIMEOUT: ${APP_RUN_TIMEOUT:-60m} + SERVER_SERVLET_SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-false} + secrets: + - source: postgres_password + target: spring.datasource.password + - source: app_master_key + target: app.master-key + - source: admin_password + target: app.admin-password + - source: dashscope_api_key + target: dashscope_key + volumes: + # DockerSandbox 的 bind mount 源路径由宿主机 daemon 解释,因此容器内外必须使用相同绝对路径。 + - type: bind + source: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data} + target: ${MANUAGENT_DATA_ROOT:-/srv/manuagent/data} + # AgentScope 需要通过宿主机 Docker Engine 创建、执行并销毁隔离的 Runtime 容器。 + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + expose: + - "8080" + networks: + - manuagent-network + healthcheck: + test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://127.0.0.1:8080/api/auth/csrf"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 30s + stop_grace_period: 30s + + frontend: + image: ${FRONTEND_IMAGE:-manuagent-frontend:0.1.0} + build: + context: ./web-ui + dockerfile: Dockerfile + restart: unless-stopped + init: true + depends_on: + backend: + condition: service_healthy + restart: true + ports: + # 默认严格监听 0.0.0.0:5173;仅当宿主机保留该端口时,才通过 FRONTEND_PORT 临时覆盖。 + - "0.0.0.0:${FRONTEND_PORT:-5173}:80" + networks: + - manuagent-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--output-document=/dev/null", "http://127.0.0.1/"] + interval: 10s + timeout: 5s + retries: 6 + start_period: 10s + +networks: + manuagent-network: + # 固定网络名,确保后端动态创建的 Agent Runtime 容器可以加入同一个网络。 + name: manuagent-network + driver: bridge + +volumes: + postgres-data: + +secrets: + postgres_password: + environment: POSTGRES_PASSWORD + app_master_key: + environment: APP_MASTER_KEY + admin_password: + environment: APP_ADMIN_PASSWORD + dashscope_api_key: + environment: DASHSCOPE_API_KEY diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..8861ff7 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,6 @@ +target +.idea +*.iml +*.log +.env +.env.* diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..cbd88b5 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1 + +# Maven 构建阶段使用与项目一致的 JDK 21,并利用 BuildKit 缓存减少重复下载依赖的时间。 +FROM maven:3.9.13-eclipse-temurin-21 AS builder + +WORKDIR /workspace +COPY pom.xml ./ +COPY src ./src +# 直接打包只解析项目真正需要的依赖;独立 go-offline 会额外下载大量未参与构建的报告插件。 +RUN --mount=type=cache,target=/root/.m2 mvn -B -DskipTests package + +# AgentScope DockerSandbox 通过 docker 命令创建 Runtime,直接复用官方镜像中的 CLI 二进制。 +FROM docker:29-cli AS docker-cli + +FROM eclipse-temurin:21-jre-jammy + +# curl 用于容器健康检查;gosu 用于完成目录和 Docker Socket 权限初始化后降权运行 Java。 +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl gosu \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker + +RUN groupadd --gid 10001 manuagent \ + && useradd --uid 10001 --gid 10001 --create-home --shell /bin/bash manuagent \ + && mkdir -p /opt/manuagent /srv/manuagent/data \ + && chown -R manuagent:manuagent /opt/manuagent /srv/manuagent + +WORKDIR /opt/manuagent +COPY --from=builder /workspace/target/*.jar app.jar +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod 0755 /usr/local/bin/docker-entrypoint.sh + +EXPOSE 8080 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["java", "-XX:MaxRAMPercentage=75.0", "-Djava.security.egd=file:/dev/urandom", "-jar", "/opt/manuagent/app.jar"] diff --git a/server/docker-entrypoint.sh b/server/docker-entrypoint.sh new file mode 100644 index 0000000..9d68b7d --- /dev/null +++ b/server/docker-entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -eu + +# Agent Runtime 的目录通过宿主机 Docker daemon 再次挂载,因此后端必须能写入共享数据根目录。 +data_root="${APP_DATA_ROOT:-/srv/manuagent/data}" +mkdir -p "$data_root" +chown manuagent:manuagent "$data_root" + +# Docker Socket 的组 ID 在不同 Linux 发行版和 Docker Desktop 中并不固定。 +# 启动时读取真实组 ID 并把低权限应用用户加入对应组,避免以 root 身份运行 Spring Boot。 +docker_socket="/var/run/docker.sock" +if [ ! -S "$docker_socket" ]; then + echo "错误:未挂载 $docker_socket,后端无法创建 Agent Runtime 容器。" >&2 + exit 1 +fi + +docker_gid="$(stat -c '%g' "$docker_socket")" +docker_group="$(getent group "$docker_gid" | cut -d: -f1 || true)" +if [ -z "$docker_group" ]; then + docker_group="docker-host" + groupadd --gid "$docker_gid" "$docker_group" +fi +usermod -aG "$docker_group" manuagent + +exec gosu manuagent "$@" diff --git a/web-ui/.dockerignore b/web-ui/.dockerignore new file mode 100644 index 0000000..bbaa109 --- /dev/null +++ b/web-ui/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +client +*.tsbuildinfo +npm-debug.log +.env +.env.* diff --git a/web-ui/Dockerfile b/web-ui/Dockerfile new file mode 100644 index 0000000..ecf778f --- /dev/null +++ b/web-ui/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +# 第一阶段只负责安装锁定依赖并生成 Vite 静态资源,避免把 Node.js 和源码带入运行镜像。 +FROM node:22-bookworm-slim AS builder + +WORKDIR /workspace + +# 先复制依赖清单以复用 Docker 构建缓存;只有依赖变化时才重新执行 npm ci。 +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# 第二阶段使用精简 Nginx 提供静态页面,并把同源 /api 请求转发给后端服务。 +FROM nginx:1.29.8-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /workspace/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/web-ui/nginx.conf b/web-ui/nginx.conf new file mode 100644 index 0000000..88a4e3c --- /dev/null +++ b/web-ui/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name _; + + # 企业材料允许上传到 160 MB,与 Spring Boot 的请求上限保持一致。 + client_max_body_size 160m; + + root /usr/share/nginx/html; + index index.html; + + # 前后端保持同源,浏览器中的 Session Cookie、CSRF Token 与流式事件接口无需额外跨域配置。 + location /api/ { + proxy_pass http://backend:8080; + 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 $scheme; + + # Agent 事件采用长连接流式返回,关闭代理缓冲后事件才能及时到达前端。 + proxy_buffering off; + proxy_request_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + add_header X-Accel-Buffering no; + } + + # Vue Router 使用 history 模式,未命中的前端路由统一回退到入口页面。 + location / { + try_files $uri $uri/ /index.html; + } +}