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; } # ------------------------------------------------------------------