Files
assistant-storefront/app/views/widget_tests/index.html.erb
Liang XJ 4bd11c0ecc
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
feat: add search_image and product_list content types with image upload
## 新增功能

### 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>
2026-01-27 19:03:46 +08:00

531 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.test-panel {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.test-panel h1 {
margin-top: 0;
color: #333;
font-size: 24px;
}
.status-card {
background: #f8f9fa;
border-left: 4px solid #28a745;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.status-card.visitor {
border-left-color: #ffc107;
}
.status-card h3 {
margin-top: 0;
font-size: 16px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
font-weight: 600;
color: #495057;
}
.status-value {
color: #6c757d;
font-family: monospace;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #28a745;
color: white;
}
.btn-primary:hover {
background: #218838;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
}
.log-section {
margin-top: 30px;
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
.log-section h3 {
color: #4ec9b0;
margin-top: 0;
font-size: 14px;
}
.hidden {
display: none;
}
</style>
<body>
<div class="test-panel">
<h1>🧪 Chatwoot Widget 测试页面</h1>
<p style="color: #666; margin-bottom: 20px;">
<strong>测试流程:</strong><br>
1. 页面加载时以游客模式启动(不做任何处理)<br>
2. 点击"模拟用户登录":使用 reset() 清除聊天 + setUser() 设置用户(不刷新页面)<br>
3. 点击"用户退出":使用 reset() 清除内容并刷新页面
</p>
<div id="statusCard" class="status-card visitor">
<h3 id="statusTitle">👤 当前状态:游客模式</h3>
<div class="status-item">
<span class="status-label">User ID:</span>
<span class="status-value" id="displayUserId">无</span>
</div>
<div class="status-item">
<span class="status-label">Token:</span>
<span class="status-value" id="displayToken">不存在</span>
</div>
<div class="status-item">
<span class="status-label">登录状态:</span>
<span class="status-value" id="displayLoginStatus">未登录</span>
</div>
</div>
<div class="button-group">
<button class="btn btn-primary" id="loginBtn" onclick="simulateLogin()">🔐 模拟用户登录</button>
<button class="btn btn-danger hidden" id="logoutBtn" onclick="simulateLogout()">🚪 用户退出</button>
<button class="btn btn-secondary" onclick="refreshPage()">🔄 刷新页面</button>
<button class="btn btn-secondary" onclick="openWidget()">💬 打开聊天</button>
<button class="btn btn-secondary" onclick="clearAllData()">🗑️ 清除所有数据</button>
<button class="btn btn-warning hidden" id="cancelRefreshBtn" onclick="cancelRefresh()">❌ 取消刷新</button>
</div>
<div id="refreshNotice" class="hidden" style="margin-top: 15px; padding: 10px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;">
<strong>⏳ 页面将在 <span id="countdown">2</span> 秒后自动刷新...</strong>
</div>
<div class="log-section">
<h3>📋 控制台日志</h3>
<div id="logOutput">等待操作...</div>
</div>
</div>
</body>
<%
# 测试用的用户信息
# 检查 cookie 中是否有 token
token_present = cookies[:token].present?
if token_present
test_user_id = '211845'
test_user_hash = OpenSSL::HMAC.hexdigest(
'sha256',
@web_widget.hmac_token,
test_user_id.to_s
)
else
test_user_id = nil
test_user_hash = nil
end
%>
<script>
// 测试用的 JWT Token
const TEST_TOKEN = 'eyJ0eXAiOiJqd3QifQ.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvOiIsImV4cCI6MTc3MjAwNTk1MSwiaWF0IjoxNzY5NDEzOTUxLCJuYmYiOjE3Njk0MTM5NTEsInVzZXJJZCI6MjExODQ1LCJ0eXBlIjoyLCJ0ZW5hbnRJZCI6MiwidWlkIjoyMTE4NDUsInMiOiI4VG5wd2QiLCJqdGkiOiI5MjFlYzhhMDE2Yjk3MTA5OTI1MjgxMjUzZWQ1MzBkYSJ9.ql7ianUz3Scn_y0JbMXjeq56BVhjpQqt2_vrdq0kUL4';
// 测试用户信息
const TEST_USER = {
userId: '<%= test_user_id %>',
userHash: '<%= test_user_hash %>',
email: '211845@example.com',
name: '测试用户 211845'
};
// 当前状态
let isLoggedIn = false;
let currentUser = null;
let refreshTimer = null;
let countdownTimer = null;
let hadUserIdentifier = false; // 记录之前是否有过用户标识
// Helper function to get cookie value by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// Helper function to set cookie
function setCookie(name, value, days = 7) {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = name + '=' + value + '; expires=' + expires + '; path=/';
}
// Helper function to delete cookie
function deleteCookie(name) {
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';
}
// 日志输出
function addLog(message) {
const logOutput = document.getElementById('logOutput');
const timestamp = new Date().toLocaleTimeString();
logOutput.innerHTML += `<div>[${timestamp}] ${message}</div>`;
logOutput.scrollTop = logOutput.scrollHeight;
console.log(message);
}
// 更新状态显示
function updateStatusDisplay() {
const token = getCookie('token');
const userId = currentUser ? currentUser.userId : null;
document.getElementById('displayUserId').textContent = userId || '无';
document.getElementById('displayToken').textContent = token ? '存在' : '不存在';
document.getElementById('displayLoginStatus').textContent = isLoggedIn ? '已登录' : '未登录';
const statusCard = document.getElementById('statusCard');
const statusTitle = document.getElementById('statusTitle');
const loginBtn = document.getElementById('loginBtn');
const logoutBtn = document.getElementById('logoutBtn');
if (isLoggedIn) {
statusCard.className = 'status-card';
statusTitle.textContent = '✅ 当前状态:已登录';
loginBtn.classList.add('hidden');
logoutBtn.classList.remove('hidden');
} else {
statusCard.className = 'status-card visitor';
statusTitle.textContent = '👤 当前状态:游客模式';
loginBtn.classList.remove('hidden');
logoutBtn.classList.add('hidden');
}
}
// 初始化 Chatwoot Widget
window.chatwootSettings = {
hideMessageBubble: false,
position: '<%= @widget_position %>',
locale: 'zh_CN',
useBrowserLanguage: false,
type: '<%= @widget_type %>',
widgetStyle: '<%= @widget_style %>',
darkMode: '<%= @dark_mode %>',
};
(function(d,t) {
var BASE_URL = '';
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src= BASE_URL + "/packs/js/sdk.js";
g.async = true;
s.parentNode.insertBefore(g,s);
g.onload=function(){
addLog('✅ Chatwoot SDK 加载完成');
// 检查当前登录状态(仅用于显示)
const token = getCookie('token');
addLog('🔍 检查登录状态...');
addLog(' - Token 存在: ' + (token ? '是' : '否'));
addLog(' - userId (服务器): ' + ('<%= test_user_id %>' || '无'));
// 无论是否有 token都以游客模式启动
// 用户信息通过点击登录按钮来设置
addLog('👤 游客模式启动(不做任何处理)');
hadUserIdentifier = false;
// 游客模式:不清除任何数据,保留现有会话
const widgetConfig = {
websiteToken: '<%= @web_widget.website_token %>',
baseUrl: BASE_URL,
locale: 'zh_CN',
useBrowserLanguage: false
};
addLog('📝 初始化 widgetConfig (游客模式):');
addLog(' - websiteToken: ' + widgetConfig.websiteToken);
addLog(' - userIdentifier: (未设置)');
window.chatwootSDK.run(widgetConfig);
addLog('✅ 游客模式已启动');
updateStatusDisplay();
};
})(document,"script");
// 模拟用户登录
function simulateLogin() {
addLog('🔐 开始模拟用户登录...');
// 设置 token cookie
setCookie('token', TEST_TOKEN);
// 验证 token 是否设置成功
const tokenCheck = getCookie('token');
if (tokenCheck) {
addLog('✅ Token 已设置到 cookie');
// 设置当前用户状态
isLoggedIn = true;
currentUser = TEST_USER;
addLog('🔄 使用 reset() 清除之前的聊天内容...');
if (window.$chatwoot && window.$chatwoot.reset) {
window.$chatwoot.reset();
addLog('✅ 已清除聊天内容和会话数据');
} else {
addLog('⚠️ Chatwoot reset() 方法不可用');
}
// 等待 reset 完成后设置用户信息
setTimeout(function() {
addLog('👤 设置用户信息...');
if (window.$chatwoot && window.$chatwoot.setUser) {
window.$chatwoot.setUser(currentUser.userId, {
identifier_hash: currentUser.userHash,
email: currentUser.email,
name: currentUser.name,
phone_number: '',
custom_attributes: {
jwt_token: tokenCheck,
mall_token: tokenCheck,
login_status: 'logged_in'
}
});
addLog('✅ 用户信息已设置: ' + currentUser.name);
addLog('✅ 登录成功(无需刷新页面)');
} else {
addLog('⚠️ window.$chatwoot 未就绪');
}
updateStatusDisplay();
}, 500);
} else {
addLog('❌ Token 设置失败');
}
}
// 模拟用户退出
function simulateLogout() {
addLog('🚪 开始模拟用户退出...');
// 删除 token cookie
deleteCookie('token');
isLoggedIn = false;
currentUser = null;
addLog('✅ Token 已从 cookie 删除');
// 使用 Chatwoot 官方的 reset() 方法清除用户和会话数据
if (window.$chatwoot && window.$chatwoot.reset) {
window.$chatwoot.reset();
addLog('✅ 已调用 Chatwoot reset() - 清除用户和会话数据');
} else {
addLog('⚠️ Chatwoot reset() 方法不可用');
}
// 清除 localStorage 缓存
try {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (key.includes('chatwoot') || key.includes('cw_') || key.includes('widget'))) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
addLog('🧹 已清除 ' + keysToRemove.length + ' 个 localStorage 缓存项');
} catch (e) {
addLog('⚠️ 清除缓存时出错: ' + e.message);
}
addLog('⏳ 即将刷新页面以完成退出...');
// 显示刷新倒计时
document.getElementById('refreshNotice').classList.remove('hidden');
document.getElementById('cancelRefreshBtn').classList.remove('hidden');
document.getElementById('countdown').textContent = '2';
let countdown = 2;
countdownTimer = setInterval(() => {
countdown--;
document.getElementById('countdown').textContent = countdown;
if (countdown <= 0) {
clearInterval(countdownTimer);
addLog('🔄 正在刷新页面...');
location.reload();
}
}, 1000);
refreshTimer = setTimeout(() => {
location.reload();
}, 2000);
}
// 取消刷新
function cancelRefresh() {
if (refreshTimer) {
clearTimeout(refreshTimer);
refreshTimer = null;
}
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
document.getElementById('refreshNotice').classList.add('hidden');
document.getElementById('cancelRefreshBtn').classList.add('hidden');
addLog('✅ 已取消自动刷新');
addLog('⚠️ 注意Chatwoot Widget 仍保持登录状态,需要手动刷新页面才能完全恢复游客模式');
}
// 清除所有数据(包括会话)
function clearAllData() {
addLog('🗑️ 开始清除所有数据...');
// 使用 Chatwoot reset() 方法清除会话
if (window.$chatwoot && window.$chatwoot.reset) {
window.$chatwoot.reset();
addLog('✅ 已调用 Chatwoot reset() - 清除会话和用户数据');
}
// 删除 token cookie
deleteCookie('token');
addLog('✅ Token 已删除');
// 清除 localStorage
try {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (key.includes('chatwoot') || key.includes('cw_') || key.includes('widget'))) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
addLog('🧹 已清除 ' + keysToRemove.length + ' 个 localStorage 项');
} catch (e) {
addLog('⚠️ 清除 localStorage 时出错: ' + e.message);
}
isLoggedIn = false;
currentUser = null;
hadUserIdentifier = false;
updateStatusDisplay();
addLog('✅ 所有数据已清除');
// 刷新页面
addLog('⏳ 即将刷新页面...');
setTimeout(() => {
location.reload();
}, 500);
}
// 刷新页面
function refreshPage() {
addLog('🔄 正在刷新页面...');
setTimeout(() => {
location.reload();
}, 500);
}
// 打开聊天窗口
function openWidget() {
if (window.$chatwoot && window.$chatwoot.toggle) {
window.$chatwoot.toggle('open');
addLog('💬 聊天窗口已打开');
} else {
addLog('⚠️ Chatwoot 未就绪');
}
}
// 事件监听
window.addEventListener('chatwoot:ready', function() {
addLog('✅ Chatwoot Widget 已就绪');
})
window.addEventListener('chatwoot:on-message', function(e) {
addLog('📨 收到消息');
})
window.addEventListener('chatwoot:opened', function() {
addLog('🔓 Widget 已打开');
})
window.addEventListener('chatwoot:closed', function() {
addLog('🔒 Widget 已关闭');
})
// 初始化
addLog('🚀 测试页面已加载');
</script>