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:
@@ -0,0 +1,501 @@
|
||||
## 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();
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user