diff --git a/deploy-server/nginx.conf b/deploy-server/nginx.conf
index 4ff82b4..c845723 100644
--- a/deploy-server/nginx.conf
+++ b/deploy-server/nginx.conf
@@ -1,41 +1,357 @@
-server {
- listen 80;
- listen [::]:80;
- server_name localhost;
+# =============================================================================
+# 企微IT智能服务台 — Nginx 反向代理配置
+# =============================================================================
+# 部署说明:
+# - 本 nginx 运行在 Docker 容器内,负责统一路由
+# - 数据查询平台运行在**另一台主机**,通过 proxy_pass 转发
+# - 修改 DATAQUERY_HOST 为数据平台实际 IP 地址
+#
+# 路由规则:
+# /itdesk/ → H5 员工端静态文件
+# /itagent/ → 坐席工作台静态文件
+# /itadmin/ → 管理后台静态文件
+# /itportal/ → 统一入口(角色选择)静态文件
+# /itterminal/ → 小鱼终端大屏静态文件
+# /itportal/meetingroom/ → 会议室API(终端+H5共用,代理到后端)
+# /api/ → 后端 FastAPI(容器名 backend:8000)
+# /ws/ → WebSocket(容器名 backend:8000,支持升级)
+# / → IT 数据查询平台(远程主机)
+# =============================================================================
- # 前端静态文件
- location / {
- root /usr/share/nginx/html;
- index index.html index.htm;
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ # ------------------------------------------------------------------
+ # 日志格式
+ # ------------------------------------------------------------------
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+ error_log /var/log/nginx/error.log warn;
+
+ # ------------------------------------------------------------------
+ # 基础配置
+ # ------------------------------------------------------------------
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+ types_hash_max_size 2048;
+ client_max_body_size 50m; # 支持文件上传(企微媒体文件)
+
+ # ------------------------------------------------------------------
+ # Gzip 压缩(前端静态资源)
+ # ------------------------------------------------------------------
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_types text/plain text/css text/xml text/javascript
+ application/javascript application/xml+rss
+ application/json application/ld+json;
+
+ # =================================================================
+ # 上游服务定义(Docker 内部网络)
+ # =================================================================
+ upstream backend_api {
+ server backend:8000;
}
- # API 反向代理到后端
- location /api/ {
- proxy_pass http://wecom_it_backend_green:8000/;
- 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;
+ # =================================================================
+ # HTTPS 服务:监听 443 端口(SSL)
+ # =================================================================
+ server {
+ listen 80;
+ listen 443 ssl;
+ server_name itsupport.servyou.com.cn;
+
+ # SSL 证书配置(使用通配符证书 *.servyou.com.cn)
+ ssl_certificate /etc/nginx/ssl/servyou.com.cn.crt;
+ ssl_certificate_key /etc/nginx/ssl/servyou.com.cn.key;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 1d;
+
+ # ------------------------------------------------------------------
+ # H5 员工端 — /itdesk/
+ # ------------------------------------------------------------------
+ location /itdesk/ {
+ alias /usr/share/nginx/html/itdesk/;
+ index index.html;
+ try_files $uri /itdesk/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 坐席工作台 — /itagent/
+ # ------------------------------------------------------------------
+ location /itagent/ {
+ alias /usr/share/nginx/html/itagent/;
+ index index.html;
+ try_files $uri /itagent/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 管理后台 — /itadmin/
+ # ------------------------------------------------------------------
+ location /itadmin/ {
+ alias /usr/share/nginx/html/itadmin/;
+ index index.html;
+ try_files $uri /itadmin/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 小鱼终端大屏 — /itterminal/
+ # ------------------------------------------------------------------
+ location /itterminal/ {
+ alias /usr/share/nginx/html/itterminal/;
+ index index.html;
+ try_files $uri /itterminal/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 会议室 API — /itportal/meetingroom/
+ # 说明:终端和H5共用的会议室预定/报修/指南API
+ # 必须在 /itportal/ 静态文件之前匹配(nginx最长前缀优先)
+ # ------------------------------------------------------------------
+ location /itportal/meetingroom/ {
+ proxy_pass http://backend_api;
+ 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;
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+ }
+
+ # ------------------------------------------------------------------
+ # 统一入口 Portal — /itportal/
+ # ------------------------------------------------------------------
+ location /itportal/ {
+ alias /usr/share/nginx/html/itportal/;
+ index index.html;
+ try_files $uri /itportal/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 后端 API — /api/
+ # ------------------------------------------------------------------
+ location /api/ {
+ proxy_pass http://backend_api/;
+ 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;
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+ }
+
+ # ------------------------------------------------------------------
+ # H5 用户端 — /h5/
+ # 说明:H5 是静态前端应用,必须配置为静态文件服务(alias)!
+ # ⚠️ 禁止改为 proxy_pass,否则返回 404(后端无 /h5/ 路由)
+ # 关联 CASE:CASE-20260714-02 / CASE-20260716-01
+ # ------------------------------------------------------------------
+ location /h5/ {
+ alias /usr/share/nginx/html/h5/;
+ index index.html;
+ try_files $uri /h5/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 静态媒体文件 — /media/ (企微下载的图片/H5上传的文件)
+ # 代理到后端 /api/media/ 接口(容器内路径 /app/uploads/)
+ # ------------------------------------------------------------------
+ location /media/ {
+ proxy_pass http://backend_api/media/;
+ 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;
+ expires 30d;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # ------------------------------------------------------------------
+ # WebSocket — /ws/
+ # ------------------------------------------------------------------
+ location /ws/ {
+ proxy_pass http://backend_api;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_read_timeout 86400s;
+ }
+
+ # ------------------------------------------------------------------
+ # IT 数据查询平台 — /(根路径,反代到远程主机)
+ # ------------------------------------------------------------------
+ location / {
+ proxy_pass http://10.80.0.130:8080;
+ 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;
+ proxy_connect_timeout 30s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+ }
}
- # H5 静态文件服务
- location /h5/ {
- alias /usr/share/nginx/html/h5/;
- index index.html;
- try_files $uri $uri/ /h5/index.html;
- }
+ # =================================================================
+ # 备用:监听 80 端口(开发调试用)
+ # =================================================================
+ server {
+ listen 80;
+ server_name localhost;
- # H5 API 反向代理(更具体的路径)
- location /h5/api/ {
- proxy_pass http://wecom_it_backend_green:8000/;
- 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;
- }
+ # ------------------------------------------------------------------
+ # 健康检查端点(用于 Docker healthcheck)
+ # ------------------------------------------------------------------
+ location = /itdesk/health {
+ access_log off;
+ return 200 "healthy\n";
+ add_header Content-Type text/plain;
+ }
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
+ # ------------------------------------------------------------------
+ # H5 员工端 — /itdesk/
+ # ------------------------------------------------------------------
+ # 注意:alias + try_files $uri/ 会导致 301 重定向死循环,
+ # 移除 $uri/ 避免触发 nginx 的目录重定向行为
+ location /itdesk/ {
+ alias /usr/share/nginx/html/itdesk/;
+ index index.html;
+ try_files $uri /itdesk/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 坐席工作台 — /itagent/
+ # ------------------------------------------------------------------
+ location /itagent/ {
+ alias /usr/share/nginx/html/itagent/;
+ index index.html;
+ try_files $uri /itagent/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 管理后台 — /itadmin/
+ # ------------------------------------------------------------------
+ location /itadmin/ {
+ alias /usr/share/nginx/html/itadmin/;
+ index index.html;
+ try_files $uri /itadmin/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 小鱼终端大屏 — /itterminal/
+ # ------------------------------------------------------------------
+ location /itterminal/ {
+ alias /usr/share/nginx/html/itterminal/;
+ index index.html;
+ try_files $uri /itterminal/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 会议室 API — /itportal/meetingroom/
+ # 说明:终端和H5共用的会议室预定/报修/指南API
+ # 必须在 /itportal/ 静态文件之前匹配(nginx最长前缀优先)
+ # ------------------------------------------------------------------
+ location /itportal/meetingroom/ {
+ proxy_pass http://backend_api;
+ 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;
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+ }
+
+ # ------------------------------------------------------------------
+ # 统一入口 Portal — /itportal/
+ # ------------------------------------------------------------------
+ location /itportal/ {
+ alias /usr/share/nginx/html/itportal/;
+ index index.html;
+ try_files $uri /itportal/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # 后端 API — /api/
+ # ------------------------------------------------------------------
+ location /api/ {
+ proxy_pass http://backend_api/;
+ 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;
+
+ # 超时设置(AI 回复可能较慢)
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+ }
+
+ # ------------------------------------------------------------------
+ # H5 用户端 — /h5/
+ # 说明:H5 是静态前端应用,必须配置为静态文件服务(alias)!
+ # ⚠️ 禁止改为 proxy_pass,否则返回 404(后端无 /h5/ 路由)
+ # 关联 CASE:CASE-20260714-02 / CASE-20260716-01
+ # ------------------------------------------------------------------
+ location /h5/ {
+ alias /usr/share/nginx/html/h5/;
+ index index.html;
+ try_files $uri /h5/index.html;
+ }
+
+ # ------------------------------------------------------------------
+ # WebSocket — /ws/(坐席端实时通信)
+ # ------------------------------------------------------------------
+ location /ws/ {
+ proxy_pass http://backend_api;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_read_timeout 86400s; # WebSocket 长连接
+ }
+
+ # ------------------------------------------------------------------
+ # IT 数据查询平台 — /(根路径,反代到远程主机)
+ # ------------------------------------------------------------------
+ # 说明:数据查询平台部署在另一台主机,
+ # 通过 Nginx 反代实现同一域名下访问。
+ # 修改 $dataquery_host 为实际 IP。
+ # ------------------------------------------------------------------
+ location / {
+ # 数据平台远程主机(修改为实际 IP)
+ # 方式1:在 /etc/nginx/nginx.conf 同目录放 env 文件
+ # 方式2:docker-compose.yml 中通过 command 覆盖
+ proxy_pass http://10.80.0.130:8080; # ← 修改为数据平台实际 IP:端口
+ 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;
+
+ # 超时设置
+ proxy_connect_timeout 30s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+ }
}
}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 93096d1..c6cb007 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -87,7 +87,7 @@ services:
# --------------------------------------------------------------------------
backend:
build:
- context: ./backend
+ context: ./src/backend
dockerfile: Dockerfile.dev
image: wecom-it-desk-backend:dev
container_name: dev_wecom_backend
@@ -113,9 +113,9 @@ services:
ports:
- "8000:8000"
volumes:
- - ./backend/app:/app/app
- - ./backend/alembic:/app/alembic
- - ./backend/scripts:/app/scripts
+ - ./src/backend/app:/app/app
+ - ./src/backend/alembic:/app/alembic
+ - ./src/backend/scripts:/app/scripts
command: >
sh -c "uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
depends_on:
@@ -133,7 +133,7 @@ services:
# --------------------------------------------------------------------------
frontend-agent:
build:
- context: ./frontend-agent
+ context: ./src/frontend-agent
dockerfile: Dockerfile.dev
image: wecom-it-desk-agent:dev
container_name: dev_wecom_frontend_agent
@@ -144,7 +144,7 @@ services:
ports:
- "5173:5173"
volumes:
- - ./frontend-agent/src:/app/src
+ - ./src/frontend-agent/src:/app/src
depends_on:
- backend
networks:
@@ -155,7 +155,7 @@ services:
# --------------------------------------------------------------------------
frontend-h5:
build:
- context: ./frontend-h5
+ context: ./src/frontend-h5
dockerfile: Dockerfile.dev
image: wecom-it-desk-h5:dev
container_name: dev_wecom_frontend_h5
@@ -168,14 +168,14 @@ services:
ports:
- "5174:5174"
volumes:
- - ./frontend-h5/src:/app/src
+ - ./src/frontend-h5/src:/app/src
# dev 热更新:同时挂载 public/,否则容器镜像内烘焙的 public/ 不含后续新增资源
# (如 duckula.webp),导致 Vite 解析
失败、整个应用无法挂载
- - ./frontend-h5/public:/app/public
+ - ./src/frontend-h5/public:/app/public
# 挂载 vite.config.ts 使代理目标等配置可热生效(无需重建镜像)
- - ./frontend-h5/vite.config.ts:/app/vite.config.ts
+ - ./src/frontend-h5/vite.config.ts:/app/vite.config.ts
# 挂载 index.html 使 CSP(含 dev WS 端口)修改可热生效
- - ./frontend-h5/index.html:/app/index.html
+ - ./src/frontend-h5/index.html:/app/index.html
depends_on:
- backend
networks:
@@ -186,7 +186,7 @@ services:
# --------------------------------------------------------------------------
frontend-admin:
build:
- context: ./frontend-admin
+ context: ./src/frontend-admin
dockerfile: Dockerfile.dev
image: wecom-it-desk-admin:dev
container_name: dev_wecom_frontend_admin
@@ -196,7 +196,7 @@ services:
ports:
- "5175:5175"
volumes:
- - ./frontend-admin/src:/app/src
+ - ./src/frontend-admin/src:/app/src
depends_on:
- backend
networks:
diff --git a/docker-compose.yml b/docker-compose.yml
index 8a817b0..6231c40 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -95,7 +95,7 @@ services:
# --------------------------------------------------------------------------
backend:
build:
- context: ./backend
+ context: ./src/backend
dockerfile: Dockerfile
image: wecom-it-desk-backend:latest
container_name: wecom_it_backend
@@ -130,9 +130,10 @@ services:
# Dify 原生直连(优先于 dify2openai 代理,修复 [object Object] bug)
- DIFY_NATIVE_BASE_URL=${DIFY_NATIVE_BASE_URL:-}
- DIFY_NATIVE_API_KEY=${DIFY_NATIVE_API_KEY:-}
- # Dify 双路径超时预算切分(v4.0 P0-6,12+12 < wait_for 30s)
- - DIFY_NATIVE_TIMEOUT=${DIFY_NATIVE_TIMEOUT:-12}
- - DIFY_PROXY_TIMEOUT=${DIFY_PROXY_TIMEOUT:-12}
+ # Dify 双路径超时预算切分(v4.0 P0-6,20+20 < wait_for 30s)
+ # 2026-07-20: 12→20,用户反馈高峰期超时较多
+ - DIFY_NATIVE_TIMEOUT=${DIFY_NATIVE_TIMEOUT:-20}
+ - DIFY_PROXY_TIMEOUT=${DIFY_PROXY_TIMEOUT:-20}
# AI Wingman(留空禁用)
- DIFY_WINGMAN_API_URL=${DIFY_WINGMAN_API_URL:-}
- DIFY_WINGMAN_API_KEY=${DIFY_WINGMAN_API_KEY:-}
@@ -162,8 +163,8 @@ services:
# 安全告警(企微机器人 webhook)
- CONTENT_AUDIT_WEBHOOK=${CONTENT_AUDIT_WEBHOOK}
volumes:
- # 代码热更新(生产环境:宿主机 ./app → 容器 /app/app)
- - ./app:/app/app
+ # 代码热更新(生产环境:宿主机 ./src/backend/app → 容器 /app/app)
+ - ./src/backend/app:/app/app
- backend-uploads:/app/uploads
- ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs
depends_on:
@@ -205,12 +206,12 @@ services:
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- - ./frontend-h5/dist:/usr/share/nginx/html/itdesk:ro
- - ./frontend-h5/dist:/usr/share/nginx/html/h5:ro
- - ./frontend-agent/dist:/usr/share/nginx/html/itagent:ro
- - ./frontend-admin/dist:/usr/share/nginx/html/itadmin:ro
+ - ./src/frontend-h5/dist:/usr/share/nginx/html/itdesk:ro
+ - ./src/frontend-h5/dist:/usr/share/nginx/html/h5:ro
+ - ./src/frontend-agent/dist:/usr/share/nginx/html/itagent:ro
+ - ./src/frontend-admin/dist:/usr/share/nginx/html/itadmin:ro
- ./frontend-portal/dist:/usr/share/nginx/html/itportal:ro
- - ./frontend-terminal/dist:/usr/share/nginx/html/itterminal:ro
+ - ./src/frontend-terminal/dist:/usr/share/nginx/html/itterminal:ro
depends_on:
- backend
networks:
diff --git a/docs/class-diagram.mermaid b/docs/class-diagram.mermaid
index 72743ce..aa9e488 100644
--- a/docs/class-diagram.mermaid
+++ b/docs/class-diagram.mermaid
@@ -1,171 +1,74 @@
classDiagram
- direction TB
-
- %% ===================== 后端 =====================
-
- class SessionQueryService {
- +AsyncSession db
- +__init__(db: AsyncSession)
- +get_conversations(status, agent_id, page, page_size) Tuple~List~Conversation~, int~
- +get_agent_conversations(agent_id, page, page_size) Tuple~List~Conversation~, int~
- +get_conversation(conversation_id, include_messages) Conversation
- +get_employee_history_messages(employee_id, limit, before, current_conversation_id) Tuple~List~Message~, bool, Dict~str,str~~
+ class OptionSelectPayload {
+ +string conversation_id
+ +string option_label
+ +string option_value
+ +string selected_from_message_id
+ +UUID client_msg_id
+ +string question_id
+ +string option_id
+ +validate() bool
}
-
class Message {
- +str id
- +str conversation_id
- +str sender_type
- +str sender_id
- +str sender_name
- +str content
- +str msg_type
- +Optional~str~ reply_to_id
- +Optional~str~ media_url
- +Optional~str~ file_name
- +Optional~int~ file_size
- +Optional~Dict~ extra_data
- +bool ai_suggestion
- +str status
- +bool is_read
+ +string id
+ +string conversation_id
+ +string sender_type
+ +string sender_id
+ +string content
+ +string msg_type
+ +dict extra_data
+datetime created_at
+ +__init__(...)
}
-
- class Conversation {
- +str id
- +str employee_id
- +str employee_name
- +str status
- +str assigned_agent_id
- +datetime last_message_at
- +str last_message_summary
- +datetime created_at
+ class OptionSelectHandler {
+ +__init__(session_factory, ws_manager)
+ +handle(payload, employee_id) Message
+ -acquire_advisory_lock(db, key) void
+ -find_duplicate(db, payload) Message
+ -persist(db, payload, employee_id) Message
+ -to_message_dict(message, masked) dict
}
-
- class HistoryMessageListResponse {
- +List~MessageResponse~ items
- +bool has_more
- +Dict~str,str~ conversation_summaries
+ class SensitiveMask {
+ +mask_sensitive_text(text) string
+ +mask_sensitive_value(value) string
}
-
- class MessageResponse {
- +str id
- +str conversation_id
- +str sender_type
- +str sender_id
- +str sender_name
- +str content
- +str msg_type
- +bool ai_suggestion
- +bool is_read
- +datetime created_at
- +Optional~str~ sender_avatar
+ class DifyFeedbackContext {
+ +string feedback_type
+ +string question_id
+ +string option_id
+ +string option_value
+ +string option_label
+ +to_inputs() dict
}
-
- %% 后端关系
- SessionQueryService ..> Message : 查询
- SessionQueryService ..> Conversation : 查询
- HistoryMessageListResponse *-- MessageResponse : contains
- Message }--|| Conversation : belongs to
-
- %% ===================== 前端 API 层 =====================
-
- class MessageAPI {
- <>
- +getMessages(conversationId, params) Promise~MessageListData~
- +sendMessage(conversationId, content, msgType, options) Promise~Message~
- +pollMessages(conversationId, afterMessageId) Promise~MessageListData~
- +getHistoryMessages(employeeId, params) Promise~HistoryMessageListData~
+ class SelectedOptionsSnapshotBuilder {
+ +__init__(db)
+ +build(conversation_id) list
}
-
- class HistoryMessageListData {
- <>
- +Message[] items
- +bool has_more
- +Record~str,str~ conversation_summaries
+ class ConnectionManager {
+ +dict active_connections
+ +dict employee_connections
+ +__init__()
+ +broadcast(data) void
+ +broadcast_to_employees(ids, data) void
}
-
- MessageAPI ..> HistoryMessageListData : returns
- MessageAPI ..> BackendAPI : HTTP GET
-
- %% ===================== 前端 Store 层 =====================
-
class ConversationStore {
- <>
- %% 现有状态
- +Ref~Conversation[]~ conversations
- +Ref~str~ currentConversationId
- +Ref~Message[]~ messages
- +Ref~bool~ loadingMessages
-
- %% 新增:历史模式状态
- +Ref~bool~ historyMode
- +Ref~Message[]~ historyMessages
- +Ref~bool~ historyLoading
- +Ref~bool~ historyHasMore
- +Ref~Record~str,str~~ historyConversationSummaries
- +Ref~str~ historyCursor
-
- %% 新增:计算属性
- +Computed~Message[]~ displayMessages
- +Computed~bool~ isHistoryReadonly
-
- %% 现有方法
- +fetchConversations() void
- +selectConversation(conversationId) void
- +fetchMessages(conversationId) void
- +sendReply(content, replyToId) void
-
- %% 新增:历史模式方法
- +enableHistoryMode() Promise~void~
- +disableHistoryMode() void
- +loadMoreHistory() Promise~void~
- +resetHistoryState() void
+ +Message[] messages
+ +sendOptionSelect(selection) void
+ +handleNewMessage(data) void
+ +latestSelection(question_id) Message
}
-
- ConversationStore ..> MessageAPI : calls
- ConversationStore ..> Message : manages
-
- %% ===================== 前端组件层 =====================
-
- class UserInfoBar {
- <>
- +Props: conversation, availableAgents, canInviteCollaborator
- +Emits: assign, resolve, toggle-pin, toggle-todo, transfer, invite
- +Emits: toggle-history %% 新增
- +resetForNewConversation() void
- %% 新增:历史开关三态
- +historyToggleClass: Computed~string~
- }
-
- class ChatArea {
- <>
- +conversationStore: ConversationStore
- +messageListRef: Ref~HTMLElement~
- %% 新增:渲染逻辑
- +renderedItems: Computed~Array~
- +handleToggleHistory() void
- +handleScrollUp() void
- %% 修改:displayMessages 替代 messages
- }
-
- class ConversationSeparator {
- <>
- +Props: summary: string
- +Props: isCurrent: boolean
- %% 纯展示组件,无 emit
- }
-
class MessageBubble {
- <>
- +Props: message: Message
- +Emits: reply, scroll-to-message
+ +Message message
+ +isLatestSelection() bool
+ +maskedContent() string
}
- %% 组件关系
- ChatArea ..> ConversationStore : uses
- ChatArea ..> UserInfoBar : contains
- ChatArea ..> ConversationSeparator : renders
- ChatArea ..> MessageBubble : renders
- UserInfoBar --|> ChatArea : child component
- ConversationSeparator --|> ChatArea : child component
+ OptionSelectHandler --> OptionSelectPayload : validates
+ OptionSelectHandler --> Message : creates/returns
+ OptionSelectHandler --> SensitiveMask : masks outbound data
+ OptionSelectHandler --> ConnectionManager : broadcasts
+ OptionSelectHandler --> DifyFeedbackContext : creates after commit
+ SelectedOptionsSnapshotBuilder --> Message : queries latest per question
+ ConversationStore o-- Message : stores
+ MessageBubble --> ConversationStore : derives latest
+ MessageBubble --> Message : renders
diff --git a/docs/sequence-diagram.mermaid b/docs/sequence-diagram.mermaid
index c4b2120..6fdc261 100644
--- a/docs/sequence-diagram.mermaid
+++ b/docs/sequence-diagram.mermaid
@@ -1,81 +1,36 @@
sequenceDiagram
- participant U as 坐席用户
- participant UI as UserInfoBar.vue
- participant CA as ChatArea.vue
- participant Store as ConversationStore
- participant API as message.ts API
- participant BE as Backend API
- participant DB as Database
+ autonumber
+ actor User as 员工
+ participant H5 as H5 ConversationStore
+ participant WS as FastAPI ws.py
+ participant DB as PostgreSQL/messages
+ participant WSM as ws_manager
+ participant Agent as 坐席 Store/MessageBubble
+ participant Task as h5_ai_task.py
+ participant Dify as Dify Workflow
+ participant REST as messages.py
- Note over U,DB: ========== 阶段1:打开历史模式 ==========
-
- U->>UI: 点击"历史会话"开关按钮
- UI->>CA: $emit('toggle-history')
- CA->>Store: enableHistoryMode()
- Store->>Store: historyMode = true, historyLoading = true
- Store->>API: getHistoryMessages(employeeId, {limit:50, current_conversation_id})
- API->>BE: GET /api/employees/{employee_id}/history-messages?limit=50
- BE->>DB: SELECT conversations WHERE employee_id=?
- DB-->>BE: 会话ID列表 [conv1, conv2, conv3...]
- BE->>DB: SELECT messages WHERE conversation_id IN (...) ORDER BY created_at DESC LIMIT 51
- DB-->>BE: 消息列表 (50条 + 1条判断has_more)
- BE->>DB: 对每个会话查首条employee消息, 取前20字
- DB-->>BE: conversation_summaries {conv1:"摘要1", conv2:"摘要2"...}
- BE-->>API: {items:[...], has_more:true, conversation_summaries:{...}}
- API-->>Store: HistoryMessageListData
- Store->>Store: historyMessages = items.reverse() (ASC排序)
- Store->>Store: historyHasMore = has_more
- Store->>Store: historyCursor = historyMessages[0].id
- Store->>Store: historyConversationSummaries = conversation_summaries
- Store->>Store: historyLoading = false
- Store-->>CA: displayMessages 响应更新 (返回historyMessages)
- CA->>CA: 渲染消息列表 + 插入ConversationSeparator
- CA->>CA: 隐藏 ReplyBox + ReplySuggestArea (只读模式)
- CA-->>U: 显示合并历史时间线
-
- Note over U,DB: ========== 阶段2:向上滚动加载更多 ==========
-
- U->>CA: 向上滚动到顶部
- CA->>Store: loadMoreHistory()
- Store->>Store: historyLoading = true
- Store->>API: getHistoryMessages(employeeId, {limit:50, before:historyCursor})
- API->>BE: GET /api/employees/{employee_id}/history-messages?limit=50&before={msgId}
- BE->>DB: 获取before消息的created_at
- BE->>DB: SELECT messages WHERE conv IN (...) AND created_at < before_time ORDER BY DESC LIMIT 51
- DB-->>BE: 更旧的消息列表
- BE->>DB: 对新涉及的会话查首条employee消息摘要
- DB-->>BE: 补充 conversation_summaries
- BE-->>API: {items:[...], has_more:false, conversation_summaries:{...}}
- API-->>Store: HistoryMessageListData
- Store->>Store: historyMessages = newItems.reverse() + historyMessages (前插)
- Store->>Store: historyCursor = historyMessages[0].id (更新游标)
- Store->>Store: historyHasMore = has_more
- Store->>Store: merge conversation_summaries
- Store->>Store: historyLoading = false
- Store-->>CA: displayMessages 更新
- CA-->>U: 显示更多历史消息 + 新分隔条
-
- Note over U,DB: ========== 阶段3:关闭历史模式 ==========
-
- U->>UI: 再次点击"历史会话"开关
- UI->>CA: $emit('toggle-history')
- CA->>Store: disableHistoryMode()
- Store->>Store: historyMode = false
- Store->>Store: 清空 historyMessages, historyCursor, historyConversationSummaries
- Store-->>CA: displayMessages 返回 messages (正常数据源)
- CA->>CA: 恢复 ReplyBox + ReplySuggestArea
- CA-->>U: 显示当前会话正常消息
-
- Note over U,DB: ========== 阶段4:切换会话自动重置 ==========
-
- U->>CA: 点击左栏其他会话
- CA->>Store: selectConversation(newConvId)
- Store->>Store: resetHistoryState() — historyMode=false, 清空历史状态
- Store->>Store: currentConversationId = newConvId
- Store->>API: getMessages(newConvId, {limit:50})
- API->>BE: GET /api/conversations/{newConvId}/messages?limit=50
- BE-->>API: 正常消息列表
- API-->>Store: MessageListData
- Store->>Store: messages = data.items
- Store-->>CA: displayMessages 返回 messages
- CA-->>U: 显示新会话消息 (历史模式已关闭)
+ User->>H5: 点击选项
+ H5->>H5: 首次生成 client_msg_id(UUID)
+ H5->>WS: option_select(6字段)
+ WS->>DB: BEGIN + pg_advisory_xact_lock(key)
+ WS->>DB: 查询同会话同 client_msg_id
+ alt 5秒内重复
+ DB-->>WS: 返回首次 Message
+ WS-->>H5: 不重复落库/广播/Dify
+ else 首次受理
+ WS->>DB: INSERT Message(msg_type=option_select)
+ WS->>DB: COMMIT
+ WS->>WSM: broadcast new_message(masked message)
+ WSM-->>Agent: new_message
+ Agent->>Agent: 追加消息并按 question_id 计算最新
+ WS->>Task: create_task(feedback_context)
+ Task->>Dify: inputs{feedback_type, question_id, option_id...}
+ Dify-->>Task: 下一轮结构化回复
+ end
+ Agent-xWSM: 网络断开
+ Agent->>REST: GET /conversations/{id}/messages
+ REST->>DB: SELECT conversation_id ORDER BY created_at
+ DB-->>REST: 含 option_select 历史
+ REST-->>Agent: masked items
+ Agent->>Agent: 用 created_at + id 恢复最新高亮
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 50c9592..c845723 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -95,6 +95,7 @@ http {
location /itagent/ {
alias /usr/share/nginx/html/itagent/;
index index.html;
+ try_files $uri /itagent/index.html;
}
# ------------------------------------------------------------------
@@ -242,6 +243,7 @@ http {
location /itagent/ {
alias /usr/share/nginx/html/itagent/;
index index.html;
+ try_files $uri /itagent/index.html;
}
# ------------------------------------------------------------------