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:
2026-07-09 14:53:27 +08:00
parent 805402f938
commit 0dabb8cfa5
136 changed files with 1240 additions and 14484 deletions
+55 -10
View File
@@ -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)
}