classDiagram direction LR %% ====== 数据模型(已存在) ====== class Message { +UUID id +UUID conversation_id +string sender_type +string sender_id +string content +string msg_type +dict extra_data +datetime created_at +string status } class OptionSelectExtraData { +string question_id +string option_id +string option_value +string client_msg_id +string selected_from_message_id +string option_label_masked +string option_label_original } OptionSelectExtraData --o Message : embedded in extra_data JSONB %% ====== 服务类(已存在 + v1.1 新增) ====== class OptionSelectHandler { +__init__(session_factory) +handle(payload, employee_id) Message -acquire_advisory_lock(db, key) -find_duplicate(db, payload) -persist(db, payload, employee_id) } class ConnectionManager { +dict active_connections +dict employee_connections +__init__() +broadcast(data) +broadcast_to_employees(ids, data) } OptionSelectHandler --> ConnectionManager : broadcast + broadcast_to_employees (v1.1) %% ★ 新增 v1.1 class BackendObserver { <> +list events +record_event(metric_name, value, tags) void +get_metrics(name_filter) list +metric_names$ list~string~ } ConnectionManager ..> BackendObserver : triggers latency record OptionSelectHandler ..> BackendObserver : commit_ts record class SensitiveMask { <> +mask_sensitive_text(text) string +mask_sensitive_value(value) string } class H5MessageEndpoint { +h5_get_messages(limit, before) -_mask_outbound(messages) list } H5MessageEndpoint --> SensitiveMask : masks option_select content %% ====== 前端 store / component ====== class ConversationStore { +Message[] messages +bool wsConnected +Ref~string[]~ selectedOptionLabels +Ref~string[]~ selectedOptionIdsFromHistory +sendOptionSelect(value,label,sourceId,qId,oId) void +handleNewMessage(data) void +handleAiReply(data) void +groupedMessagesByQuestion() Message[] +latestSelectionForQuestion(qId) string|null } ConversationStore o-- Message : owns class MessageBubble { +Message msg +bool isLatestInGroup +bool isOptionSelectedComputed +computed data-question-id } MessageBubble --> ConversationStore : reads MessageBubble --> Message : renders class ChatPanel { +computed groupedMessages +render bubbles } ChatPanel --> ConversationStore : groupedMessagesByQuestion ChatPanel --> MessageBubble : iter class UseH5WebSocket { +Ref wsConnected +computed rttMs +onmessage handler } UseH5WebSocket --> BackendObserver : report e2e latency UseH5WebSocket --> ConversationStore : dispatch class MeasureOptionLatencyScript { <> +run() Report -employeeClick() timestamp -agentRender() timestamp -diff() ms } MeasureOptionLatencyScript ..> BackendObserver : queries metrics %% 不变量标注 note for ConversationStore "selectedOptionLabels v1.1 标 deprecated\n仅作日志兼容, UI 判定走 selectedOptionIdsFromHistory" note for BackendObserver "命名约定: {feature}_{aspect}_{metric}_{unit}\n例: option_select_broadcast_latency_ms" note for Message "msg_type 选项 select 类型\nv1.1 不引入新枚举"