chore(deploy): 本地部署脚本、容器配置与运行说明
PostgreSQL/pgvector 与 Redis 独立容器,后端、worker、前端可一键启停;不覆盖既有密钥。
This commit is contained in:
1 parent
e0baca15a9
commit
18732801b5
18 files changed
+1796
No files matched your search
+57
@@ -0,0 +1,57 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
netcat-traditional \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Python dependencies. Bump pip's per-request timeout and retry count so
|
||||||
|
# the large opencv/onnxruntime wheel downloads survive slow links and the
|
||||||
|
# heavier amd64/x86_64 wheels don't abort with ReadTimeoutError.
|
||||||
|
ENV PIP_DEFAULT_TIMEOUT=120
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir --retries 10 -r requirements.txt
|
||||||
|
|
||||||
|
# Node.js + npx and uv/uvx for STDIO MCP servers (npx @elastic/mcp-server-…,
|
||||||
|
# uvx mcp-server-…). Copied from the official images instead of apt, which
|
||||||
|
# only ships an EOL Node 18 on bookworm. Kept below the pip layer so an
|
||||||
|
# upstream node/uv tag bump can't invalidate the cached pip layer.
|
||||||
|
COPY --from=node:22-slim /usr/local/bin/node /usr/local/bin/node
|
||||||
|
COPY --from=node:22-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||||
|
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
|
||||||
|
ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /uvx /usr/local/bin/
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY backend/app ./app
|
||||||
|
COPY backend/alembic.ini .
|
||||||
|
COPY backend/alembic ./alembic
|
||||||
|
COPY backend/scripts ./scripts
|
||||||
|
COPY backend/assets ./assets
|
||||||
|
|
||||||
|
# Create required directories including cache directories
|
||||||
|
RUN mkdir -p uploads/agents && \
|
||||||
|
mkdir -p .cache/huggingface/hub && \
|
||||||
|
chmod -R 755 .cache
|
||||||
|
|
||||||
|
# Make startup script executable
|
||||||
|
RUN chmod +x ./scripts/start.sh
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
ENV PORT=8000
|
||||||
|
# Set HuggingFace cache directories
|
||||||
|
ENV HF_HOME=/app/.cache/huggingface
|
||||||
|
ENV HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
ENV HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
|
||||||
|
# Expose the port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the startup script
|
||||||
|
CMD ["./scripts/start.sh"]
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
netcat-traditional \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Python dependencies. Bump pip's per-request timeout and retry count so
|
||||||
|
# the large opencv/onnxruntime wheel downloads survive slow links and the
|
||||||
|
# heavier amd64/x86_64 wheels don't abort with ReadTimeoutError.
|
||||||
|
ENV PIP_DEFAULT_TIMEOUT=120
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir --retries 10 -r requirements.txt
|
||||||
|
RUN crawl4ai-setup
|
||||||
|
|
||||||
|
# Node.js + npx and uv/uvx for STDIO MCP servers (npx @elastic/mcp-server-…,
|
||||||
|
# uvx mcp-server-…). Copied from the official images instead of apt, which
|
||||||
|
# only ships an EOL Node 18 on bookworm. Kept below the pip layer so an
|
||||||
|
# upstream node/uv tag bump can't invalidate the cached pip layer.
|
||||||
|
COPY --from=node:22-slim /usr/local/bin/node /usr/local/bin/node
|
||||||
|
COPY --from=node:22-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||||
|
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
|
||||||
|
ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /uvx /usr/local/bin/
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY backend/app ./app
|
||||||
|
COPY backend/alembic.ini .
|
||||||
|
COPY backend/alembic ./alembic
|
||||||
|
COPY backend/scripts ./scripts
|
||||||
|
COPY backend/assets ./assets
|
||||||
|
|
||||||
|
# Create required directories including cache directories
|
||||||
|
RUN mkdir -p uploads/agents && \
|
||||||
|
mkdir -p temp && \
|
||||||
|
mkdir -p .cache/huggingface/hub && \
|
||||||
|
chmod -R 755 .cache && \
|
||||||
|
chmod -R 755 temp
|
||||||
|
|
||||||
|
# Make startup script executable
|
||||||
|
RUN chmod +x ./scripts/start.sh
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
ENV PORT=8000
|
||||||
|
ENV ENVIRONMENT=production
|
||||||
|
|
||||||
|
# Set HuggingFace cache directories
|
||||||
|
ENV HF_HOME=/app/.cache/huggingface
|
||||||
|
ENV HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
ENV HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
|
||||||
|
# Expose the port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the startup script
|
||||||
|
CMD ["./scripts/start.sh"]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Copy package files first
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Copy frontend source code
|
||||||
|
COPY frontend/ .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Create startup script in /usr/local/bin
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
echo "Waiting for backend..."\n\
|
||||||
|
until curl -f http://backend:8000/health; do\n\
|
||||||
|
echo "Backend not ready - sleeping"\n\
|
||||||
|
sleep 5\n\
|
||||||
|
done\n\
|
||||||
|
\n\
|
||||||
|
echo "Backend is ready - building widget and webclient"\n\
|
||||||
|
cd /app && npm run build:widgetdocker\n\
|
||||||
|
cd /app && npm run build:webclient\n\
|
||||||
|
\n\
|
||||||
|
echo "Starting frontend development server"\n\
|
||||||
|
cd /app && npm run dev -- --host 0.0.0.0 --port 3000' > /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
# Make startup script executable
|
||||||
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the application in development mode
|
||||||
|
CMD ["/bin/bash", "/usr/local/bin/start.sh"]
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:22-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Copy package files first
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
|
||||||
|
# Copy frontend source code
|
||||||
|
COPY frontend/ .
|
||||||
|
|
||||||
|
# Install dependencies with legacy peer deps
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Build the application, widget, and webclient. Use the :prod webclient build so
|
||||||
|
# the shipped loader bakes the production fallback URL (the real backend URL is
|
||||||
|
# resolved at runtime via window.replypierBaseUrl / APP_CONFIG regardless).
|
||||||
|
RUN npm run build-only && \
|
||||||
|
npm run build:widgetdocker && \
|
||||||
|
npm run build:webclient:prod && \
|
||||||
|
if [ ! -f dist/index.html ]; then \
|
||||||
|
echo "<!DOCTYPE html><html><head><meta charset='utf-8'><title>ReplyPier</title><link rel='icon' href='/favicon.ico'></head><body><div id='app'></div><script type='module' src='/src/main.ts'></script></body></html>" > dist/index.html; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Remove default nginx static assets
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
# Copy the built files to nginx
|
||||||
|
COPY --from=builder /app/dist/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Also serve the docker-built widget (widget.js/widget.css). build:widgetdocker
|
||||||
|
# emits these to /app/backend-assets; without this they are discarded and a
|
||||||
|
# frontend-served widget (VITE_WIDGET_URL -> frontend) falls back to the stale
|
||||||
|
# checked-in bundle. Fixed filenames, so this won't clobber hashed dashboard assets.
|
||||||
|
COPY --from=builder /app/backend-assets/ /usr/share/nginx/html/assets/
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Copy entrypoint script
|
||||||
|
COPY frontend/docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/ || exit 1
|
||||||
|
|
||||||
|
# Use entrypoint script
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
FROM postgres:16
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
postgresql-server-dev-16 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install pg_vector extension with compatibility fixes for ARM64
|
||||||
|
RUN git clone https://github.com/pgvector/pgvector.git && \
|
||||||
|
cd pgvector && \
|
||||||
|
git checkout v0.5.1 && \
|
||||||
|
make OPTFLAGS="" && \
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Add initialization scripts
|
||||||
|
COPY ./backend/scripts/init-vector.sql /docker-entrypoint-initdb.d/
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV POSTGRES_DB=replypier
|
||||||
|
ENV POSTGRES_USER=postgres
|
||||||
|
ENV POSTGRES_PASSWORD=postgres
|
||||||
|
|
||||||
|
# Set PostgreSQL configuration for better stability with pgvector
|
||||||
|
RUN echo "shared_preload_libraries = 'vector'" >> /usr/share/postgresql/postgresql.conf.sample
|
||||||
|
RUN echo "max_connections = 200" >> /usr/share/postgresql/postgresql.conf.sample
|
||||||
|
RUN echo "shared_buffers = 256MB" >> /usr/share/postgresql/postgresql.conf.sample
|
||||||
|
|
||||||
|
# Expose PostgreSQL port
|
||||||
|
EXPOSE 5432
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# ReplyPier 本机使用
|
||||||
|
|
||||||
|
## 打开演示页与客服工作台
|
||||||
|
|
||||||
|
- 网站悬浮客服演示:<http://127.0.0.1:3018/local-demo.html>
|
||||||
|
- 管理员 / 人工客服登录:<http://127.0.0.1:3018/>
|
||||||
|
- 会话工作台:<http://127.0.0.1:3018/conversations>
|
||||||
|
|
||||||
|
### 本机账号
|
||||||
|
|
||||||
|
| 身份 | 登录邮箱 | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 管理员 | `admin@example.com` | 管理配置、查看全部会话与评分 |
|
||||||
|
| 客服 A | `support.a@example.com` | 接待、处理自己的会话 |
|
||||||
|
| 客服 B | `support.b@example.com` | 多客服协作与会话隔离 |
|
||||||
|
|
||||||
|
密码保存在 [本机账号文件](deploy/local/.state/test-accounts.json),分别查看 `admin.password`、`agent_a.password`、`agent_b.password`。不要公开或提交该文件。不同客服使用不同浏览器或无痕窗口登录,避免账号互相覆盖。
|
||||||
|
|
||||||
|
### 使用流程
|
||||||
|
|
||||||
|
1. 以人工客服登录工作台,保持页面在线。
|
||||||
|
2. 在另一个浏览器窗口打开演示页,点击右下角悬浮按钮。
|
||||||
|
3. 提问知识库中已有答案的产品或售后问题。
|
||||||
|
4. 发“我要转人工。”,在工作台 Inbox 中找到待人工会话,点击 **Take over chat**。
|
||||||
|
5. 人工回复后,点击 **End Chat → End Chat & Request Rating**,访客可以提交星级和文字评价。
|
||||||
|
6. 管理员在 **Inbox → Closed** 查看对话,在 **Analytics → Agent Performance → Human Agents** 查看评分,在 **Analytics → Customers → View Details** 查看文字评价。
|
||||||
|
|
||||||
|
本机接待时段为每天全天,两名客服属于 `Local Support` 分组。示例知识为虚构政策,正式使用前应替换为实际业务资料。
|
||||||
|
|
||||||
|
## 启动、停止与日志
|
||||||
|
|
||||||
|
Mac 重启后,在终端执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/a11/1020-1030/01-projects/replypier
|
||||||
|
./deploy/local/local.sh up
|
||||||
|
```
|
||||||
|
|
||||||
|
常用命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/local/local.sh status
|
||||||
|
./deploy/local/local.sh logs backend
|
||||||
|
./deploy/local/local.sh logs knowledge
|
||||||
|
./deploy/local/local.sh logs frontend
|
||||||
|
./deploy/local/local.sh restart
|
||||||
|
./deploy/local/local.sh stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`stop` 停止本项目服务及数据库、缓存,不删除数据卷。`restart` 重启应用进程,保留数据库与缓存。未配置系统开机自启动。
|
||||||
|
|
||||||
|
修改前端源码后,执行 `./deploy/local/local.sh build`,再执行 `restart`。构建会根据已有 Widget ID 生成演示页。`setup` 用于安装依赖和构建,不覆盖已有环境配置,也不创建新管理员。
|
||||||
|
|
||||||
|
## 部署结构与数据
|
||||||
|
|
||||||
|
Python 后端、知识库 worker、前端预览服务在宿主机运行;PostgreSQL 与 Redis 在 Colima 虚拟机中运行。
|
||||||
|
|
||||||
|
| 服务 | 本机地址 |
|
||||||
|
| --- | --- |
|
||||||
|
| 工作台 / 演示页 | `127.0.0.1:3018` |
|
||||||
|
| API / Widget / Socket.IO | `127.0.0.1:8018` |
|
||||||
|
| PostgreSQL 16 + pgvector | `127.0.0.1:15432` |
|
||||||
|
| Redis | `127.0.0.1:16380` |
|
||||||
|
|
||||||
|
- Docker context:`colima-bytedesk`。
|
||||||
|
- Compose 项目名:`replypier-local`。
|
||||||
|
- 数据库卷:`replypier-local_postgres_data`。
|
||||||
|
- 配置:`backend/.env`、`frontend/.env`、`deploy/local/.env`。
|
||||||
|
- 运行文件、日志、构建产物、嵌入模型缓存:`deploy/local/.state/`。
|
||||||
|
- Widget 与演示配置:[test-environment.json](deploy/local/.state/test-environment.json)。
|
||||||
|
- 模型配置:DeepSeek / `deepseek-flash`;API Key 由应用加密保存在数据库中。
|
||||||
|
|
||||||
|
启动脚本使用上述 context、项目名和卷。不要随意改名、删除数据卷或关闭承载数据库的 Colima 虚拟机。
|
||||||
|
|
||||||
|
恢复数据时须同时保留数据库和 `backend/.env` 中的加密配置,否则加密保存的模型配置及消息可能无法读取。不要覆盖密钥、重置管理员,或将私有环境文件提交和对外分享。
|
||||||
|
|
||||||
|
## 会话权限检查
|
||||||
|
|
||||||
|
会话读取、接管、转交、加入 Socket 房间和发消息,都需要同时检查组织、角色和会话可见性。相应实现位于 [session_to_agent.py](backend/app/api/session_to_agent.py) 和 [widget_chat.py](backend/app/api/widget_chat.py)。
|
||||||
|
|
||||||
|
按需执行定向回归:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/a11/1020-1030/01-projects/replypier/backend
|
||||||
|
ENV_FILE=/dev/null ENVIRONMENT=test NO_ENTERPRISE=1 .venv/bin/python -m pytest tests/api/test_session_to_agent.py -q -o addopts=''
|
||||||
|
```
|
||||||
|
|
||||||
|
## 网站接入与功能边界
|
||||||
|
|
||||||
|
服务地址仅监听本机。公网接入需要可访问的服务地址、HTTPS、真实网站域名及相应 CORS / Widget 配置,不能将 `127.0.0.1` 作为公网网站的服务地址。
|
||||||
|
|
||||||
|
固定客户长期绑定负责客服的身份映射与自动路由尚未定制。界面主要为英文;邮件邀请、重置密码、Firebase 离线推送和生产容量需在对应任务中配置或验证。
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ReplyPier
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
One-off backfill: encrypt conversation content written before encryption at rest.
|
||||||
|
|
||||||
|
New rows are already encrypted by the application; this walks the rows that predate
|
||||||
|
it. Safe to run against a live database, restartable after a Ctrl-C, and idempotent —
|
||||||
|
already-encrypted rows are skipped, so re-running it costs one scan and changes
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
Run it on the HOST, not inside the backend container:
|
||||||
|
|
||||||
|
DATABASE_URL=postgresql://... ENCRYPTION_KEY=... \
|
||||||
|
python backend/scripts/encrypt_chat_at_rest.py --dry-run
|
||||||
|
|
||||||
|
Importing the `app` package would pull in the FastAPI app and the whole ML stack
|
||||||
|
(and has OOM'd production before), so app/core/encryption.py is loaded directly by
|
||||||
|
file path — the crypto logic itself is never duplicated here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
from psycopg.types.json import Jsonb
|
||||||
|
|
||||||
|
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
DEFAULT_BATCH_SIZE = 500
|
||||||
|
|
||||||
|
TEXT = "text"
|
||||||
|
JSON = "json"
|
||||||
|
|
||||||
|
# What to encrypt, one entry per table so each is scanned once.
|
||||||
|
# (table, primary key, columns, kind) — kind picks the "already encrypted" test and
|
||||||
|
# the value transform, which is all that differs between text and JSONB columns.
|
||||||
|
TARGETS = (
|
||||||
|
("chat_history", "id", ("message",), TEXT),
|
||||||
|
("session_to_agents", "session_id",
|
||||||
|
("transfer_description", "end_chat_description", "ticket_summary",
|
||||||
|
"ticket_description"), TEXT),
|
||||||
|
# The agno agent memory blob. agno's PostgresStorage defaults to the "ai"
|
||||||
|
# schema, and app.agents.encrypted_storage does not override it, so the table
|
||||||
|
# is ai.agent_sessions — schema-qualified here or to_regclass won't find it.
|
||||||
|
("ai.agent_sessions", "session_id", ("memory",), JSON),
|
||||||
|
# Guardrail review excerpts: the snippet of the message that tripped a rule,
|
||||||
|
# encrypted like any other visitor text. Included so the table is covered if
|
||||||
|
# excerpts are ever written before the column type lands on an environment.
|
||||||
|
("guardrail_events", "id", ("excerpt",), TEXT),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module(name: str, path: Path) -> ModuleType:
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def load_encryption() -> ModuleType:
|
||||||
|
"""Load app.core.encryption without importing the `app` package.
|
||||||
|
|
||||||
|
Its two dependencies (logger, config) are light — the weight is in app/__init__,
|
||||||
|
which imports the FastAPI app — so they are registered under their real names
|
||||||
|
and the import inside encryption.py resolves against these.
|
||||||
|
"""
|
||||||
|
for pkg in ("app", "app.core"):
|
||||||
|
if pkg not in sys.modules:
|
||||||
|
stub = ModuleType(pkg)
|
||||||
|
stub.__path__ = []
|
||||||
|
sys.modules[pkg] = stub
|
||||||
|
core = BACKEND_DIR / "app" / "core"
|
||||||
|
_load_module("app.core.logger", core / "logger.py")
|
||||||
|
_load_module("app.core.config", core / "config.py")
|
||||||
|
return _load_module("app.core.encryption", core / "encryption.py")
|
||||||
|
|
||||||
|
|
||||||
|
def _plaintext_predicate(crypto, column, kind):
|
||||||
|
"""SQL fragment + parameter matching a value this run still has to encrypt."""
|
||||||
|
if kind == TEXT:
|
||||||
|
return f"{column} NOT LIKE %s", crypto.ENCRYPTED_PREFIX + crypto.SEPARATOR + "%"
|
||||||
|
# jsonb_exists() rather than the ? operator, which reads as a placeholder.
|
||||||
|
return f"NOT jsonb_exists({column}, %s)", crypto.JSON_MARKER
|
||||||
|
|
||||||
|
|
||||||
|
def _encrypt(crypto, value, kind):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return crypto.encrypt_value(value) if kind == TEXT else Jsonb(crypto.encrypt_json(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _table_exists(conn, table) -> bool:
|
||||||
|
"""agent_sessions is created lazily by agno on the first agent run, so a fresh
|
||||||
|
or agent-less database legitimately lacks it — skip rather than crash."""
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT to_regclass(%s)", (table,))
|
||||||
|
return cur.fetchone()[0] is not None
|
||||||
|
|
||||||
|
|
||||||
|
def backfill(conn, crypto, table, pk, columns, kind, batch_size, dry_run) -> int:
|
||||||
|
"""Encrypt every plaintext value across one table's columns in a single pass.
|
||||||
|
|
||||||
|
Keyset pagination on the primary key with a commit per batch: restartable after
|
||||||
|
an interruption, and never more than batch_size rows in memory.
|
||||||
|
"""
|
||||||
|
predicates, predicate_params = zip(
|
||||||
|
*(_plaintext_predicate(crypto, column, kind) for column in columns))
|
||||||
|
# A row needs work if ANY of its columns is still plaintext.
|
||||||
|
needs_work = " OR ".join(f"({column} IS NOT NULL AND {p})"
|
||||||
|
for column, p in zip(columns, predicates))
|
||||||
|
selected = ", ".join(columns)
|
||||||
|
assignments = ", ".join(f"{column} = %s" for column in columns)
|
||||||
|
|
||||||
|
changed = 0
|
||||||
|
last_pk = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
where, params = f"({needs_work})", list(predicate_params)
|
||||||
|
if last_pk is not None:
|
||||||
|
where += f" AND {pk} > %s"
|
||||||
|
params.append(last_pk)
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT {pk}, {selected} FROM {table} WHERE {where} "
|
||||||
|
f"ORDER BY {pk} LIMIT %s",
|
||||||
|
(*params, batch_size),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
last_pk = rows[-1][0]
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
cur.executemany(
|
||||||
|
f"UPDATE {table} SET {assignments} WHERE {pk} = %s",
|
||||||
|
[(*[_encrypt(crypto, value, kind) for value in values], key)
|
||||||
|
for key, *values in rows],
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
changed += len(rows)
|
||||||
|
print(f" {table}: {changed} rows", flush=True)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--dry-run", action="store_true",
|
||||||
|
help="report how many rows would be encrypted, change nothing")
|
||||||
|
parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE,
|
||||||
|
help=f"rows per transaction (default {DEFAULT_BATCH_SIZE})")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
database_url = os.getenv("DATABASE_URL")
|
||||||
|
if not database_url:
|
||||||
|
print("DATABASE_URL is not set", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
# The app's URL carries a SQLAlchemy driver suffix (postgresql+psycopg://) that
|
||||||
|
# libpq — and therefore psycopg.connect — does not understand. Strip it.
|
||||||
|
database_url = re.sub(r"^(postgresql|postgres)\+\w+://", r"\1://", database_url)
|
||||||
|
crypto = load_encryption()
|
||||||
|
# Belt and braces: the module raises on a missing key outside development, but a
|
||||||
|
# host that happens to export ENVIRONMENT=development would get a generated key,
|
||||||
|
# and encrypting live data with a throwaway key destroys it.
|
||||||
|
if not os.getenv(crypto.ENCRYPTION_KEY_ENV):
|
||||||
|
print(f"{crypto.ENCRYPTION_KEY_ENV} is not set — refusing to encrypt with a "
|
||||||
|
"generated key", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
with psycopg.connect(database_url) as conn:
|
||||||
|
for table, pk, columns, kind in TARGETS:
|
||||||
|
if not _table_exists(conn, table):
|
||||||
|
print(f" {table}: table not present, skipping", flush=True)
|
||||||
|
continue
|
||||||
|
total += backfill(conn, crypto, table, pk, columns, kind,
|
||||||
|
args.batch_size, args.dry_run)
|
||||||
|
|
||||||
|
verb = "would encrypt" if args.dry_run else "encrypted"
|
||||||
|
print(f"{verb} {total} rows")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Set environment variable to prevent tokenizer deadlock warnings
|
||||||
|
export TOKENIZERS_PARALLELISM=false
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
while ! nc -z db 5432; do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
echo "PostgreSQL is ready!"
|
||||||
|
|
||||||
|
# Check if Firebase credentials exist, but don't create them if missing
|
||||||
|
if [ ! -z "$FIREBASE_CREDENTIALS" ] && [ ! -f "$FIREBASE_CREDENTIALS" ]; then
|
||||||
|
echo "Warning: Firebase credentials file not found at $FIREBASE_CREDENTIALS. Continuing without Firebase credentials..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Preload embedding models to avoid runtime issues
|
||||||
|
echo "Preloading embedding models..."
|
||||||
|
python scripts/preload_models.py
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Models preloaded successfully!"
|
||||||
|
else
|
||||||
|
echo "Warning: Model preloading failed. Continuing anyway..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
echo "Running database migrations..."
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Use only 2 workers to reduce resource usage and database connection issues
|
||||||
|
WORKERS=1
|
||||||
|
|
||||||
|
# Start the application with Gunicorn
|
||||||
|
echo "Starting FastAPI application with Gunicorn ($WORKERS workers)..."
|
||||||
|
gunicorn app.main:app \
|
||||||
|
--workers $WORKERS \
|
||||||
|
--worker-class uvicorn.workers.UvicornWorker \
|
||||||
|
--bind 0.0.0.0:8000 \
|
||||||
|
--timeout 120 \
|
||||||
|
--keep-alive 5 \
|
||||||
|
--log-level info \
|
||||||
|
--access-logfile - \
|
||||||
|
--error-logfile - \
|
||||||
|
--preload
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: replypier-local
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: pgvector/pgvector:0.8.2-pg16
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: replypier
|
||||||
|
POSTGRES_USER: replypier
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Run ./deploy/local/local.sh init first}
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:15432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
command: ["postgres", "-c", "shared_buffers=64MB", "-c", "max_connections=80"]
|
||||||
|
mem_limit: 384m
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U replypier -d replypier"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7.4
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:16380:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "64mb", "--maxmemory-policy", "allkeys-lru"]
|
||||||
|
mem_limit: 96m
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>ReplyPier 网站客服测试</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; min-height: 100vh; color: #172033; background: #f5f7fb; font: 17px/1.8 system-ui, sans-serif; }
|
||||||
|
main { max-width: 880px; margin: 0 auto; padding: 96px 30px; }
|
||||||
|
.eyebrow { color: #2563eb; font-size: 14px; font-weight: 700; letter-spacing: .08em; }
|
||||||
|
h1 { font-size: clamp(30px, 5vw, 48px); line-height: 1.3; margin: 16px 0 22px; }
|
||||||
|
p { color: #526078; }
|
||||||
|
section { margin-top: 36px; padding: 26px 30px; background: white; border: 1px solid #e2e8f0; border-radius: 16px; }
|
||||||
|
h2 { font-size: 20px; margin: 0 0 14px; }
|
||||||
|
ol { margin: 0; padding-left: 24px; }
|
||||||
|
li { padding: 7px 0; }
|
||||||
|
a { color: #1d4ed8; }
|
||||||
|
.note { font-size: 14px; margin-top: 25px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="eyebrow">REPLYPIER · 本机测试</div>
|
||||||
|
<h1>你的网页,接入 AI 与人工客服。</h1>
|
||||||
|
<p>点击右下角悬浮按钮展开对话。这里连接的是本机部署的 ReplyPier 和你配置的 DeepSeek。</p>
|
||||||
|
<section>
|
||||||
|
<h2>按这个顺序试一遍</h2>
|
||||||
|
<ol>
|
||||||
|
<li>问知识库:<strong>青禾测试套餐的退款期多长,校验码是什么?</strong></li>
|
||||||
|
<li>要求转接:<strong>我要转人工。</strong></li>
|
||||||
|
<li>在客服工作台接管会话,回复后结束接待,再回这里提交评分。</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
<p><a href="/conversations" target="_blank" rel="noopener">打开客服工作台 ↗</a></p>
|
||||||
|
<p class="note">套餐、政策与校验码均为虚构测试数据。此页面仅供本机测试;对话会调用已配置的模型 API。</p>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
window.replypierId = "__WIDGET_ID__";
|
||||||
|
window.replypierBaseUrl = "http://127.0.0.1:8018/api/v1";
|
||||||
|
window.replypierConfig = { position: "bottom-right" };
|
||||||
|
</script>
|
||||||
|
<script src="/webclient/replypier.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Create private configuration for the loopback-only local deployment."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
LOCAL = ROOT / "deploy" / "local"
|
||||||
|
STATE = LOCAL / ".state"
|
||||||
|
|
||||||
|
|
||||||
|
def write_new(filename: Path, text: str) -> None:
|
||||||
|
if filename.exists():
|
||||||
|
return
|
||||||
|
with filename.open("x", encoding="utf-8") as stream:
|
||||||
|
filename.chmod(0o600)
|
||||||
|
stream.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
STATE.mkdir(parents=True, exist_ok=True)
|
||||||
|
STATE.chmod(0o700)
|
||||||
|
compose_env = LOCAL / ".env"
|
||||||
|
write_new(compose_env, f"POSTGRES_PASSWORD={secrets.token_hex(24)}\n")
|
||||||
|
password = compose_env.read_text().strip().split("=", 1)[1]
|
||||||
|
fernet_key = base64.b64encode(base64.urlsafe_b64encode(secrets.token_bytes(32))).decode()
|
||||||
|
settings = {
|
||||||
|
"DATABASE_URL": f"postgresql+psycopg://replypier:{password}@127.0.0.1:15432/replypier",
|
||||||
|
"REDIS_ENABLED": "true",
|
||||||
|
"REDIS_URL": "redis://127.0.0.1:16380/0",
|
||||||
|
"ENVIRONMENT": "production",
|
||||||
|
"JWT_SECRET_KEY": secrets.token_hex(32),
|
||||||
|
"CONVERSATION_SECRET_KEY": secrets.token_hex(32),
|
||||||
|
"ENCRYPTION_KEY": fernet_key,
|
||||||
|
"FRONTEND_URL": "http://127.0.0.1:3018",
|
||||||
|
"BACKEND_URL": "http://127.0.0.1:8018",
|
||||||
|
"VITE_WIDGET_URL": "http://127.0.0.1:8018",
|
||||||
|
"CORS_ORIGINS": json.dumps(["http://127.0.0.1:3018", "http://localhost:3018", "http://127.0.0.1:8018"]),
|
||||||
|
"WIDGET_ASSETS_DIR": str(STATE / "assets"),
|
||||||
|
"FASTEMBED_CACHE_PATH": str(STATE / "fastembed"),
|
||||||
|
"S3_FILE_STORAGE": "false",
|
||||||
|
"AWS_EC2_METADATA_DISABLED": "true",
|
||||||
|
"MCP_STDIO_ENABLED": "false",
|
||||||
|
"AGNO_TELEMETRY": "false",
|
||||||
|
"AGNO_MONITOR": "false",
|
||||||
|
"EMBEDDING_MAX_WORKERS": "1",
|
||||||
|
"KB_MAX_WORKERS": "1",
|
||||||
|
"OMP_NUM_THREADS": "1",
|
||||||
|
"POLL_INTERVAL": "30",
|
||||||
|
"SMTP_SERVER": "127.0.0.1",
|
||||||
|
"SMTP_PORT": "1025",
|
||||||
|
"SMTP_USERNAME": "",
|
||||||
|
"SMTP_PASSWORD": "",
|
||||||
|
}
|
||||||
|
write_new(ROOT / "backend" / ".env", "".join(f"{key}={value}\n" for key, value in settings.items()))
|
||||||
|
write_new(ROOT / "frontend" / ".env", "VITE_API_URL=http://127.0.0.1:8018/api/v1\nVITE_WS_URL=ws://127.0.0.1:8018\nVITE_WIDGET_URL=http://127.0.0.1:8018\n")
|
||||||
|
print("Local configuration is ready; existing files were not overwritten.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+159
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
LOCAL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT="$(cd "${LOCAL}/../.." && pwd)"
|
||||||
|
STATE="${LOCAL}/.state"
|
||||||
|
PYTHON="${ROOT}/backend/.venv/bin/python"
|
||||||
|
export ENV_FILE="${ROOT}/backend/.env"
|
||||||
|
export NO_ENTERPRISE=1
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker --context colima-bytedesk compose --env-file "${LOCAL}/.env" -f "${LOCAL}/compose.yaml" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_process() {
|
||||||
|
local name="$1" directory="$2" pid
|
||||||
|
shift 2
|
||||||
|
if [[ -f "${STATE}/${name}.pid" ]] && kill -0 "$(cat "${STATE}/${name}.pid")" 2>/dev/null; then
|
||||||
|
printf '%s is already running.\n' "${name}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
pid="$("${PYTHON}" - "${STATE}/${name}.log" "${directory}" "$@" <<'PY'
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(sys.argv[1], "a") as log:
|
||||||
|
process = subprocess.Popen(sys.argv[3:], cwd=sys.argv[2], stdin=subprocess.DEVNULL,
|
||||||
|
stdout=log, stderr=log, start_new_session=True)
|
||||||
|
print(process.pid)
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
printf '%s\n' "${pid}" > "${STATE}/${name}.pid"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_process() {
|
||||||
|
local name="$1" pid command
|
||||||
|
if [[ ! -f "${STATE}/${name}.pid" ]]; then return; fi
|
||||||
|
pid="$(cat "${STATE}/${name}.pid")"
|
||||||
|
if kill -0 "${pid}" 2>/dev/null; then
|
||||||
|
command="$(ps -p "${pid}" -o command=)"
|
||||||
|
if [[ "${command}" != *"${ROOT}"* ]]; then
|
||||||
|
printf 'Refusing to stop an unrelated process at PID %s.\n' "${pid}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
kill "${pid}"
|
||||||
|
for attempt in {1..40}; do
|
||||||
|
if ! kill -0 "${pid}" 2>/dev/null; then break; fi
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
rm -f "${STATE}/${name}.pid"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_frontend() {
|
||||||
|
mkdir -p "${STATE}/assets"
|
||||||
|
cd "${ROOT}/frontend"
|
||||||
|
node_modules/.bin/vite build --outDir "${STATE}/dashboard" --emptyOutDir
|
||||||
|
node_modules/.bin/vite build --config vite.widget.config.ts --outDir "${STATE}/assets" --emptyOutDir
|
||||||
|
if [[ ! -f "${STATE}/assets/widget.css" ]]; then
|
||||||
|
cp "${STATE}"/assets/unused/*.css "${STATE}/assets/widget.css"
|
||||||
|
fi
|
||||||
|
node_modules/.bin/esbuild src/webclient/replypier.js --bundle --minify --format=iife \
|
||||||
|
--target=es2015 --loader:.svg=text --define:process.env.NODE_ENV='"production"' \
|
||||||
|
--define:__REPLYPIER_API_URL__='"http://127.0.0.1:8018/api/v1"' \
|
||||||
|
--outfile="${STATE}/dashboard/webclient/replypier.min.js"
|
||||||
|
cat > "${STATE}/dashboard/config.js" <<'JS'
|
||||||
|
self.APP_CONFIG = {
|
||||||
|
API_URL: "http://127.0.0.1:8018/api/v1",
|
||||||
|
WS_URL: "ws://127.0.0.1:8018",
|
||||||
|
WIDGET_URL: "http://127.0.0.1:8018",
|
||||||
|
NODE_ENV: "production"
|
||||||
|
};
|
||||||
|
JS
|
||||||
|
if [[ -f "${STATE}/test-environment.json" ]]; then
|
||||||
|
"${PYTHON}" - "${LOCAL}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
local = Path(sys.argv[1])
|
||||||
|
state = local / ".state"
|
||||||
|
environment = json.loads((state / "test-environment.json").read_text())
|
||||||
|
template = (local / "demo.template.html").read_text()
|
||||||
|
(state / "dashboard/local-demo.html").write_text(template.replace("__WIDGET_ID__", environment["widget_id"]))
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
init)
|
||||||
|
python3 "${LOCAL}/init_env.py"
|
||||||
|
;;
|
||||||
|
setup)
|
||||||
|
python3 "${LOCAL}/init_env.py"
|
||||||
|
if [[ ! -x "${PYTHON}" ]]; then
|
||||||
|
uv venv --python 3.12 "${ROOT}/backend/.venv"
|
||||||
|
fi
|
||||||
|
uv pip install --python "${PYTHON}" -r "${LOCAL}/requirements.lock.txt"
|
||||||
|
(cd "${ROOT}/frontend" && npm ci --no-audit --no-fund)
|
||||||
|
build_frontend
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
build_frontend
|
||||||
|
;;
|
||||||
|
up)
|
||||||
|
if [[ ! -f "${STATE}/dashboard/index.html" ]]; then
|
||||||
|
printf 'Run %s setup first.\n' "$0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! colima --profile bytedesk status >/dev/null 2>&1; then
|
||||||
|
colima --profile bytedesk start --activate=false --ssh-config=false
|
||||||
|
fi
|
||||||
|
compose up -d --wait --wait-timeout 180
|
||||||
|
compose exec -T db psql -U replypier -d replypier -c 'CREATE EXTENSION IF NOT EXISTS vector;'
|
||||||
|
(cd "${ROOT}/backend" && "${PYTHON}" -m alembic upgrade head)
|
||||||
|
start_process backend "${ROOT}/backend" "${PYTHON}" -m uvicorn app.main:app --host 127.0.0.1 --port 8018
|
||||||
|
start_process knowledge "${ROOT}/backend" "${PYTHON}" -m app.workers.run_knowledge_processor
|
||||||
|
start_process frontend "${ROOT}/frontend" "$(command -v node)" "${ROOT}/frontend/node_modules/vite/bin/vite.js" preview --outDir "${STATE}/dashboard" --host 127.0.0.1 --port 3018 --strictPort
|
||||||
|
for attempt in {1..90}; do
|
||||||
|
if curl --silent --fail http://127.0.0.1:8018/health >/dev/null && curl --silent --fail http://127.0.0.1:3018/ >/dev/null; then
|
||||||
|
printf '\nReplyPier: http://127.0.0.1:3018/\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
printf 'Startup did not become healthy; inspect %s/*.log\n' "${STATE}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_process frontend
|
||||||
|
stop_process knowledge
|
||||||
|
stop_process backend
|
||||||
|
compose stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
stop_process frontend
|
||||||
|
stop_process knowledge
|
||||||
|
stop_process backend
|
||||||
|
exec "$0" up
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
compose ps
|
||||||
|
for name in backend knowledge frontend; do
|
||||||
|
if [[ -f "${STATE}/${name}.pid" ]]; then
|
||||||
|
ps -p "$(cat "${STATE}/${name}.pid")" -o pid=,etime=,command= || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
curl --silent --show-error --fail http://127.0.0.1:8018/health
|
||||||
|
printf '\n'
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
tail -n 70 "${STATE}/${2:-backend}.log"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'Usage: %s {init|setup|build|up|stop|restart|status|logs [backend|knowledge|frontend]}\n' "$0" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
agno==1.7.6
|
||||||
|
aiofiles==25.1.0
|
||||||
|
aiohappyeyeballs==2.7.1
|
||||||
|
aiohttp==3.14.5
|
||||||
|
aiosignal==1.4.0
|
||||||
|
aiosqlite==0.22.1
|
||||||
|
alembic==1.20.0
|
||||||
|
alphashape==1.3.1
|
||||||
|
annotated-doc==0.0.5
|
||||||
|
annotated-types==0.8.0
|
||||||
|
anthropic==1.17.0
|
||||||
|
anyio==4.15.1
|
||||||
|
asyncpg==0.32.0
|
||||||
|
attrs==26.1.0
|
||||||
|
bcrypt==3.2.2
|
||||||
|
beautifulsoup4==4.15.0
|
||||||
|
bidict==0.24.1
|
||||||
|
boto3==1.43.116
|
||||||
|
botocore==1.43.116
|
||||||
|
brotli==1.2.0
|
||||||
|
bs4==0.0.2
|
||||||
|
cachecontrol==0.14.4
|
||||||
|
certifi==2026.10.19
|
||||||
|
cffi==2.1.1
|
||||||
|
chardet==7.7.0
|
||||||
|
charset-normalizer==3.5.2
|
||||||
|
click==8.5.0
|
||||||
|
click-log==0.4.0
|
||||||
|
cloudpickle==3.1.2
|
||||||
|
coloredlogs==15.0.1
|
||||||
|
coverage==7.4.1
|
||||||
|
crawl4ai==0.9.5
|
||||||
|
cryptography==50.0.1
|
||||||
|
cssselect==1.6.0
|
||||||
|
defusedxml==0.7.1
|
||||||
|
distro==1.9.0
|
||||||
|
dnspython==2.9.0
|
||||||
|
docstring-parser==0.18.0
|
||||||
|
duckduckgo-search==8.1.1
|
||||||
|
ecdsa==0.19.2
|
||||||
|
email-validator==2.3.0
|
||||||
|
eval-type-backport==0.4.0
|
||||||
|
fake-useragent==2.2.0
|
||||||
|
fastapi==0.137.2
|
||||||
|
fastembed==0.7.3
|
||||||
|
fastuuid==0.14.0
|
||||||
|
filelock==4.2.4
|
||||||
|
firebase-admin==6.9.0
|
||||||
|
flatbuffers==25.12.19
|
||||||
|
frozenlist==1.8.0
|
||||||
|
fsspec==2026.9.0
|
||||||
|
gitdb==4.0.12
|
||||||
|
gitpython==3.2.1
|
||||||
|
google-ai-generativelanguage==0.6.15
|
||||||
|
google-api-core==2.33.0
|
||||||
|
google-api-python-client==2.201.0
|
||||||
|
google-auth==2.62.0
|
||||||
|
google-auth-httplib2==0.4.4
|
||||||
|
google-cloud-aiplatform==1.91.0
|
||||||
|
google-cloud-bigquery==3.46.2
|
||||||
|
google-cloud-core==2.8.0
|
||||||
|
google-cloud-firestore==2.27.0
|
||||||
|
google-cloud-resource-manager==1.18.0
|
||||||
|
google-cloud-storage==2.19.0
|
||||||
|
google-crc32c==1.9.0
|
||||||
|
google-generativeai==0.8.6
|
||||||
|
google-resumable-media==2.11.0
|
||||||
|
googleapis-common-protos==1.75.0
|
||||||
|
greenlet==3.5.6
|
||||||
|
groq==1.7.0
|
||||||
|
grpc-google-iam-v1==0.14.4
|
||||||
|
grpcio==1.84.0
|
||||||
|
grpcio-status==1.71.2
|
||||||
|
gunicorn==26.2.0
|
||||||
|
h11==0.16.0
|
||||||
|
h2==4.4.1
|
||||||
|
hf-xet==1.7.0
|
||||||
|
hpack==4.2.0
|
||||||
|
httpcore==1.0.9
|
||||||
|
httpcore2==2.16.0
|
||||||
|
httplib2==0.32.0
|
||||||
|
httpx==0.28.1
|
||||||
|
httpx-sse==0.4.3
|
||||||
|
httpx2==2.16.0
|
||||||
|
huggingface-hub==0.36.2
|
||||||
|
humanfriendly==10.0
|
||||||
|
humanize==4.16.0
|
||||||
|
hyperframe==6.1.0
|
||||||
|
idna==3.20
|
||||||
|
importlib-metadata==9.0.1
|
||||||
|
iniconfig==2.3.1
|
||||||
|
invoke==2.2.1
|
||||||
|
jinja2==3.1.6
|
||||||
|
jiter==0.17.0
|
||||||
|
jmespath==1.1.0
|
||||||
|
joblib==1.6.0
|
||||||
|
jsonschema==4.26.0
|
||||||
|
jsonschema-specifications==2025.9.1
|
||||||
|
lark==1.3.1
|
||||||
|
loguru==0.7.3
|
||||||
|
lxml==6.1.3
|
||||||
|
mako==1.4.3
|
||||||
|
markdown==3.11.2
|
||||||
|
markdown-it-py==4.2.0
|
||||||
|
markdownify==1.2.3
|
||||||
|
markupsafe==3.0.4
|
||||||
|
mcp==1.30.0
|
||||||
|
mdurl==0.1.2
|
||||||
|
mistralai==1.12.4
|
||||||
|
mmh3==5.3.1
|
||||||
|
mpmath==1.3.0
|
||||||
|
msgpack==1.2.3
|
||||||
|
multidict==7.1.0
|
||||||
|
networkx==3.7
|
||||||
|
nh3==0.3.7
|
||||||
|
nltk==3.10.3
|
||||||
|
numpy==2.2.6
|
||||||
|
ollama==0.6.3
|
||||||
|
onnxruntime==1.23.0
|
||||||
|
openai==3.35.1
|
||||||
|
opencv-python==4.12.0.88
|
||||||
|
opentelemetry-api==1.45.1
|
||||||
|
opentelemetry-exporter-http-transport==0.66b1
|
||||||
|
opentelemetry-exporter-otlp-common==0.66b1
|
||||||
|
opentelemetry-exporter-otlp-proto-common==1.45.1
|
||||||
|
opentelemetry-exporter-otlp-proto-http==1.45.1
|
||||||
|
opentelemetry-proto==1.45.1
|
||||||
|
opentelemetry-sdk==1.45.1
|
||||||
|
opentelemetry-semantic-conventions==0.66b1
|
||||||
|
packaging==26.3
|
||||||
|
paramiko==3.5.1
|
||||||
|
passlib==1.7.4
|
||||||
|
patchright==1.64.1
|
||||||
|
pgvector==0.5.1
|
||||||
|
pillow==11.3.0
|
||||||
|
playwright==1.64.0
|
||||||
|
playwright-stealth==2.0.3
|
||||||
|
pluggy==1.7.0
|
||||||
|
primp==2.0.1
|
||||||
|
propcache==0.5.4
|
||||||
|
proto-plus==1.28.2
|
||||||
|
protobuf==5.29.6
|
||||||
|
psutil==7.2.2
|
||||||
|
psycopg==3.3.6
|
||||||
|
psycopg-binary==3.3.6
|
||||||
|
py-rust-stemmers==0.1.8
|
||||||
|
pyactiveresource==2.2.2
|
||||||
|
pyasn1==0.6.4
|
||||||
|
pyasn1-modules==0.4.2
|
||||||
|
pyclipper==1.4.0
|
||||||
|
pycparser==3.11
|
||||||
|
pydantic==2.14.1
|
||||||
|
pydantic-core==2.50.1
|
||||||
|
pydantic-settings==2.16.1
|
||||||
|
pyee==14.0.0
|
||||||
|
pygments==2.21.0
|
||||||
|
pyjwt==2.15.1
|
||||||
|
pymysql==1.2.3
|
||||||
|
pynacl==1.6.2
|
||||||
|
pyopenssl==26.4.0
|
||||||
|
pyparsing==3.3.3
|
||||||
|
pypdf==6.21.1
|
||||||
|
pytest==9.1.1
|
||||||
|
pytest-asyncio==1.4.0
|
||||||
|
pytest-cov==4.1.0
|
||||||
|
pytest-mock==3.17.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
python-dotenv==1.2.4
|
||||||
|
python-engineio==4.14.0
|
||||||
|
python-jose==3.5.0
|
||||||
|
python-multipart==0.0.32
|
||||||
|
python-socketio==5.17.0
|
||||||
|
pytz==2026.5
|
||||||
|
pyyaml==6.0.3
|
||||||
|
rank-bm25==0.2.2
|
||||||
|
rapidocr-onnxruntime==1.4.4
|
||||||
|
razorpay==2.0.1
|
||||||
|
redis==8.1.0
|
||||||
|
referencing==0.37.0
|
||||||
|
regex==2026.9.29
|
||||||
|
requests==2.34.2
|
||||||
|
rich==15.0.0
|
||||||
|
rpds-py==2026.9.1
|
||||||
|
rsa==4.9.1
|
||||||
|
rtree==1.4.1
|
||||||
|
s3transfer==0.19.3
|
||||||
|
scipy==1.18.1
|
||||||
|
shapely==2.2.0
|
||||||
|
shellingham==1.5.4
|
||||||
|
shopifyapi==12.7.0
|
||||||
|
simple-websocket==1.1.0
|
||||||
|
six==1.17.0
|
||||||
|
smmap==5.0.3
|
||||||
|
sniffio==1.3.1
|
||||||
|
snowballstemmer==2.2.0
|
||||||
|
soupsieve==3.0.3
|
||||||
|
sqlalchemy==2.1.5
|
||||||
|
sqlglot==30.24.0
|
||||||
|
sse-starlette==3.5.0
|
||||||
|
sshtunnel==0.5.1
|
||||||
|
starlette==1.6.0
|
||||||
|
sympy==1.14.0
|
||||||
|
textblob==0.20.1
|
||||||
|
tiktoken==0.14.0
|
||||||
|
tokenizers==0.23.3
|
||||||
|
tomli==2.5.0
|
||||||
|
tqdm==4.70.1
|
||||||
|
trimesh==5.1.1
|
||||||
|
truststore==0.10.5
|
||||||
|
typer==0.27.3
|
||||||
|
typing-extensions==4.16.0
|
||||||
|
typing-inspection==0.4.4
|
||||||
|
unclecode-litellm==1.81.13
|
||||||
|
uritemplate==4.2.0
|
||||||
|
urllib3==2.8.0
|
||||||
|
uv==0.13.4
|
||||||
|
uvicorn==0.54.0
|
||||||
|
websockets==17.3
|
||||||
|
wsproto==1.3.2
|
||||||
|
xxhash==3.8.2
|
||||||
|
yarl==1.25.1
|
||||||
|
zipp==4.1.1
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Create the local demonstration through the application's real HTTP API."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
LOCAL = Path(__file__).resolve().parent
|
||||||
|
STATE = LOCAL / ".state"
|
||||||
|
API = "http://127.0.0.1:8018/api/v1/"
|
||||||
|
|
||||||
|
|
||||||
|
def checked(response: httpx.Response) -> dict | list:
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
accounts = json.loads((STATE / "test-accounts.json").read_text())
|
||||||
|
with httpx.Client(base_url=API, timeout=600, trust_env=False) as client:
|
||||||
|
admin = accounts["admin"]
|
||||||
|
login = checked(client.post("users/login", data={"username": admin["email"], "password": admin["password"]}))
|
||||||
|
client.headers["Authorization"] = "Bearer " + login["access_token"]
|
||||||
|
org_id = login["user"]["organization_id"]
|
||||||
|
hours = {day: {"start": "00:00", "end": "23:59", "enabled": True} for day in (
|
||||||
|
"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"
|
||||||
|
)}
|
||||||
|
checked(client.patch(f"organizations/{org_id}", json={"business_hours": hours}))
|
||||||
|
roles = checked(client.get("roles"))
|
||||||
|
role_id = next(role["id"] for role in roles if role["name"] == "Agent")
|
||||||
|
users = checked(client.get("users"))
|
||||||
|
staff = []
|
||||||
|
for key in ("agent_a", "agent_b"):
|
||||||
|
account = accounts[key]
|
||||||
|
user = next((item for item in users if item["email"] == account["email"]), None)
|
||||||
|
if user is None:
|
||||||
|
user = checked(client.post("users", json={
|
||||||
|
"email": account["email"], "full_name": account["name"],
|
||||||
|
"password": account["password"], "role_id": role_id,
|
||||||
|
"see_all_ai_chats": False, "see_all_org_chats": False,
|
||||||
|
}))
|
||||||
|
accounts[key]["id"] = user["id"]
|
||||||
|
staff.append(user)
|
||||||
|
|
||||||
|
groups = checked(client.get("groups"))
|
||||||
|
group = next((item for item in groups if item["name"] == "Local Support"), None)
|
||||||
|
if group is None:
|
||||||
|
group = checked(client.post("groups", json={"name": "Local Support"}))
|
||||||
|
members = {user["id"] for user in group.get("users", [])}
|
||||||
|
for user in staff:
|
||||||
|
if user["id"] not in members:
|
||||||
|
checked(client.post(f"groups/{group['id']}/users/{user['id']}"))
|
||||||
|
|
||||||
|
agents = checked(client.get("agent/list"))
|
||||||
|
agent = next((item for item in agents if item["name"] == "网站客服演示"), None)
|
||||||
|
if agent is None:
|
||||||
|
agent = checked(client.post("agent", json={
|
||||||
|
"name": "网站客服演示", "agent_type": "customer_support",
|
||||||
|
"instructions": [
|
||||||
|
"你是本地客服测试助手,全程使用简体中文,回答简短清楚。",
|
||||||
|
"涉及青禾测试套餐、退款期限、校验码时,必须先查询知识库,不得凭常识编造。",
|
||||||
|
"用户明确要求转人工、真人或客服人员时,立即转人工,不要继续追问。",
|
||||||
|
"本环境的政策、套餐和校验码都是虚构测试数据,不代表真实公司的业务。",
|
||||||
|
],
|
||||||
|
"ai_replies_enabled": True, "transfer_to_human": True,
|
||||||
|
"ask_for_rating": True, "handoff_collect_email": False,
|
||||||
|
"handoff_collect_name": False, "ticketing_enabled": False,
|
||||||
|
"allow_attachments": True,
|
||||||
|
}))
|
||||||
|
checked(client.put(f"agent/{agent['id']}/groups", json=[group["id"]]))
|
||||||
|
checked(client.post(f"agent/{agent['id']}/customization", json={
|
||||||
|
"chat_style": "CHATBOT", "collect_email": False,
|
||||||
|
"accent_color": "#2563eb", "welcome_title": "网站智能客服",
|
||||||
|
"welcome_subtitle": "AI 解答 · 人工接待 · 服务评价",
|
||||||
|
"welcome_message": "你好!我是测试客服,可以查询演示知识库,也可以帮你转人工。",
|
||||||
|
"quick_actions": [], "chat_initiation_messages": [], "allow_new_chat": True,
|
||||||
|
}))
|
||||||
|
widgets = checked(client.get(f"widgets/agent/{agent['id']}"))
|
||||||
|
widget = widgets[0] if widgets else checked(client.post("widgets", json={"name": "Local Website", "agent_id": agent["id"]}))
|
||||||
|
environment = {"org_id": org_id, "agent_id": agent["id"], "widget_id": widget["id"],
|
||||||
|
"group_id": group["id"], "staff": [{"id": user["id"], "email": user["email"]} for user in staff],
|
||||||
|
"demo_url": "http://127.0.0.1:3018/local-demo.html"}
|
||||||
|
(STATE / "test-accounts.json").write_text(json.dumps(accounts, ensure_ascii=False, indent=2))
|
||||||
|
(STATE / "test-environment.json").write_text(json.dumps(environment, ensure_ascii=False, indent=2))
|
||||||
|
template = (LOCAL / "demo.template.html").read_text()
|
||||||
|
(STATE / "dashboard/local-demo.html").write_text(template.replace("__WIDGET_ID__", widget["id"]))
|
||||||
|
print("Created two scoped human agents, a support group, and the website widget.", flush=True)
|
||||||
|
|
||||||
|
existing = checked(client.get(f"knowledge/agent/{agent['id']}"))["knowledge"]
|
||||||
|
title = "青禾测试套餐演示规则"
|
||||||
|
if not any(item.get("name") == title or item.get("source") == title for item in existing):
|
||||||
|
print("Indexing the demonstration knowledge source; the first run downloads the embedding model.", flush=True)
|
||||||
|
result = checked(client.post("knowledge/add/text", json={
|
||||||
|
"org_id": org_id, "agent_id": agent["id"], "title": title,
|
||||||
|
"content": "这是一份技术验证专用的虚构知识库,不代表任何真实公司的政策。\n"
|
||||||
|
"青禾测试套餐的退款期限为购买后17天内;客服校验码为QH-7319。\n"
|
||||||
|
"申请退款需要提供演示订单编号,不需要提供银行卡密码。\n"
|
||||||
|
"人工客服可以处理演示订单核对、退款申请和复杂问题;用户要求人工时应立即转接。\n"
|
||||||
|
"服务完成后,访客可以提交1到5星评分和文字评价,管理员可以查看记录。",
|
||||||
|
}))
|
||||||
|
environment["knowledge_id"] = result["knowledge"]["id"]
|
||||||
|
(STATE / "test-environment.json").write_text(json.dumps(environment, ensure_ascii=False, indent=2))
|
||||||
|
print("Local demonstration is ready:", environment["demo_url"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
image: replypier/frontend:latest
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
env_file:
|
||||||
|
- ./frontend/.env
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- replypier-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: replypier/backend:latest
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- REDIS_ENABLED=true
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- WORKERS=1
|
||||||
|
- TIMEOUT=120
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
# HuggingFace caching configuration
|
||||||
|
- HF_HOME=/app/.cache/huggingface
|
||||||
|
- HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
- HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
# Reduce parallelism to avoid conflicts
|
||||||
|
- EMBEDDING_MAX_WORKERS=4
|
||||||
|
- KB_MAX_WORKERS=4
|
||||||
|
# Enable safe embedding mode for Docker
|
||||||
|
- EMBEDDING_SINGLE_THREADED=true
|
||||||
|
- EMBEDDING_SEQUENTIAL_FALLBACK=true
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- replypier-network
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- backend_data:/app/uploads
|
||||||
|
- temp_data:/app/temp
|
||||||
|
- ./config:/app/config
|
||||||
|
# Add caching volumes for models
|
||||||
|
- huggingface_cache:/app/.cache/huggingface
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-X", "GET", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
knowledge_processor:
|
||||||
|
image: replypier/backend:latest
|
||||||
|
command: python -m app.workers.knowledge_processor
|
||||||
|
environment:
|
||||||
|
- REDIS_ENABLED=true
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# HuggingFace caching configuration (shared with backend)
|
||||||
|
- HF_HOME=/app/.cache/huggingface
|
||||||
|
- HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
- HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
# PyTorch configuration (shared with backend)
|
||||||
|
# Reduce parallelism to avoid conflicts
|
||||||
|
- EMBEDDING_MAX_WORKERS=2
|
||||||
|
- KB_MAX_WORKERS=2
|
||||||
|
# Enable safe embedding mode for Docker
|
||||||
|
- EMBEDDING_SINGLE_THREADED=true
|
||||||
|
- EMBEDDING_SEQUENTIAL_FALLBACK=true
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- replypier-network
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- backend_data:/app/uploads
|
||||||
|
- temp_data:/app/temp
|
||||||
|
- ./config:/app/config
|
||||||
|
# Share the same caching volumes with backend
|
||||||
|
- huggingface_cache:/app/.cache/huggingface
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
crm_sync:
|
||||||
|
image: replypier/backend:latest
|
||||||
|
command: python -m app.workers.crm_sync
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- replypier-network
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
ticket_investigator:
|
||||||
|
image: replypier/backend:latest
|
||||||
|
command: python -m app.workers.ticket_investigator
|
||||||
|
environment:
|
||||||
|
- REDIS_ENABLED=true
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# FastEmbed (dedup embeddings) shares the HuggingFace cache
|
||||||
|
- HF_HOME=/app/.cache/huggingface
|
||||||
|
- HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
- HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- replypier-network
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- huggingface_cache:/app/.cache/huggingface
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
db:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_DB=replypier
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- replypier-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- replypier-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 5s
|
||||||
|
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
|
||||||
|
|
||||||
|
networks:
|
||||||
|
replypier-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
backend_data:
|
||||||
|
temp_data:
|
||||||
|
# Add new volumes for model caching
|
||||||
|
huggingface_cache:
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.frontend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file:
|
||||||
|
- ./frontend/.env
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- frontend_node_modules:/app/node_modules
|
||||||
|
- backend_assets:/app/backend-assets
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Development mode
|
||||||
|
- DEBUG=True
|
||||||
|
- ENVIRONMENT=development
|
||||||
|
# HuggingFace caching configuration
|
||||||
|
- HF_HOME=/app/.cache/huggingface
|
||||||
|
- HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
- HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
# Reduce parallelism to avoid conflicts
|
||||||
|
- EMBEDDING_MAX_WORKERS=4
|
||||||
|
- KB_MAX_WORKERS=4
|
||||||
|
# Enable safe embedding mode for Docker
|
||||||
|
- EMBEDDING_SINGLE_THREADED=true
|
||||||
|
- EMBEDDING_SEQUENTIAL_FALLBACK=true
|
||||||
|
volumes:
|
||||||
|
# Hot reload: mount source code over the built code
|
||||||
|
- ./backend/app:/app/app
|
||||||
|
- ./backend/alembic:/app/alembic
|
||||||
|
- ./backend/alembic.ini:/app/alembic.ini
|
||||||
|
- ./backend/requirements.txt:/app/requirements.txt
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./config:/app/config
|
||||||
|
- backend_assets:/app/assets
|
||||||
|
# Add caching volumes for models
|
||||||
|
- huggingface_cache:/app/.cache/huggingface
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-X", "GET", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
# Use watchdog for file change detection and auto-reload
|
||||||
|
command: bash -c "python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
|
||||||
|
# Keep container running
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
|
||||||
|
knowledge_processor:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: python -m app.workers.knowledge_processor
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- DEBUG=True
|
||||||
|
- ENVIRONMENT=development
|
||||||
|
# HuggingFace caching configuration (shared with backend)
|
||||||
|
- HF_HOME=/app/.cache/huggingface
|
||||||
|
- HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
- HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
# PyTorch configuration (shared with backend)
|
||||||
|
# Reduce parallelism to avoid conflicts
|
||||||
|
- EMBEDDING_MAX_WORKERS=2
|
||||||
|
- KB_MAX_WORKERS=2
|
||||||
|
# Enable safe embedding mode for Docker
|
||||||
|
- EMBEDDING_SINGLE_THREADED=true
|
||||||
|
- EMBEDDING_SEQUENTIAL_FALLBACK=true
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./config:/app/config
|
||||||
|
# Share the same caching volumes with backend
|
||||||
|
- huggingface_cache:/app/.cache/huggingface
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
ticket_investigator:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: python -m app.workers.ticket_investigator
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- DEBUG=True
|
||||||
|
- ENVIRONMENT=development
|
||||||
|
# FastEmbed (dedup embeddings) shares the HuggingFace cache
|
||||||
|
- HF_HOME=/app/.cache/huggingface
|
||||||
|
- HF_HUB_CACHE=/app/.cache/huggingface/hub
|
||||||
|
- HF_HUB_DISABLE_TELEMETRY=1
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
- huggingface_cache:/app/.cache/huggingface
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
crm_sync:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: python -m app.workers.crm_sync
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- DEBUG=True
|
||||||
|
- ENVIRONMENT=development
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
db:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=replypier
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 5s
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
frontend_node_modules:
|
||||||
|
backend_assets:
|
||||||
|
# Add new volumes for model caching
|
||||||
|
huggingface_cache:
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Generate config.js with runtime environment variables
|
||||||
|
cat <<EOF > /usr/share/nginx/html/config.js
|
||||||
|
// Runtime configuration generated at container startup.
|
||||||
|
// Assigned to \`self\`, not \`window\`: the page sees it as window.APP_CONFIG
|
||||||
|
// (self === window there), and the service worker can importScripts() this
|
||||||
|
// same file to read the Firebase config, which has no window object.
|
||||||
|
self.APP_CONFIG = {
|
||||||
|
// API URLs
|
||||||
|
API_URL: "${VITE_API_URL:-http://localhost:8000/api/v1}",
|
||||||
|
WS_URL: "${VITE_WS_URL:-ws://localhost:8000}",
|
||||||
|
WIDGET_URL: "${VITE_WIDGET_URL:-http://localhost:8000}",
|
||||||
|
|
||||||
|
// Firebase Configuration
|
||||||
|
FIREBASE_API_KEY: "${VITE_FIREBASE_API_KEY:-}",
|
||||||
|
FIREBASE_AUTH_DOMAIN: "${VITE_FIREBASE_AUTH_DOMAIN:-}",
|
||||||
|
FIREBASE_PROJECT_ID: "${VITE_FIREBASE_PROJECT_ID:-}",
|
||||||
|
FIREBASE_MESSAGING_SENDER_ID: "${VITE_FIREBASE_MESSAGING_SENDER_ID:-}",
|
||||||
|
FIREBASE_APP_ID: "${VITE_FIREBASE_APP_ID:-}",
|
||||||
|
FIREBASE_STORAGE_BUCKET: "${VITE_FIREBASE_STORAGE_BUCKET:-}",
|
||||||
|
FIREBASE_MEASUREMENT_ID: "${VITE_FIREBASE_MEASUREMENT_ID:-}",
|
||||||
|
FIREBASE_VAPID_KEY: "${VITE_FIREBASE_VAPID_KEY:-}",
|
||||||
|
|
||||||
|
// Razorpay Checkout
|
||||||
|
RAZORPAY_KEY_ID: "${VITE_RAZORPAY_KEY_ID:-}",
|
||||||
|
// Google Tag Manager container (enables purchase/checkout analytics)
|
||||||
|
GTM_ID: "${VITE_GTM_ID:-}",
|
||||||
|
|
||||||
|
// Google Fonts API
|
||||||
|
GOOGLE_FONTS_API_KEY: "${VITE_GOOGLE_FONTS_API_KEY:-}",
|
||||||
|
|
||||||
|
// Node Environment
|
||||||
|
NODE_ENV: "${NODE_ENV:-development}",
|
||||||
|
HOST: "${HOST:-0.0.0.0}"
|
||||||
|
};
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
# Enhanced Security headers.
|
||||||
|
# This config only ever runs inside the frontend image, i.e. for self-hosted
|
||||||
|
# deployments - app.replypier.site serves the SPA from its own reverse proxy
|
||||||
|
# and never routes here, so its CSP is set there and is unaffected by this file.
|
||||||
|
# The CSP is repeated in every location block below because an add_header inside
|
||||||
|
# a location cancels the inherited ones - change one, change all five.
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.shopify.com https://connect.facebook.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.shopify.com; font-src 'self' data: https://fonts.gstatic.com https://cdn.shopify.com; img-src 'self' data: blob: http: https:; connect-src 'self' https: http: ws: wss: blob:; frame-src 'self' http://localhost:* https:; worker-src 'self' blob:;" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# PWA control files must never be long-cached: a stale sw.js pins every
|
||||||
|
# installed client to an old build, and config.js is runtime-substituted
|
||||||
|
# per environment. (Exact locations win over the regex cache block below;
|
||||||
|
# add_header in a location cancels inherited headers, so re-add security.)
|
||||||
|
location = /sw.js {
|
||||||
|
add_header Cache-Control "no-cache" always;
|
||||||
|
add_header Service-Worker-Allowed "/" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.shopify.com https://connect.facebook.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.shopify.com; font-src 'self' data: https://fonts.gstatic.com https://cdn.shopify.com; img-src 'self' data: blob: http: https:; connect-src 'self' https: http: ws: wss: blob:; frame-src 'self' http://localhost:* https:; worker-src 'self' blob:;" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /config.js {
|
||||||
|
add_header Cache-Control "no-cache" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.shopify.com https://connect.facebook.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.shopify.com; font-src 'self' data: https://fonts.gstatic.com https://cdn.shopify.com; img-src 'self' data: blob: http: https:; connect-src 'self' https: http: ws: wss: blob:; frame-src 'self' http://localhost:* https:; worker-src 'self' blob:;" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /manifest.webmanifest {
|
||||||
|
default_type application/manifest+json;
|
||||||
|
add_header Cache-Control "no-cache" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.shopify.com https://connect.facebook.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.shopify.com; font-src 'self' data: https://fonts.gstatic.com https://cdn.shopify.com; img-src 'self' data: blob: http: https:; connect-src 'self' https: http: ws: wss: blob:; frame-src 'self' http://localhost:* https:; worker-src 'self' blob:;" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
|
||||||
|
# Ensure security headers are also sent for static assets
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.shopify.com https://connect.facebook.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.shopify.com; font-src 'self' data: https://fonts.gstatic.com https://cdn.shopify.com; img-src 'self' data: blob: http: https:; connect-src 'self' https: http: ws: wss: blob:; frame-src 'self' http://localhost:* https:; worker-src 'self' blob:;" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API proxy
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:8000/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
# Forward the real client IP so per-IP rate limiting keys on the client,
|
||||||
|
# not this proxy container. Overwrites any client-supplied X-Real-IP.
|
||||||
|
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;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user