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,572 @@
|
||||
## Code Context
|
||||
|
||||
**Query:** conversation, message, conversation participant, attachment, message template, incoming message service, outgoing message service, conversation status, message type
|
||||
|
||||
### Entry Points
|
||||
|
||||
- **Sms::IncomingMessageService** (class) - app/services/sms/incoming_message_service.rb:1
|
||||
- **Line::IncomingMessageService** (class) - app/services/line/incoming_message_service.rb:4
|
||||
- **Twilio::IncomingMessageService** (class) - app/services/twilio/incoming_message_service.rb:1
|
||||
|
||||
### Related Symbols
|
||||
|
||||
- app/services/sms/incoming_message_service.rb: perform:6, account:23, channel:27, phone_number:31, formatted_phone_number:35, set_contact:39, conversation_params:50, set_conversation:59, contact_attributes:72, attach_files:79
|
||||
|
||||
### Code
|
||||
|
||||
#### Sms::IncomingMessageService (app/services/sms/incoming_message_service.rb:1)
|
||||
|
||||
```ruby
|
||||
class Sms::IncomingMessageService
|
||||
include ::FileTypeHelper
|
||||
|
||||
pattr_initialize [:inbox!, :params!]
|
||||
|
||||
def perform
|
||||
set_contact
|
||||
set_conversation
|
||||
@message = @conversation.messages.create!(
|
||||
content: params[:text],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
message_type: :incoming,
|
||||
sender: @contact,
|
||||
source_id: params[:id]
|
||||
)
|
||||
attach_files
|
||||
@message.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account
|
||||
@account ||= @inbox.account
|
||||
end
|
||||
|
||||
def channel
|
||||
@channel ||= @inbox.channel
|
||||
end
|
||||
|
||||
def phone_number
|
||||
params[:from]
|
||||
end
|
||||
|
||||
def formatted_phone_number
|
||||
TelephoneNumber.parse(phone_number).international_number
|
||||
end
|
||||
|
||||
def set_contact
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: params[:from],
|
||||
inbox: @inbox,
|
||||
contact_attributes: contact_attributes
|
||||
).perform
|
||||
|
||||
@contact_inbox = contact_inbox
|
||||
@contact = contact_inbox.contact
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
}
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
|
||||
@conversation = if @inbox.lock_to_single_conversation
|
||||
@contact_inbox.conversations.last
|
||||
else
|
||||
@contact_inbox.conversations.where
|
||||
|
||||
// ... truncated ...
|
||||
```
|
||||
|
||||
#### Line::IncomingMessageService (app/services/line/incoming_message_service.rb:4)
|
||||
|
||||
```ruby
|
||||
class Line::IncomingMessageService
|
||||
include ::FileTypeHelper
|
||||
pattr_initialize [:inbox!, :params!]
|
||||
LINE_STICKER_IMAGE_URL = 'https://stickershop.line-scdn.net/stickershop/v1/sticker/%s/android/sticker.png'.freeze
|
||||
|
||||
def perform
|
||||
# probably test events
|
||||
return if params[:events].blank?
|
||||
|
||||
parse_events
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_events
|
||||
params[:events].each do |event|
|
||||
next unless event_type_message?(event)
|
||||
|
||||
get_line_contact_info(event)
|
||||
next if @line_contact_info['userId'].blank?
|
||||
|
||||
set_contact
|
||||
set_conversation
|
||||
|
||||
next unless message_created? event
|
||||
|
||||
attach_files event['message']
|
||||
@message.save!
|
||||
end
|
||||
end
|
||||
|
||||
def message_created?(event)
|
||||
@message = @conversation.messages.build(
|
||||
content: message_content(event),
|
||||
account_id: @inbox.account_id,
|
||||
content_type: message_content_type(event),
|
||||
inbox_id: @inbox.id,
|
||||
message_type: :incoming,
|
||||
sender: @contact,
|
||||
source_id: event['message']['id'].to_s
|
||||
)
|
||||
@message
|
||||
end
|
||||
|
||||
def message_content(event)
|
||||
message_type = event.dig('message', 'type')
|
||||
case message_type
|
||||
when 'text'
|
||||
event.dig('message', 'text')
|
||||
when 'sticker'
|
||||
sticker_id = event.dig('message', 'stickerId')
|
||||
sticker_image_url(sticker_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Currently, Chatwoot doesn't support stickers. As a temporary solution,
|
||||
# we're displaying stickers as images using the sticker ID in markdown format.
|
||||
# This is subject to change in the future
|
||||
// ... truncated ...
|
||||
```
|
||||
|
||||
#### Twilio::IncomingMessageService (app/services/twilio/incoming_message_service.rb:1)
|
||||
|
||||
```ruby
|
||||
class Twilio::IncomingMessageService
|
||||
include ::FileTypeHelper
|
||||
include ::Twilio::WhatsappIdentifierHelper
|
||||
|
||||
pattr_initialize [:params!]
|
||||
|
||||
def perform
|
||||
return if twilio_channel.blank?
|
||||
|
||||
set_contact
|
||||
set_conversation
|
||||
@message = @conversation.messages.build(
|
||||
content: message_body,
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
message_type: :incoming,
|
||||
sender: @contact,
|
||||
source_id: params[:SmsSid]
|
||||
)
|
||||
attach_files
|
||||
attach_location if location_message?
|
||||
@message.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def twilio_channel
|
||||
@twilio_channel ||= ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) if params[:MessagingServiceSid].present?
|
||||
if params[:AccountSid].present? && params[:To].present?
|
||||
@twilio_channel ||= ::Channel::TwilioSms.find_by(account_sid: params[:AccountSid],
|
||||
phone_number: params[:To])
|
||||
end
|
||||
log_channel_not_found if @twilio_channel.blank?
|
||||
@twilio_channel
|
||||
end
|
||||
|
||||
def log_channel_not_found
|
||||
Rails.logger.warn(
|
||||
'[TWILIO] Incoming message channel lookup failed ' \
|
||||
"account_sid=#{params[:AccountSid]} " \
|
||||
"to=#{params[:To]} " \
|
||||
"messaging_service_sid=#{params[:MessagingServiceSid]} " \
|
||||
"sms_sid=#{params[:SmsSid]}"
|
||||
)
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= twilio_channel.inbox
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= inbox.account
|
||||
end
|
||||
|
||||
# Twilio WhatsApp phone payloads arrive as `whatsap
|
||||
// ... truncated ...
|
||||
```
|
||||
|
||||
#### perform (app/services/sms/incoming_message_service.rb:6)
|
||||
|
||||
```ruby
|
||||
def perform
|
||||
set_contact
|
||||
set_conversation
|
||||
@message = @conversation.messages.create!(
|
||||
content: params[:text],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
message_type: :incoming,
|
||||
sender: @contact,
|
||||
source_id: params[:id]
|
||||
)
|
||||
attach_files
|
||||
@message.save!
|
||||
end
|
||||
```
|
||||
|
||||
#### account (app/services/sms/incoming_message_service.rb:23)
|
||||
|
||||
```ruby
|
||||
def account
|
||||
@account ||= @inbox.account
|
||||
end
|
||||
```
|
||||
|
||||
#### channel (app/services/sms/incoming_message_service.rb:27)
|
||||
|
||||
```ruby
|
||||
def channel
|
||||
@channel ||= @inbox.channel
|
||||
end
|
||||
```
|
||||
|
||||
#### phone_number (app/services/sms/incoming_message_service.rb:31)
|
||||
|
||||
```ruby
|
||||
def phone_number
|
||||
params[:from]
|
||||
end
|
||||
```
|
||||
|
||||
#### formatted_phone_number (app/services/sms/incoming_message_service.rb:35)
|
||||
|
||||
```ruby
|
||||
def formatted_phone_number
|
||||
TelephoneNumber.parse(phone_number).international_number
|
||||
end
|
||||
```
|
||||
|
||||
#### set_contact (app/services/sms/incoming_message_service.rb:39)
|
||||
|
||||
```ruby
|
||||
def set_contact
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: params[:from],
|
||||
inbox: @inbox,
|
||||
contact_attributes: contact_attributes
|
||||
).perform
|
||||
|
||||
@contact_inbox = contact_inbox
|
||||
@contact = contact_inbox.contact
|
||||
end
|
||||
```
|
||||
|
||||
#### conversation_params (app/services/sms/incoming_message_service.rb:50)
|
||||
|
||||
```ruby
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
#### set_conversation (app/services/sms/incoming_message_service.rb:59)
|
||||
|
||||
```ruby
|
||||
def set_conversation
|
||||
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
|
||||
@conversation = if @inbox.lock_to_single_conversation
|
||||
@contact_inbox.conversations.last
|
||||
else
|
||||
@contact_inbox.conversations.where
|
||||
.not(status: :resolved).last
|
||||
end
|
||||
return if @conversation
|
||||
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
end
|
||||
```
|
||||
|
||||
#### contact_attributes (app/services/sms/incoming_message_service.rb:72)
|
||||
|
||||
```ruby
|
||||
def contact_attributes
|
||||
{
|
||||
name: formatted_phone_number,
|
||||
phone_number: phone_number
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
#### attach_files (app/services/sms/incoming_message_service.rb:79)
|
||||
|
||||
```ruby
|
||||
def attach_files
|
||||
return if params[:media].blank?
|
||||
|
||||
params[:media].each do |media_url|
|
||||
# we don't need to process this files since chatwoot doesn't support it
|
||||
next if media_url.end_with?('.smil', '.xml')
|
||||
|
||||
attachment_file = Down.download(
|
||||
media_url,
|
||||
http_basic_authentication: [channel.provider_config['api_key'], channel.provider_config['api_secret']]
|
||||
)
|
||||
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(attachment_file.content_type),
|
||||
file: {
|
||||
io: attachment_file,
|
||||
filename: attachment_file.original_filename,
|
||||
content_type: attachment_file.content_type
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### process_event_params (app/jobs/webhooks/sms_events_job.rb:17)
|
||||
|
||||
```ruby
|
||||
def process_event_params(channel, params)
|
||||
if delivery_event?(params)
|
||||
Sms::DeliveryStatusService.new(channel: channel, params: params[:message].with_indifferent_access).perform
|
||||
else
|
||||
Sms::IncomingMessageService.new(inbox: channel.inbox, params: params[:message].with_indifferent_access).perform
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### perform (app/services/line/incoming_message_service.rb:9)
|
||||
|
||||
```ruby
|
||||
def perform
|
||||
# probably test events
|
||||
return if params[:events].blank?
|
||||
|
||||
parse_events
|
||||
end
|
||||
```
|
||||
|
||||
#### parse_events (app/services/line/incoming_message_service.rb:18)
|
||||
|
||||
```ruby
|
||||
def parse_events
|
||||
params[:events].each do |event|
|
||||
next unless event_type_message?(event)
|
||||
|
||||
get_line_contact_info(event)
|
||||
next if @line_contact_info['userId'].blank?
|
||||
|
||||
set_contact
|
||||
set_conversation
|
||||
|
||||
next unless message_created? event
|
||||
|
||||
attach_files event['message']
|
||||
@message.save!
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### message_created? (app/services/line/incoming_message_service.rb:35)
|
||||
|
||||
```ruby
|
||||
def message_created?(event)
|
||||
@message = @conversation.messages.build(
|
||||
content: message_content(event),
|
||||
account_id: @inbox.account_id,
|
||||
content_type: message_content_type(event),
|
||||
inbox_id: @inbox.id,
|
||||
message_type: :incoming,
|
||||
sender: @contact,
|
||||
source_id: event['message']['id'].to_s
|
||||
)
|
||||
@message
|
||||
end
|
||||
```
|
||||
|
||||
#### message_content (app/services/line/incoming_message_service.rb:48)
|
||||
|
||||
```ruby
|
||||
def message_content(event)
|
||||
message_type = event.dig('message', 'type')
|
||||
case message_type
|
||||
when 'text'
|
||||
event.dig('message', 'text')
|
||||
when 'sticker'
|
||||
sticker_id = event.dig('message', 'stickerId')
|
||||
sticker_image_url(sticker_id)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### sticker_image_url (app/services/line/incoming_message_service.rb:65)
|
||||
|
||||
```ruby
|
||||
def sticker_image_url(sticker_id)
|
||||
""
|
||||
end
|
||||
```
|
||||
|
||||
#### message_content_type (app/services/line/incoming_message_service.rb:69)
|
||||
|
||||
```ruby
|
||||
def message_content_type(event)
|
||||
return 'sticker' if event['message']['type'] == 'sticker'
|
||||
|
||||
'text'
|
||||
end
|
||||
```
|
||||
|
||||
#### attach_files (app/services/line/incoming_message_service.rb:75)
|
||||
|
||||
```ruby
|
||||
def attach_files(message)
|
||||
return unless message_type_non_text?(message['type'])
|
||||
|
||||
response = inbox.channel.client.get_message_content(message['id'])
|
||||
|
||||
extension = get_file_extension(response)
|
||||
file_name = message['fileName'] || "media-#{message['id']}.#{extension}"
|
||||
temp_file = Tempfile.new(file_name)
|
||||
temp_file.binmode
|
||||
temp_file << response.body
|
||||
temp_file.rewind
|
||||
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_content_type(response),
|
||||
file: {
|
||||
io: temp_file,
|
||||
filename: file_name,
|
||||
content_type: response.content_type
|
||||
}
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
#### get_file_extension (app/services/line/incoming_message_service.rb:98)
|
||||
|
||||
```ruby
|
||||
def get_file_extension(response)
|
||||
if response.content_type&.include?('/')
|
||||
response.content_type.split('/')[1]
|
||||
else
|
||||
'bin'
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### event_type_message? (app/services/line/incoming_message_service.rb:106)
|
||||
|
||||
```ruby
|
||||
def event_type_message?(event)
|
||||
event['type'] == 'message' || event['type'] == 'sticker'
|
||||
end
|
||||
```
|
||||
|
||||
#### message_type_non_text? (app/services/line/incoming_message_service.rb:110)
|
||||
|
||||
```ruby
|
||||
def message_type_non_text?(type)
|
||||
[
|
||||
Line::Bot::Event::MessageType::Video,
|
||||
Line::Bot::Event::MessageType::Audio,
|
||||
Line::Bot::Event::MessageType::Image,
|
||||
Line::Bot::Event::MessageType::File
|
||||
].include?(type)
|
||||
end
|
||||
```
|
||||
|
||||
#### account (app/services/line/incoming_message_service.rb:119)
|
||||
|
||||
```ruby
|
||||
def account
|
||||
@account ||= inbox.account
|
||||
end
|
||||
```
|
||||
|
||||
#### get_line_contact_info (app/services/line/incoming_message_service.rb:123)
|
||||
|
||||
```ruby
|
||||
def get_line_contact_info(event)
|
||||
@line_contact_info = JSON.parse(inbox.channel.client.get_profile(event['source']['userId']).body)
|
||||
end
|
||||
```
|
||||
|
||||
#### set_contact (app/services/line/incoming_message_service.rb:127)
|
||||
|
||||
```ruby
|
||||
def set_contact
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: @line_contact_info['userId'],
|
||||
inbox: inbox,
|
||||
contact_attributes: contact_attributes
|
||||
).perform
|
||||
|
||||
@contact_inbox = contact_inbox
|
||||
@contact = contact_inbox.contact
|
||||
end
|
||||
```
|
||||
|
||||
#### conversation_params (app/services/line/incoming_message_service.rb:138)
|
||||
|
||||
```ruby
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
#### set_conversation (app/services/line/incoming_message_service.rb:147)
|
||||
|
||||
```ruby
|
||||
def set_conversation
|
||||
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
|
||||
@conversation = if @inbox.lock_to_single_conversation
|
||||
@contact_inbox.conversations.last
|
||||
else
|
||||
@contact_inbox.conversations.where.not(status: :resolved).last
|
||||
end
|
||||
return if @conversation
|
||||
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
end
|
||||
```
|
||||
|
||||
#### contact_attributes (app/services/line/incoming_message_service.rb:159)
|
||||
|
||||
```ruby
|
||||
def contact_attributes
|
||||
{
|
||||
name: @line_contact_info['displayName'],
|
||||
avatar_url: @line_contact_info['pictureUrl'],
|
||||
additional_attributes: additional_attributes
|
||||
}
|
||||
end
|
||||
```
|
||||
Reference in New Issue
Block a user