Files
gochat/docs/requirements/M08-codegraph-context.md
T
rogee 0dabb8cfa5 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 相关修改
2026-07-09 14:53:27 +08:00

501 lines
15 KiB
Markdown

## Code Context
**Query:** notification, notification subscription, webhook, integration hook, installation webhook, action cable listener, push notification, email notification
### Entry Points
- **InstallationWebhookListener** (class) - app/listeners/installation_webhook_listener.rb:1
- **ActionCableListener** (class) - app/listeners/action_cable_listener.rb:1
- **BaseActionCableConnector** (class) - app/javascript/shared/helpers/BaseActionCableConnector.js:6
### Related Symbols
- app/listeners/base_listener.rb: BaseListener:1
- app/listeners/installation_webhook_listener.rb: account_created:2, account:12, users:16, deliver_webhook_payloads:20
- app/dispatchers/async_dispatcher.rb: listeners:11
- app/listeners/action_cable_listener.rb: notification_created:4, notification_updated:10, notification_deleted:16, account_cache_invalidated:32
### Code
#### InstallationWebhookListener (app/listeners/installation_webhook_listener.rb:1)
```ruby
class InstallationWebhookListener < BaseListener
def account_created(event)
payload = account(event).webhook_data.merge(
event: __method__.to_s,
users: users(event)
)
deliver_webhook_payloads(payload)
end
private
def account(event)
event.data[:account]
end
def users(event)
account(event).administrators.map(&:webhook_data)
end
def deliver_webhook_payloads(payload)
# Deliver the installation event
webhook_url = InstallationConfig.find_by(name: 'INSTALLATION_EVENTS_WEBHOOK_URL')&.value
WebhookJob.perform_later(webhook_url, payload) if webhook_url
end
end
```
#### ActionCableListener (app/listeners/action_cable_listener.rb:1)
```ruby
class ActionCableListener < BaseListener
include Events::Types
def notification_created(event)
notification, account, unread_count, count = extract_notification_and_account(event)
tokens = [event.data[:notification].user.pubsub_token]
broadcast(account, tokens, NOTIFICATION_CREATED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
end
def notification_updated(event)
notification, account, unread_count, count = extract_notification_and_account(event)
tokens = [event.data[:notification].user.pubsub_token]
broadcast(account, tokens, NOTIFICATION_UPDATED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
end
def notification_deleted(event)
notification_data = event.data[:notification_data]
user = User.find_by(id: notification_data[:user_id])
account = Account.find_by(id: notification_data[:account_id])
return if user.blank? || account.blank?
notification_finder = NotificationFinder.new(user, account)
tokens = [user.pubsub_token]
broadcast(account, tokens, NOTIFICATION_DELETED, {
notification: { id: notification_data[:id] },
unread_count: notification_finder.unread_count,
count: notification_finder.count
})
end
def account_cache_invalidated(event)
account = event.data[:account]
tokens = user_tokens(account, account.agents)
broadcast(account, tokens, ACCOU
// ... truncated ...
```
#### BaseActionCableConnector (app/javascript/shared/helpers/BaseActionCableConnector.js:6)
```javascript
class BaseActionCableConnector {
static isDisconnected = false;
constructor(
app,
pubsubToken,
websocketHost = '',
presenceInterval = PRESENCE_INTERVAL
) {
const websocketURL = websocketHost ? `${websocketHost}/cable` : undefined;
this.consumer = createConsumer(websocketURL);
this.subscription = this.consumer.subscriptions.create(
{
channel: 'RoomChannel',
pubsub_token: pubsubToken,
account_id: app.$store.getters.getCurrentAccountId,
user_id: app.$store.getters.getCurrentUserID,
},
{
updatePresence() {
this.perform('update_presence');
},
received: this.onReceived,
disconnected: () => {
BaseActionCableConnector.isDisconnected = true;
this.onDisconnected();
this.initReconnectTimer();
},
}
);
this.app = app;
this.events = {};
this.reconnectTimer = null;
this.isAValidEvent = () => true;
this.triggerPresenceInterval = () => {
setTimeout(() => {
this.subscription.updatePresence();
this.triggerPresenceInterval();
}, presenceInterval);
};
this.triggerPresenceInterval();
}
checkConnection() {
const isConnectionActive = this.consumer.connection.isOpen();
const isReconnected =
BaseActionCableConnector.isDisconnected && isConnectionActive;
if (isReconnected) {
this.clearReconnectTimer();
this.onReconnect();
BaseActionCa
// ... truncated ...
```
#### account_created (app/listeners/installation_webhook_listener.rb:2)
```ruby
def account_created(event)
payload = account(event).webhook_data.merge(
event: __method__.to_s,
users: users(event)
)
deliver_webhook_payloads(payload)
end
```
#### account (app/listeners/installation_webhook_listener.rb:12)
```ruby
def account(event)
event.data[:account]
end
```
#### users (app/listeners/installation_webhook_listener.rb:16)
```ruby
def users(event)
account(event).administrators.map(&:webhook_data)
end
```
#### deliver_webhook_payloads (app/listeners/installation_webhook_listener.rb:20)
```ruby
def deliver_webhook_payloads(payload)
# Deliver the installation event
webhook_url = InstallationConfig.find_by(name: 'INSTALLATION_EVENTS_WEBHOOK_URL')&.value
WebhookJob.perform_later(webhook_url, payload) if webhook_url
end
```
#### listeners (app/dispatchers/async_dispatcher.rb:11)
```ruby
def listeners
[
AutomationRuleListener.instance,
CampaignListener.instance,
CsatSurveyListener.instance,
HookListener.instance,
InstallationWebhookListener.instance,
NotificationListener.instance,
ParticipationListener.instance,
Conversations::UnreadCounts::Listener.instance,
ReportingEventListener.instance,
WebhookListener.instance
]
end
```
#### notification_created (app/listeners/action_cable_listener.rb:4)
```ruby
def notification_created(event)
notification, account, unread_count, count = extract_notification_and_account(event)
tokens = [event.data[:notification].user.pubsub_token]
broadcast(account, tokens, NOTIFICATION_CREATED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
end
```
#### notification_updated (app/listeners/action_cable_listener.rb:10)
```ruby
def notification_updated(event)
notification, account, unread_count, count = extract_notification_and_account(event)
tokens = [event.data[:notification].user.pubsub_token]
broadcast(account, tokens, NOTIFICATION_UPDATED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
end
```
#### notification_deleted (app/listeners/action_cable_listener.rb:16)
```ruby
def notification_deleted(event)
notification_data = event.data[:notification_data]
user = User.find_by(id: notification_data[:user_id])
account = Account.find_by(id: notification_data[:account_id])
return if user.blank? || account.blank?
notification_finder = NotificationFinder.new(user, account)
tokens = [user.pubsub_token]
broadcast(account, tokens, NOTIFICATION_DELETED, {
notification: { id: notification_data[:id] },
unread_count: notification_finder.unread_count,
count: notification_finder.count
})
end
```
#### account_cache_invalidated (app/listeners/action_cable_listener.rb:32)
```ruby
def account_cache_invalidated(event)
account = event.data[:account]
tokens = user_tokens(account, account.agents)
broadcast(account, tokens, ACCOUNT_CACHE_INVALIDATED, {
cache_keys: event.data[:cache_keys]
})
end
```
#### message_created (app/listeners/action_cable_listener.rb:41)
```ruby
def message_created(event)
message, account = extract_message_and_account(event)
conversation = message.conversation
tokens = user_tokens(account, conversation.inbox.members) + contact_tokens(conversation.contact_inbox, message)
broadcast(account, tokens, MESSAGE_CREATED, message.push_event_data)
end
```
#### message_updated (app/listeners/action_cable_listener.rb:49)
```ruby
def message_updated(event)
message, account = extract_message_and_account(event)
conversation = message.conversation
tokens = user_tokens(account, conversation.inbox.members) + contact_tokens(conversation.contact_inbox, message)
broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data.merge(previous_changes: event.data[:previous_changes]))
end
```
#### first_reply_created (app/listeners/action_cable_listener.rb:57)
```ruby
def first_reply_created(event)
message, account = extract_message_and_account(event)
conversation = message.conversation
tokens = user_tokens(account, conversation.inbox.members)
broadcast(account, tokens, FIRST_REPLY_CREATED, message.push_event_data)
end
```
#### conversation_created (app/listeners/action_cable_listener.rb:65)
```ruby
def conversation_created(event)
conversation, account = extract_conversation_and_account(event)
tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox)
broadcast(account, tokens, CONVERSATION_CREATED, conversation.push_event_data)
end
```
#### conversation_read (app/listeners/action_cable_listener.rb:72)
```ruby
def conversation_read(event)
conversation, account = extract_conversation_and_account(event)
tokens = user_tokens(account, conversation.inbox.members)
broadcast(account, tokens, CONVERSATION_READ, conversation.push_event_data)
end
```
#### conversation_status_changed (app/listeners/action_cable_listener.rb:79)
```ruby
def conversation_status_changed(event)
conversation, account = extract_conversation_and_account(event)
tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox)
broadcast(account, tokens, CONVERSATION_STATUS_CHANGED, conversation.push_event_data)
end
```
#### conversation_updated (app/listeners/action_cable_listener.rb:86)
```ruby
def conversation_updated(event)
conversation, account = extract_conversation_and_account(event)
tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox)
broadcast(account, tokens, CONVERSATION_UPDATED, conversation.push_event_data)
end
```
#### conversation_unread_count_changed (app/listeners/action_cable_listener.rb:93)
```ruby
def conversation_unread_count_changed(event)
account, inbox_members = ::Conversations::UnreadCounts::BroadcastScope.new(event).perform
return if account.blank? || !account.feature_enabled?('conversation_unread_counts')
tokens = user_tokens(account, inbox_members)
broadcast(account, tokens, CONVERSATION_UNREAD_COUNT_CHANGED, {})
end
```
#### conversation_typing_on (app/listeners/action_cable_listener.rb:102)
```ruby
def conversation_typing_on(event)
conversation = event.data[:conversation]
account = conversation.account
user = event.data[:user]
tokens = typing_event_listener_tokens(account, conversation, user)
broadcast(
account,
tokens,
CONVERSATION_TYPING_ON,
conversation: conversation.push_event_data,
user: user.push_event_data,
is_private: event.data[:is_private] || false
)
end
```
#### conversation_typing_off (app/listeners/action_cable_listener.rb:118)
```ruby
def conversation_typing_off(event)
conversation = event.data[:conversation]
account = conversation.account
user = event.data[:user]
tokens = typing_event_listener_tokens(account, conversation, user)
broadcast(
account,
tokens,
CONVERSATION_TYPING_OFF,
conversation: conversation.push_event_data,
user: user.push_event_data,
is_private: event.data[:is_private] || false
)
end
```
#### assignee_changed (app/listeners/action_cable_listener.rb:134)
```ruby
def assignee_changed(event)
conversation, account = extract_conversation_and_account(event)
tokens = user_tokens(account, conversation.inbox.members)
broadcast(account, tokens, ASSIGNEE_CHANGED, conversation.push_event_data)
end
```
#### team_changed (app/listeners/action_cable_listener.rb:141)
```ruby
def team_changed(event)
conversation, account = extract_conversation_and_account(event)
tokens = user_tokens(account, conversation.inbox.members)
broadcast(account, tokens, TEAM_CHANGED, conversation.push_event_data)
end
```
#### conversation_contact_changed (app/listeners/action_cable_listener.rb:148)
```ruby
def conversation_contact_changed(event)
conversation, account = extract_conversation_and_account(event)
tokens = user_tokens(account, conversation.inbox.members)
broadcast(account, tokens, CONVERSATION_CONTACT_CHANGED, conversation.push_event_data)
end
```
#### contact_created (app/listeners/action_cable_listener.rb:155)
```ruby
def contact_created(event)
contact, account = extract_contact_and_account(event)
broadcast(account, [account_token(account)], CONTACT_CREATED, contact.push_event_data)
end
```
#### contact_updated (app/listeners/action_cable_listener.rb:160)
```ruby
def contact_updated(event)
contact, account = extract_contact_and_account(event)
broadcast(account, [account_token(account)], CONTACT_UPDATED, contact.push_event_data)
end
```
#### listeners (app/dispatchers/sync_dispatcher.rb:7)
```ruby
def listeners
[ActionCableListener.instance, AgentBotListener.instance]
end
```
#### <anonymous> (app/javascript/shared/helpers/BaseActionCableConnector.js:7)
```javascript
static isDisconnected = false;
```
#### constructor (app/javascript/shared/helpers/BaseActionCableConnector.js:9)
```javascript
constructor(
app,
pubsubToken,
websocketHost = '',
presenceInterval = PRESENCE_INTERVAL
) {
const websocketURL = websocketHost ? `${websocketHost}/cable` : undefined;
this.consumer = createConsumer(websocketURL);
this.subscription = this.consumer.subscriptions.create(
{
channel: 'RoomChannel',
pubsub_token: pubsubToken,
account_id: app.$store.getters.getCurrentAccountId,
user_id: app.$store.getters.getCurrentUserID,
},
{
updatePresence() {
this.perform('update_presence');
},
received: this.onReceived,
disconnected: () => {
BaseActionCableConnector.isDisconnected = true;
this.onDisconnected();
this.initReconnectTimer();
},
}
);
this.app = app;
this.events = {};
this.reconnectTimer = null;
this.isAValidEvent = () => true;
this.triggerPresenceInterval = () => {
setTimeout(() => {
this.subscription.updatePresence();
this.triggerPresenceInterval();
}, presenceInterval);
};
this.triggerPresenceInterval();
}
```