chore(config): 同步 src/ 路径迁移 + nginx SPA 路由 + 文档图简化

**src/ 路径迁移配套**:
- docker-compose.yml: backend context ./backend → ./src/backend
- docker-compose.yml: bind mount ./app → ./src/backend/app
- docker-compose.dev.yml: 3 个 bind mount 同步调整
- Dify 双路径超时从 12s 提到 20s(2026-07-20 用户反馈高峰期超时)

**nginx SPA 路由修复**(前端路由 /itagent/x → /itagent/index.html):
- nginx/nginx.conf: 加 try_files $uri /itagent/index.html

**生产环境 nginx 反代重写**:
- deploy-server/nginx.conf: 从 33 行 localhost 模板改为 357 行完整反代
  - 新增 /itdesk /itagent /itadmin /itportal /itterminal/itportal/meetingroom
    路由规则
  - 反代到 backend:8000 (API + WS)
  - / 根路径反代到数据查询平台

**架构图简化**(精简但保留关键类/时序):
- docs/class-diagram.mermaid: 215 行 → 59 行
- docs/sequence-diagram.mermaid: 115 行 → 35 行

合计 6 文件 + 470 行 / - 293 行
This commit is contained in:
Simon
2026-08-03 18:46:10 +08:00
parent 969f524962
commit 3a44141eac
6 changed files with 468 additions and 291 deletions
+335 -19
View File
@@ -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;
}
# API 反向代理到后端
location /api/ {
proxy_pass http://wecom_it_backend_green:8000/;
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;
}
# =================================================================
# 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;
}
# H5 静态文件服务
# ------------------------------------------------------------------
# 统一入口 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/ 路由)
# 关联 CASECASE-20260714-02 / CASE-20260716-01
# ------------------------------------------------------------------
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /h5/index.html;
try_files $uri /h5/index.html;
}
# H5 API 反向代理(更具体的路径)
location /h5/api/ {
proxy_pass http://wecom_it_backend_green:8000/;
# ------------------------------------------------------------------
# 静态媒体文件 — /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";
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/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;
}
# ------------------------------------------------------------------
# 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;
}
}
# =================================================================
# 备用:监听 80 端口(开发调试用)
# =================================================================
server {
listen 80;
server_name localhost;
# ------------------------------------------------------------------
# 健康检查端点(用于 Docker healthcheck
# ------------------------------------------------------------------
location = /itdesk/health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# ------------------------------------------------------------------
# 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/ 路由)
# 关联 CASECASE-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 文件
# 方式2docker-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;
}
}
}
+13 -13
View File
@@ -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 解析 <img src="/duckula.webp"> 失败、整个应用无法挂载
- ./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:
+12 -11
View File
@@ -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:
+59 -156
View File
@@ -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 {
<<module: frontend-agent/src/api/message.ts>>
+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 {
<<TypeScript interface>>
+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 {
<<Pinia Store>>
%% 现有状态
+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 {
<<Vue Component>>
+Props: conversation, availableAgents, canInviteCollaborator
+Emits: assign, resolve, toggle-pin, toggle-todo, transfer, invite
+Emits: toggle-history %% 新增
+resetForNewConversation() void
%% 新增:历史开关三态
+historyToggleClass: Computed~string~
}
class ChatArea {
<<Vue Component>>
+conversationStore: ConversationStore
+messageListRef: Ref~HTMLElement~
%% 新增:渲染逻辑
+renderedItems: Computed~Array~
+handleToggleHistory() void
+handleScrollUp() void
%% 修改:displayMessages 替代 messages
}
class ConversationSeparator {
<<Vue Component — 新增>>
+Props: summary: string
+Props: isCurrent: boolean
%% 纯展示组件,无 emit
}
class MessageBubble {
<<Vue Component — 已存在>>
+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
+34 -79
View File
@@ -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 恢复最新高亮
+2
View File
@@ -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;
}
# ------------------------------------------------------------------