Some checks failed
Lock Threads / action (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
## 新增功能
### 1. 新增消息类型
- 添加 search_image (17) 和 product_list (18) content_type
- 支持图片搜索和商品列表消息展示
### 2. 图片上传功能
- 添加 ImageUploadButton 组件,支持图片上传到 Mall API
- 上传后发送 search_image 类型消息,图片 URL 存储在 content_attributes
- 支持图片文件验证(类型、大小)
### 3. Webhook 推送优化
- 修改 webhook_listener.rb,允许 search_image 类型即使 content 为空也推送 webhook
- 解决 search_image 消息不触发 webhook 的问题
### 4. 前端组件
- 新增 SearchImage.vue 组件(widget 和 dashboard)
- 新增 ProductList.vue、Logistics.vue、OrderDetail.vue、OrderList.vue 组件
- 更新 Message.vue 路由逻辑支持新的 content_type
- 更新 UserMessage.vue 支持 search_image 消息显示
### 5. API 层修改
- widget/messages_controller.rb: 允许 content_attributes 参数
- widget/base_controller.rb: 使用前端传入的 content_attributes
- widget/conversation API: 支持 contentAttributes 参数传递
- conversation actions 和 helpers: 完整的 content_attributes 数据流
### 6. Widget 测试页面优化
- 重写 /widget_tests 页面,支持游客/用户模式切换
- 登录流程:使用 reset() + setUser(),不刷新页面
- 退出流程:使用 reset() + 刷新页面
- 添加详细的日志输出和状态显示
## 技术细节
### Message Model
```ruby
enum content_type: {
# ...existing types...
search_image: 17,
product_list: 18
}
```
### Webhook Listener
```ruby
# Allow search_image webhook even if content is blank
return if message.content.blank? && message.content_type != 'search_image'
```
### Widget Upload Flow
```
用户选择图片
→ 上传到 Mall API
→ 获取图片 URL
→ 发送消息: { content: '', content_type: 'search_image', content_attributes: { url: '...' } }
```
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
218 lines
6.5 KiB
Vue
Executable File
218 lines
6.5 KiB
Vue
Executable File
<script>
|
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
|
import ChatCard from 'shared/components/ChatCard.vue';
|
|
import ChatForm from 'shared/components/ChatForm.vue';
|
|
import ChatOptions from 'shared/components/ChatOptions.vue';
|
|
import ChatTable from 'shared/components/ChatTable.vue';
|
|
import ChatArticle from './template/Article.vue';
|
|
import EmailInput from './template/EmailInput.vue';
|
|
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction.vue';
|
|
import IntegrationCard from './template/IntegrationCard.vue';
|
|
import OrderList from 'shared/components/OrderList.vue';
|
|
import OrderDetail from 'shared/components/OrderDetail.vue';
|
|
import Logistics from 'shared/components/Logistics.vue';
|
|
import ProductList from 'shared/components/ProductList.vue';
|
|
|
|
export default {
|
|
name: 'AgentMessageBubble',
|
|
components: {
|
|
ChatArticle,
|
|
ChatCard,
|
|
ChatForm,
|
|
ChatOptions,
|
|
ChatTable,
|
|
EmailInput,
|
|
CustomerSatisfaction,
|
|
IntegrationCard,
|
|
OrderList,
|
|
OrderDetail,
|
|
Logistics,
|
|
ProductList,
|
|
},
|
|
props: {
|
|
message: { type: String, default: null },
|
|
contentType: { type: String, default: null },
|
|
messageType: { type: Number, default: null },
|
|
messageId: { type: Number, default: null },
|
|
messageContentAttributes: {
|
|
type: Object,
|
|
default: () => {},
|
|
},
|
|
},
|
|
setup() {
|
|
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
|
|
useMessageFormatter();
|
|
return {
|
|
formatMessage,
|
|
getPlainText,
|
|
truncateMessage,
|
|
highlightContent,
|
|
};
|
|
},
|
|
computed: {
|
|
isTemplate() {
|
|
return this.messageType === 3;
|
|
},
|
|
isTemplateEmail() {
|
|
return this.contentType === 'input_email';
|
|
},
|
|
isCards() {
|
|
return this.contentType === 'cards';
|
|
},
|
|
isOptions() {
|
|
return this.contentType === 'input_select';
|
|
},
|
|
isForm() {
|
|
return this.contentType === 'form';
|
|
},
|
|
isArticle() {
|
|
return this.contentType === 'article';
|
|
},
|
|
isCSAT() {
|
|
return this.contentType === 'input_csat';
|
|
},
|
|
isIntegrations() {
|
|
return this.contentType === 'integrations';
|
|
},
|
|
isTable() {
|
|
return this.contentType === 'data_table';
|
|
},
|
|
isOrderList() {
|
|
return this.contentType === 'order_list';
|
|
},
|
|
isOrderDetail() {
|
|
return this.contentType === 'order_detail';
|
|
},
|
|
isLogistics() {
|
|
return this.contentType === 'logistics';
|
|
},
|
|
isProductList() {
|
|
return this.contentType === 'product_list';
|
|
},
|
|
},
|
|
methods: {
|
|
onResponse(messageResponse) {
|
|
this.$store.dispatch('message/update', messageResponse);
|
|
},
|
|
onOptionSelect(selectedOption) {
|
|
this.onResponse({
|
|
submittedValues: [selectedOption],
|
|
messageId: this.messageId,
|
|
});
|
|
},
|
|
onFormSubmit(formValues) {
|
|
const formValuesAsArray = Object.keys(formValues).map(key => ({
|
|
name: key,
|
|
value: formValues[key],
|
|
}));
|
|
this.onResponse({
|
|
submittedValues: formValuesAsArray,
|
|
messageId: this.messageId,
|
|
});
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="chat-bubble-wrap">
|
|
<div
|
|
v-if="
|
|
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT && !isTable && !isOrderList && !isOrderDetail && !isLogistics && !isProductList
|
|
"
|
|
class="chat-bubble agent bg-n-background dark:bg-n-solid-3 text-n-slate-12"
|
|
>
|
|
<div
|
|
v-dompurify-html="formatMessage(message, false)"
|
|
class="message-content text-n-slate-12"
|
|
/>
|
|
<EmailInput
|
|
v-if="isTemplateEmail"
|
|
:message-id="messageId"
|
|
:message-content-attributes="messageContentAttributes"
|
|
/>
|
|
|
|
<IntegrationCard
|
|
v-if="isIntegrations"
|
|
:message-id="messageId"
|
|
:meeting-data="messageContentAttributes.data"
|
|
/>
|
|
</div>
|
|
<div v-if="isOptions">
|
|
<ChatOptions
|
|
:title="message"
|
|
:options="messageContentAttributes.items"
|
|
:hide-fields="!!messageContentAttributes.submitted_values"
|
|
@option-select="onOptionSelect"
|
|
/>
|
|
</div>
|
|
<ChatForm
|
|
v-if="isForm && !messageContentAttributes.submitted_values"
|
|
:items="messageContentAttributes.items"
|
|
:button-label="messageContentAttributes.button_label"
|
|
:submitted-values="messageContentAttributes.submitted_values"
|
|
@submit="onFormSubmit"
|
|
/>
|
|
<div v-if="isCards">
|
|
<ChatCard
|
|
v-for="item in messageContentAttributes.items"
|
|
:key="item.title"
|
|
:media-url="item.media_url"
|
|
:title="item.title"
|
|
:description="item.description"
|
|
:actions="item.actions"
|
|
/>
|
|
</div>
|
|
<div v-if="isArticle">
|
|
<ChatArticle :items="messageContentAttributes.items" />
|
|
</div>
|
|
<CustomerSatisfaction
|
|
v-if="isCSAT"
|
|
:message-content-attributes="messageContentAttributes.submitted_values"
|
|
:display-type="messageContentAttributes.display_type"
|
|
:message="message"
|
|
:message-id="messageId"
|
|
/>
|
|
<ChatTable
|
|
v-if="isTable"
|
|
:title="messageContentAttributes.title"
|
|
:headers="messageContentAttributes.headers"
|
|
:rows="messageContentAttributes.rows"
|
|
/>
|
|
<OrderList
|
|
v-if="isOrderList"
|
|
:orders="messageContentAttributes.orders"
|
|
/>
|
|
<OrderDetail
|
|
v-if="isOrderDetail"
|
|
:status="messageContentAttributes.status"
|
|
:status-text="messageContentAttributes.statusText"
|
|
:status-color="messageContentAttributes.statusColor"
|
|
:order-id="messageContentAttributes.orderId"
|
|
:order-time="messageContentAttributes.orderTime"
|
|
:items="messageContentAttributes.items"
|
|
:show-total="messageContentAttributes.showTotal"
|
|
:total-label="messageContentAttributes.totalLabel"
|
|
:amount-label="messageContentAttributes.amountLabel"
|
|
:actions="messageContentAttributes.actions"
|
|
/>
|
|
<Logistics
|
|
v-if="isLogistics"
|
|
:logistics-name="messageContentAttributes.logisticsName"
|
|
:tracking-number="messageContentAttributes.trackingNumber"
|
|
:current-step="messageContentAttributes.currentStep"
|
|
:is-urgent="messageContentAttributes.isUrgent"
|
|
:latest-log="messageContentAttributes.latestLog"
|
|
:latest-time="messageContentAttributes.latestTime"
|
|
:steps="messageContentAttributes.steps"
|
|
:actions="messageContentAttributes.actions"
|
|
/>
|
|
<ProductList
|
|
v-if="isProductList"
|
|
:title="messageContentAttributes.title"
|
|
:products="messageContentAttributes.products"
|
|
:actions="messageContentAttributes.actions"
|
|
/>
|
|
</div>
|
|
</template>
|