docs: 整理文档目录结构 — 清理过时文档、归集功能子目录、统一命名规范
清理: - 删除 34 份过时文档(gap reports/QA临时报告/验收报告/阶段性文档) - 删除 docs/.hermes/skills 第三方 skills 副本(16 文件) - 删除 skills-lock.json 目录归集: - 根目录仅保留 README.md 索引 - product/ — 产品与架构设计(PRD + ARCHITECTURE + P2设计文档 + AI/企业路线图) - tracking/ — Chatwoot parity 开发跟踪 - requirements/ — M01-M12 模块需求 - plans/ — 历史实现计划 - parity/ — 路由 parity 与前端契约 - qa/ — QA 报告与测试计划 - ops/ — 运维部署 命名规范: - 全小写 kebab-case,禁止全大写文件名 - product/tracking/ops 用 NN- 序号前缀 - requirements 用 MNN- 两位零填充模块号 - plans/qa 用 YYYY-MM-DD- 日期前缀 - requirements M1-M9 零填充为 M01-M09(修复字典序) 同步更新: - backend/cmd/route_parity/main.go 路径默认值 - backend/scripts/parity_frontend_smoke.sh 报告路径 - 所有 docs 内部交叉引用 - .gitignore 排除编译产物 (backend/gochat, backend/route_parity) - 新增迁移 000052/000053 - 前端 WS 相关修改
This commit is contained in:
@@ -35,6 +35,7 @@ type Client struct {
|
||||
Hub *Hub // reference back to Hub
|
||||
SubscribedRooms map[string]bool // rooms this client is subscribed to
|
||||
CancelPresence context.CancelFunc // cancel presence refresh on disconnect
|
||||
Identifier string // ActionCable subscription identifier (JSON string)
|
||||
}
|
||||
|
||||
// NewClient creates a new WebSocket client with the given identity and connection.
|
||||
@@ -201,12 +202,9 @@ func (h *Hub) Register(c *Client) {
|
||||
logger.L().Infof("ws hub: client registered (id=%s, user_id=%d, account_id=%d, is_contact=%v)",
|
||||
c.ID, c.UserID, c.AccountID, c.IsContact)
|
||||
|
||||
// Send welcome message
|
||||
welcomeMsg, _ := json.Marshal(wspkg.WSMessage{
|
||||
Event: wspkg.EventWelcome,
|
||||
Data: map[string]any{"client_id": c.ID},
|
||||
})
|
||||
c.Send <- welcomeMsg
|
||||
// Note: The ActionCable welcome frame is sent by handler.go's ServeWS
|
||||
// after upgrade. We do NOT send a WSMessage-format welcome here because
|
||||
// the JS ActionCable client only recognizes {type:"welcome"} frames.
|
||||
}
|
||||
|
||||
// Unregister removes a client from the Hub and cleans up presence.
|
||||
@@ -244,6 +242,37 @@ func (h *Hub) Unregister(c *Client) {
|
||||
logger.L().Infof("ws hub: client unregistered (id=%s, user_id=%d)", c.ID, c.UserID)
|
||||
}
|
||||
|
||||
|
||||
// wrapActionCableMessage wraps a raw event payload in the ActionCable wire format.
|
||||
// ActionCable JS expects: {"identifier":"<subscription identifier>","message":<payload>}
|
||||
// Without the identifier field, the JS client crashes with
|
||||
// "Cannot read properties of undefined (reading 'received')".
|
||||
// Without the message field, the received callback gets undefined.
|
||||
//
|
||||
// If the client has no subscription identifier yet (pre-subscribe), the
|
||||
// message is still sent but without an identifier — the JS client will
|
||||
// silently ignore it (no matching subscription).
|
||||
func wrapActionCableMessage(identifier string, data []byte) []byte {
|
||||
if identifier == "" {
|
||||
// Client hasn't subscribed yet — sending an unwrapped message would
|
||||
// crash the ActionCable JS client (no identifier to route to).
|
||||
// Return nil to signal the caller to skip delivery.
|
||||
return nil
|
||||
}
|
||||
wrapped := struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
}{
|
||||
Identifier: identifier,
|
||||
Message: data,
|
||||
}
|
||||
result, err := json.Marshal(wrapped)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SendToAccount sends a message to all clients subscribed to an account room.
|
||||
func (h *Hub) SendToAccount(accountID uint, data []byte) {
|
||||
h.mu.RLock()
|
||||
@@ -253,8 +282,12 @@ func (h *Hub) SendToAccount(accountID uint, data []byte) {
|
||||
if clientIDs, ok := h.rooms[roomName]; ok {
|
||||
for clientID := range clientIDs {
|
||||
if client, ok := h.clients[clientID]; ok {
|
||||
msg := wrapActionCableMessage(client.Identifier, data)
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case client.Send <- data:
|
||||
case client.Send <- msg:
|
||||
default:
|
||||
logger.L().Warnf("ws hub: dropping message for slow client %s", clientID)
|
||||
}
|
||||
@@ -273,8 +306,12 @@ func (h *Hub) SendToAccountConversation(accountID uint, conversationID uint, dat
|
||||
if clientIDs, ok := h.rooms[roomName]; ok {
|
||||
for clientID := range clientIDs {
|
||||
if client, ok := h.clients[clientID]; ok {
|
||||
msg := wrapActionCableMessage(client.Identifier, data)
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case client.Send <- data:
|
||||
case client.Send <- msg:
|
||||
default:
|
||||
logger.L().Warnf("ws hub: dropping message for slow client %s", clientID)
|
||||
}
|
||||
@@ -291,8 +328,12 @@ func (h *Hub) SendToRoom(room string, data []byte) {
|
||||
if clientIDs, ok := h.rooms[room]; ok {
|
||||
for clientID := range clientIDs {
|
||||
if client, ok := h.clients[clientID]; ok {
|
||||
msg := wrapActionCableMessage(client.Identifier, data)
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case client.Send <- data:
|
||||
case client.Send <- msg:
|
||||
default:
|
||||
logger.L().Warnf("ws hub: dropping message for slow client %s", clientID)
|
||||
}
|
||||
@@ -307,8 +348,12 @@ func (h *Hub) SendToClient(clientID string, data []byte) {
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
if client, ok := h.clients[clientID]; ok {
|
||||
msg := wrapActionCableMessage(client.Identifier, data)
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case client.Send <- data:
|
||||
case client.Send <- msg:
|
||||
default:
|
||||
logger.L().Warnf("ws hub: dropping message for slow client %s", clientID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user