添加用户消息记录接口;

This commit is contained in:
karlcarlo
2025-12-11 03:53:47 +08:00
parent 6029da058a
commit 420fed4bcf
49 changed files with 7574 additions and 426 deletions

View File

@@ -1,2 +1,9 @@
# API 基础地址
VITE_API_BASE_URL=http://chat.nexjs.com/api
VITE_API_BASE_URL=http://mp.yizhezhe.com/api
# 企业微信配置
VITE_WECOM_CORP_ID=your_corp_id_here
VITE_WECOM_AGENT_ID=your_agent_id_here
# 用户标签属性接口基础地址
VITE_CUSTOMER_API_BASE=https://admin.doouba.cn/api

View File

@@ -1,10 +1,14 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#4f46e5" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>兜爸心选 - 企业微信智能客服</title>
<meta name="description" content="兜爸心选企业微信智能客服系统,集成企业微信JSSDK,提供智能客服和消息发送功能" />
</head>
<body>
<div id="root"></div>

View File

@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@wecom/jssdk": "^2.3.3",
"autoprefixer": "^10.4.22",
"axios": "^1.13.2",
"daisyui": "^5.5.5",

View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@wecom/jssdk':
specifier: ^2.3.3
version: 2.3.3
autoprefixer:
specifier: ^10.4.22
version: 10.4.22(postcss@8.5.6)
@@ -541,6 +544,9 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@wecom/jssdk@2.3.3':
resolution: {integrity: sha512-bazYSD39MoiaUV1xMVxrOPsTFa37ARY0xXAqDgth4kU/xk8n61o1YiAcdn4dPM+kBnJKOfukFYPjOPQv8K5l+w==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1805,6 +1811,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@wecom/jssdk@2.3.3': {}
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0

View File

@@ -1,301 +1,598 @@
import { useState, useEffect, useRef } from 'react'
import axios from 'axios'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://chat.nexjs.com/api'
import { useState, useEffect, useRef, useCallback } from 'react';
import { useWeCom } from './hooks/useWeCom';
import { workflowService, customerService } from './services/wecomService';
import WorkflowProgress from './components/WorkflowProgress';
import { getStepNames } from './constants/workflowSteps';
import { getSceneDescription } from './constants/wecomScenes';
function App() {
const [messages, setMessages] = useState([])
const [inputMessage, setInputMessage] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [conversationId, setConversationId] = useState(null)
const [roleConfig, setRoleConfig] = useState({
name: "兜兜",
description: "你是兜兜,1989年出生,有一个5岁的女儿(2018年出生)。你是一位对用户负责、选品严格的宝妈客服。"
})
const [isEditingRole, setIsEditingRole] = useState(false)
const messagesEndRef = useRef(null)
// 企业微信 Hook
const weCom = useWeCom();
// 聊天状态
const [messages, setMessages] = useState([]);
// 工作流状态
const [workflowStatus, setWorkflowStatus] = useState({
isRunning: false,
currentStep: null,
progress: {},
result: null,
error: null
});
// 执行计数
const [executionCount, setExecutionCount] = useState(0);
const messagesEndRef = useRef(null);
const hasAutoExecutedRef = useRef(false);
// 自动滚动到最新消息
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom()
}, [messages])
scrollToBottom();
}, [messages]);
// 获取当前角色配置
useEffect(() => {
fetchRoleConfig()
}, [])
const fetchRoleConfig = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/role`)
setRoleConfig(response.data)
} catch (error) {
console.error('获取角色配置失败:', error)
}
}
const updateRoleConfig = async (newConfig) => {
try {
const response = await axios.put(`${API_BASE_URL}/role`, newConfig)
setRoleConfig(response.data.role_config)
setIsEditingRole(false)
} catch (error) {
console.error('更新角色配置失败:', error)
}
}
const sendMessage = async () => {
if (!inputMessage.trim() || isLoading) return
const userMessage = {
role: 'user',
content: inputMessage,
// 添加系统消息
const addSystemMessage = useCallback((content) => {
const systemMessage = {
role: 'system',
content,
timestamp: new Date().toLocaleTimeString()
};
setMessages(prev => [...prev, systemMessage]);
}, []);
// 添加助手消息
const addAssistantMessage = useCallback((content) => {
const assistantMessage = {
role: 'assistant',
content,
timestamp: new Date().toLocaleTimeString()
};
setMessages(prev => [...prev, assistantMessage]);
}, []);
// 处理工作流进度更新
const handleWorkflowProgress = useCallback((stepId, status, data) => {
// 添加进度消息到聊天
const stepNames = getStepNames();
if (status === 'loading') {
if (stepId !== 'auth' && stepId !== 'register' && stepId !== 'customer') { //初始化步骤不显示加载消息
addSystemMessage(`📌 正在${stepNames[stepId]}...`);
}
} else if (status === 'success') {
addSystemMessage(`✅ ${stepNames[stepId]}成功!`);
// 如果是标签步骤成功,单独显示标签消息
if (stepId === 'tags' && data && data.tags && data.tags.length > 0) {
const tagNames = data.tags.map(tag => tag.TagName).join(', ');
addAssistantMessage(`📋 客户标签: ${tagNames}`);
}
// 如果是历史步骤成功,显示历史会话内容
if (stepId === 'history') {
// 格式化并显示历史会话
if (data && data.messages && data.messages.length > 0) {
// 获取客户ID(从之前的步骤中)
const customerId = workflowStatus.progress.customer?.data?.externalUserId;
// 格式化历史会话显示
const formattedHistory = `📜 最近对话记录(${Math.min(data.messages.length, 5)}条):\n\n` +
data.messages
.slice(0, 5) // 只显示最近5条
.map((msg, index) => {
// 确定发送者角色
const role = msg.from_user === customerId ? '客户' : '客服';
// 获取消息内容
const content = msg.text?.content || msg.content || '无文本内容';
// 简化内容,避免过长
const simplifiedContent = content.length > 80
? content.substring(0, 80) + '...'
: content;
return `• ${role}:${simplifiedContent}`;
})
.join('\n');
addAssistantMessage(formattedHistory);
} else {
addAssistantMessage('📜 暂无历史会话记录');
}
// 更新工作流状态
setWorkflowStatus(prev => ({
...prev,
currentStep: stepId,
progress: {
...prev.progress,
[stepId]: {
status,
data,
timestamp: new Date().toISOString()
}
}
}));
return; // 提前返回
}
} else if (status === 'error') {
addSystemMessage(`❌ ${stepNames[stepId]}失败: ${data?.message || '未知错误'}`);
}
setMessages(prev => [...prev, userMessage])
setInputMessage('')
setIsLoading(true)
// 使用函数式更新,避免依赖 workflowStatus.progress
setWorkflowStatus(prev => ({
...prev,
currentStep: stepId,
progress: {
...prev.progress,
[stepId]: {
status,
data,
timestamp: new Date().toISOString()
}
}
}));
}, [addSystemMessage, addAssistantMessage, workflowStatus.progress.customer?.data?.externalUserId]);
// 执行智能回复工作流(不发送消息)
const executeSmartReplyWorkflow = useCallback(async () => {
setMessages([]); // 清空消息
// 使用函数式更新检查是否正在运行
setWorkflowStatus(prev => {
if (prev.isRunning) return prev;
return {
isRunning: true,
currentStep: null,
progress: {},
result: null,
error: null
};
});
setExecutionCount(prev => prev + 1);
addSystemMessage('📌 开始执行智能回复工作流...');
try {
const response = await axios.post(`${API_BASE_URL}/chat`, {
message: inputMessage,
conversation_id: conversationId
})
// 使用不发送消息的工作流版本
const result = await workflowService.executeWorkflowWithoutSend(weCom, handleWorkflowProgress);
const assistantMessage = {
role: 'assistant',
content: response.data.response,
timestamp: new Date().toLocaleTimeString()
setWorkflowStatus(prev => ({
...prev,
isRunning: false,
result,
error: null
}));
// 显示生成的回复
if (result.generatedMessage) {
// 处理 generatedMessage 可能是对象的情况
let messageContent = result.generatedMessage;
if (typeof messageContent === 'object') {
// 如果是发送步骤返回的对象,提取 message 字段
if (messageContent.message) {
messageContent = messageContent.message;
} else if (messageContent.errMsg) {
messageContent = `发送消息失败: ${messageContent.errMsg}`;
} else {
// 转换为字符串作为后备方案
messageContent = JSON.stringify(messageContent);
}
}
addAssistantMessage(`${messageContent}`);
}
// 根据工作流结果和JSSDK状态显示不同的提示语
if (result.success && weCom.sdkReady && !weCom.sdkError) {
// 工作流成功且JSSDK正常
addSystemMessage('✅ 工作流执行完成!已生成智能回复,请点击下方按钮发送到企业微信。');
} else if (result.success && (weCom.sdkError || !weCom.sdkReady)) {
// 工作流成功但JSSDK有问题
addSystemMessage('⚠️ 工作流执行完成,但企业微信JSSDK状态异常。生成的回复可能无法发送到企业微信。');
} else {
// 工作流失败
addSystemMessage('❌ 工作流执行完成,但遇到问题无法生成个性化回复。');
}
setMessages(prev => [...prev, assistantMessage])
setConversationId(response.data.conversation_id)
} catch (error) {
console.error('发送消息失败:', error)
const errorMessage = {
role: 'assistant',
content: '抱歉,兜兜暂时无法回复,请稍后再试~ 😔',
timestamp: new Date().toLocaleTimeString()
console.error('工作流执行失败:', error);
// 检查是否是获取客户ID相关的错误
let errorMessage = error.message;
let userFriendlyMessage = error.message;
if (errorMessage.includes('getCurExternalContact') || errorMessage.includes('entry 值为') || errorMessage.includes('不支持获取客户ID')) {
userFriendlyMessage = `获取客户ID失败: 当前页面不支持获取客户信息\n\n💡 请确保在企业微信的以下页面中打开此应用:\n• 外部联系人聊天页面\n• 客户详情页面\n• 单聊工具栏页面`;
} else if (errorMessage.includes('JSSDK') || errorMessage.includes('register') || errorMessage.includes('SDK')) {
userFriendlyMessage = `企业微信JSSDK错误: ${errorMessage}\n\n请检查企业微信配置或刷新页面重试。`;
} else if (errorMessage.includes('超时')) {
userFriendlyMessage = `操作超时: ${errorMessage}\n\n可能是网络问题或企业微信JSSDK未正确响应。`;
}
setMessages(prev => [...prev, errorMessage])
setWorkflowStatus(prev => ({
...prev,
isRunning: false,
error: errorMessage
}));
addSystemMessage(`❌工作流执行失败: ${userFriendlyMessage}`);
}
}, [weCom, handleWorkflowProgress]);
// 发送智能回复到企业微信
const sendSmartReply = useCallback(async () => {
if (workflowStatus.isRunning || !weCom.sdkReady) return;
setWorkflowStatus(prev => ({
...prev,
isRunning: true,
error: null
}));
addSystemMessage('📌开始发送智能回复到企业微信...');
try {
// 这里应该调用发送消息到企业微信的API
// 暂时使用模拟发送
await new Promise(resolve => setTimeout(resolve, 1000));
// 如果有生成的回复,发送它
let replyContent = '您好!我是兜爸心选的智能客服,有什么可以帮您的吗?';
if (workflowStatus.result?.generatedMessage) {
const generatedMessage = workflowStatus.result.generatedMessage;
if (typeof generatedMessage === 'object' && generatedMessage.message) {
replyContent = generatedMessage.message;
} else if (typeof generatedMessage === 'string') {
replyContent = generatedMessage;
}
}
// 调用企业微信发送消息
const sendResult = await weCom.sendChatMessage(replyContent);
addSystemMessage('✅ 智能回复已成功发送到企业微信!');
} catch (error) {
console.error('发送智能回复失败:', error);
addSystemMessage(`❌发送智能回复失败: ${error.message}`);
setWorkflowStatus(prev => ({
...prev,
error: error.message
}));
} finally {
setIsLoading(false)
setWorkflowStatus(prev => ({
...prev,
isRunning: false
}));
}
}
}, [workflowStatus.isRunning, workflowStatus.result, weCom]);
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
sendMessage()
// 检查企业微信配置和首次自动执行
useEffect(() => {
if (!weCom.isConfigured) {
addSystemMessage('⚠️ 企业微信配置缺失,请检查 .env 文件中的 VITE_WECOM_CORP_ID 和 VITE_WECOM_AGENT_ID');
} else if (weCom.sdkError) {
addSystemMessage(`⚠️ 企业微信 SDK 错误: ${weCom.sdkError.message}`);
} else if (weCom.sdkReady && !hasAutoExecutedRef.current) {
addSystemMessage(`✅ 企业微信 SDK 已就绪`);
// 首次自动执行工作流(仅一次)
hasAutoExecutedRef.current = true;
executeSmartReplyWorkflow();
}
}
}, [weCom.isConfigured, weCom.sdkReady, weCom.sdkError]);
const clearConversation = () => {
setMessages([])
setConversationId(null)
}
// 移动端侧边栏状态
const [sidebarOpen, setSidebarOpen] = useState(false);
// 页面场景状态
const [pageContext, setPageContext] = useState(null);
// 自动获取页面场景(不显示在聊天中)
useEffect(() => {
const fetchPageContext = async () => {
if (weCom.sdkReady) {
try {
console.log('开始自动获取页面场景...');
const context = await weCom.getContext();
console.log('获取到的页面场景:', context);
setPageContext(context);
// 不显示在聊天中,只在工作流执行时显示
} catch (error) {
console.error('自动获取页面场景失败:', error);
// 不显示错误在聊天中
}
} else {
console.log('企业微信SDK未就绪,无法获取页面场景');
}
};
fetchPageContext();
}, [weCom.sdkReady, weCom.getContext, weCom]);
return (
<div className="min-h-screen bg-gray-50">
{/* 顶部导航栏 */}
<header className="bg-blue-600 text-white shadow-lg">
<header className="bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-lg">
<div className="container mx-auto px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
{/* 移动端菜单按钮 */}
<button
className="lg:hidden w-10 h-10 flex items-center justify-center rounded-full hover:bg-white/20"
onClick={() => setSidebarOpen(!sidebarOpen)}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center">
<span className="text-blue-600 text-xl">🤖</span>
</div>
<div>
<h1 className="text-xl font-bold">兜爸 AI 聊天助手</h1>
<p className="text-sm opacity-90">我是{roleConfig.name},很高兴为您服务</p>
<div className="hidden sm:block">
<h1 className="text-xl font-bold">兜爸心选 - 企业微信智能客服</h1>
<p className="text-sm opacity-90">
自动获取客户消息会话与标签,生成个性化智能回复
</p>
</div>
<div className="sm:hidden">
<h1 className="text-lg font-bold">智能客服</h1>
</div>
</div>
<div className="flex items-center space-x-2">
<button
className="px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors"
onClick={() => setIsEditingRole(true)}
className="hidden sm:inline px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors"
onClick={executeSmartReplyWorkflow}
disabled={workflowStatus.isRunning || !weCom.sdkReady}
>
编辑角色
{workflowStatus.isRunning ? '执行中...' : '执行工作流'}
</button>
<button
className="px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors"
onClick={clearConversation}
>
新对话
</button>
{/* 移动端操作按钮 */}
<div className="sm:hidden flex space-x-1">
<button
className="w-8 h-8 flex items-center justify-center bg-white/20 rounded hover:bg-white/30 transition-colors"
onClick={executeSmartReplyWorkflow}
disabled={workflowStatus.isRunning || !weCom.sdkReady}
title="执行工作流"
>
{workflowStatus.isRunning ? '⏳' : '🔄️'}
</button>
</div>
</div>
</div>
</div>
</header>
{/* 聊天区域 */}
<main className="flex-1" style={{ height: 'calc(100vh - 80px - 120px)' }}>
<div className="h-full overflow-y-auto p-4">
{messages.length === 0 ? (
// 欢迎页面
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-md">
<div className="w-20 h-20 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-white text-3xl">👋</span>
</div>
<h2 className="text-2xl font-bold text-gray-800 mb-3">欢迎使用, 兜爸 AI 聊天助手</h2>
<p className="text-gray-600 mb-4">
我是{roleConfig.name},一位亲切温暖的宝妈客服。有任何关于育儿、选品、团购的问题,都可以随时问我哦~
</p>
<div className="bg-white rounded-lg p-4 border border-gray-200">
<p className="text-sm text-gray-500">
💡 提示:开始对话吧,我会以亲切温暖的风格为您解答问题
</p>
</div>
{/* 状态栏 */}
<div className="bg-white border-b border-gray-200">
<div className="container mx-auto px-4 py-2">
<div className="flex flex-col sm:flex-row sm:items-center justify-between text-sm space-y-2 sm:space-y-0">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<div className={`w-2 h-2 rounded-full ${weCom.sdkReady ? 'bg-green-500' : 'bg-red-500'}`}></div>
<span className="text-gray-700">
{weCom.sdkReady ? '企业微信 SDK 已就绪' : '企业微信 SDK 未就绪'}
</span>
</div>
<div className="hidden sm:flex items-center space-x-1">
<div className={`w-2 h-2 rounded-full ${workflowStatus.isRunning ? 'bg-yellow-500' : workflowStatus.result ? 'bg-green-500' : 'bg-gray-300'}`}></div>
<span className="text-gray-700">
{workflowStatus.isRunning ? '工作流执行中...' : workflowStatus.result ? '工作流已完成' : '工作流待执行'}
</span>
</div>
</div>
) : (
// 聊天消息列表
<div className="max-w-4xl mx-auto space-y-4">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-[70%] rounded-2xl px-4 py-3 ${
message.role === 'user'
? 'bg-blue-600 text-white rounded-br-none'
: 'bg-white border border-gray-200 rounded-bl-none shadow-sm'
}`}>
<div className="text-sm leading-relaxed">
{message.content}
</div>
<div className={`text-xs mt-1 ${
message.role === 'user' ? 'text-blue-100' : 'text-gray-500'
}`}>
{message.timestamp}
</div>
</div>
</div>
))}
{/* 加载状态 */}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white border border-gray-200 rounded-2xl rounded-bl-none px-4 py-3 shadow-sm">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
<div className="flex items-center space-x-2">
<span className="text-gray-500">
执行次数: {executionCount}
</span>
</div>
)}
</div>
</div>
</main>
</div>
{/* 输入区域 */}
<footer className="border-t border-gray-200 bg-white p-4">
<div className="max-w-4xl mx-auto">
<div className="flex items-stretch border border-gray-300 rounded-lg bg-white focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-200 transition-colors overflow-hidden">
<div className="flex-1">
<textarea
className="w-full h-full px-4 py-3 resize-none border-none focus:outline-none focus:ring-0 bg-transparent"
placeholder={`向${roleConfig.name}发送消息... (按 Enter 发送)`}
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress}
rows={2}
disabled={isLoading}
/>
{/* 移动端侧边栏遮罩 */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* 主内容区域 */}
<div className="flex h-[calc(100vh-240px)]">
{/* 聊天区域 */}
<div className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto p-2 sm:p-4">
{messages.length === 0 ? (
// 欢迎页面
<div className="flex items-center justify-center h-full px-2">
<div className="text-center max-w-md">
<div className="w-16 h-16 sm:w-20 sm:h-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-white text-2xl sm:text-3xl">👋</span>
</div>
<h2 className="text-xl sm:text-2xl font-bold text-gray-800 mb-3">欢迎使用兜爸心选智能客服</h2>
<p className="text-sm sm:text-base text-gray-600 mb-4">
本系统集成了企业微信智能回复系统,可以自动获取客户历史会话、标签属性,并生成个性化的智能回复。
</p>
<div className="bg-white rounded-lg p-3 sm:p-4 border border-gray-200">
<p className="text-xs sm:text-sm text-gray-500">
💡 系统将自动执行以下流程:
<ol className="mt-2 space-y-1 text-left pl-4">
<li className="text-xs sm:text-sm">1. 获取企业微信认证信息</li>
<li className="text-xs sm:text-sm">2. 初始化智能回复系统</li>
<li className="text-xs sm:text-sm">3. 获取当前客户ID</li>
<li className="text-xs sm:text-sm">4. 查询客户标签属性</li>
<li className="text-xs sm:text-sm">5. 生成个性化回复</li>
</ol>
</p>
</div>
</div>
</div>
) : (
// 聊天消息列表
<div className="max-w-4xl mx-auto space-y-3 sm:space-y-4">
{messages.map((message, index) => (
<div
key={index}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} ${
message.role === 'system' ? 'justify-center' : ''
}`}
>
<div className={`max-w-[85%] sm:max-w-[70%] rounded-2xl px-3 py-2 sm:px-4 sm:py-3 ${
message.role === 'user'
? 'bg-blue-600 text-white rounded-br-none'
: message.role === 'system'
? 'bg-gray-100 text-gray-700 border border-gray-200'
: 'bg-white border border-gray-200 rounded-bl-none shadow-sm'
}`}>
<div className="text-sm leading-relaxed break-words">
{message.content}
</div>
{/* <div className={`text-xs mt-1 ${
message.role === 'user' ? 'text-blue-100' :
message.role === 'system' ? 'text-gray-500' :
'text-gray-500'
}`}>
{message.timestamp}
</div> */}
</div>
</div>
))}
{/* 加载状态 */}
{workflowStatus.isRunning && (
<div className="flex justify-start">
<div className="bg-white border border-gray-200 rounded-2xl rounded-bl-none px-4 py-3 shadow-sm">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
)}
</div>
</div>
{/* 工作流状态面板 - 桌面端和移动端抽屉 */}
<div className={`hidden lg:block w-80 border-l border-gray-200 bg-white overflow-y-auto ${
sidebarOpen ? '!block fixed right-0 top-0 h-full w-80 z-50 shadow-2xl' : ''
}`}>
<div className="p-4 h-full overflow-y-auto">
{/* 移动端侧边栏头部 */}
{sidebarOpen && (
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
<h3 className="font-bold text-gray-800">工作流状态</h3>
<button
className="p-2 hover:bg-gray-100 rounded"
onClick={() => setSidebarOpen(false)}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
<WorkflowProgress
progress={workflowStatus.progress}
includeSend={false}
title={sidebarOpen ? '' : '工作流状态'}
/>
{/* 错误信息 */}
{workflowStatus.error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<div className="font-medium text-red-800 mb-1">工作流错误</div>
<div className="text-sm text-red-600">{workflowStatus.error}</div>
</div>
)}
{/* 企业微信状态 */}
<div className="mt-6 pt-4 border-t border-gray-200">
<h4 className="font-medium text-gray-700 mb-2">企业微信状态</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">SDK 状态:</span>
<span className={weCom.sdkReady ? 'text-green-600' : 'text-red-600'}>
{weCom.sdkReady ? '已就绪' : '未就绪'}
</span>
</div>
{/* 直接显示页面场景 */}
<div className="flex justify-between">
<span className="text-gray-600">页面场景:</span>
<span className="text-blue-600">
{pageContext ? (
pageContext.entry ? getSceneDescription(pageContext.entry) : '未知场景'
) : (
weCom.sdkReady ? '获取中...' : 'SDK未就绪'
)}
</span>
</div>
{weCom.sdkError && (
<div className="text-red-600 text-xs break-words">
错误: {weCom.sdkError.message}
</div>
)}
</div>
</div>
<div className="border-l border-gray-200 flex items-center">
</div>
</div>
</div>
{/* 智能回复按钮区域 */}
{workflowStatus.result && (
<footer className="border-t border-gray-200 bg-white p-3 sm:p-4">
<div className="max-w-4xl mx-auto">
<div className="flex flex-col items-center space-y-2 sm:space-y-3">
<div className="text-xs sm:text-sm text-gray-600 text-center px-2">
{workflowStatus.result?.generatedMessage ? (
<span>已生成智能回复,点击下方按钮发送到企业微信</span>
) : (
<span>执行工作流以生成智能回复</span>
)}
</div>
<button
className="bg-blue-600 text-white px-6 h-full font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors min-w-[6rem]"
onClick={sendMessage}
disabled={!inputMessage.trim() || isLoading}
className="w-full max-w-sm bg-gradient-to-r from-green-500 to-emerald-600 text-white px-4 sm:px-8 py-2 sm:py-3 rounded-lg font-medium hover:from-green-600 hover:to-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-md hover:shadow-lg"
onClick={sendSmartReply}
disabled={workflowStatus.isRunning || !weCom.sdkReady || !workflowStatus.result?.generatedMessage}
>
{isLoading ? (
{workflowStatus.isRunning ? (
<span className="flex items-center justify-center">
<span className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></span>
发送中
<span className="w-3 h-3 sm:w-4 sm:h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></span>
发送中...
</span>
) : (
<span className="flex items-center justify-center space-x-2">
<span>发送</span>
<span className="text-xs opacity-70">↵</span>
<span className="flex items-center justify-center space-x-1 sm:space-x-2">
<span className="text-sm sm:text-base">📤 发送智能回复到企业微信</span>
</span>
)}
</button>
</div>
</div>
</div>
</footer>
{/* 角色编辑模态框 */}
{isEditingRole && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-gray-800">编辑角色配置</h3>
<button
className="text-gray-500 hover:text-gray-700"
onClick={() => setIsEditingRole(false)}
>
✕
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
角色名称
</label>
<input
type="text"
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
value={roleConfig.name}
onChange={(e) => setRoleConfig(prev => ({ ...prev, name: e.target.value }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
角色描述
</label>
<textarea
className="w-full border border-gray-300 rounded-lg px-3 py-2 h-32 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
value={roleConfig.description}
onChange={(e) => setRoleConfig(prev => ({ ...prev, description: e.target.value }))}
/>
<div className="text-xs text-gray-500 text-center px-2">
点击按钮将生成的智能回复发送到当前企业微信会话
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
onClick={() => setIsEditingRole(false)}
>
取消
</button>
<button
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
onClick={() => updateRoleConfig(roleConfig)}
>
保存配置
</button>
</div>
</div>
</div>
</footer>
)}
</div>
)
);
}
export default App
export default App;

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { getStepIds, getStepNames, getStepStatusConfig } from '../constants/workflowSteps';
/**
* 工作流进度组件
* 显示工作流各个步骤的状态
*
* @param {Object} props
* @param {Object} props.progress 进度数据对象
* @param {boolean} [props.includeSend=false] 是否包含发送步骤
* @param {string} [props.title='工作流状态'] 组件标题
* @param {string} [props.className=''] 额外的CSS类名
*/
const WorkflowProgress = ({
progress = {},
includeSend = false,
title = '工作流状态',
className = ''
}) => {
const stepIds = getStepIds(includeSend);
const stepNames = getStepNames();
return (
<div className={className}>
{title && <h3 className="font-bold text-gray-800 mb-4">{title}</h3>}
<div className="space-y-3">
{stepIds.map(stepId => {
const stepInfo = progress[stepId];
const statusConfig = stepInfo ? getStepStatusConfig(stepInfo.status) : getStepStatusConfig();
return (
<div key={stepId} className="border border-gray-200 rounded-lg p-3">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm text-gray-700">{stepNames[stepId]}</span>
<span className={`text-xs px-2 py-0.5 rounded-full ${statusConfig.className}`}>
{statusConfig.text}
</span>
</div>
{stepInfo && stepInfo.data && (
<div className="text-xs text-gray-500 mt-1">
{stepId === 'context' && stepInfo.data.scene && (
<div>场景: {stepInfo.data.scene}</div>
)}
{stepId === 'customer' && stepInfo.data.externalUserId && (
<div>客户ID: {stepInfo.data.externalUserId}</div>
)}
{stepId === 'tags' && stepInfo.data.tags && (
<div>标签数: {stepInfo.data.tags.length}</div>
)}
{stepId === 'history' && stepInfo.data && (
<div>消息数: {stepInfo.data.messages?.length || 0}</div>
)}
{stepId === 'generate' && stepInfo.data && (
<div className="truncate">回复已生成</div>
)}
{stepId === 'send' && stepInfo.data && (
<div className="truncate">
发送结果: {stepInfo.data.errCode === 0 ? '成功' : '失败'}
</div>
)}
</div>
)}
{stepInfo && stepInfo.error && (
<div className="text-xs text-red-600 mt-1">{stepInfo.error}</div>
)}
</div>
);
})}
</div>
</div>
);
};
export default WorkflowProgress;

View File

@@ -0,0 +1,76 @@
/**
* 企业微信页面场景类型映射
* 将场景代码转换为友好的中文描述
*/
export const WECOM_SCENE_TYPES = {
// 从联系人详情进入
'contact_profile': '联系人详情',
// 从单聊会话的工具栏进入
'single_chat_tools': '单聊工具栏',
// 从群聊会话的工具栏进入
'group_chat_tools': '群聊工具栏',
// 从会话的聊天附件栏进入
'chat_attachment': '聊天附件栏',
// 从微信客服的工具栏进入
'single_kf_tools': '微信客服工具栏',
// 上下游单聊会话的工具栏
'chain_single_chat_tools': '上下游单聊工具栏',
// 上下游群聊会话的工具栏
'chain_group_chat_tools': '上下游群聊工具栏',
// 从内部群群看板进入
'internal_group_chat_board': '内部群看板'
};
/**
* 获取场景类型的中文描述
* @param {string} sceneCode 场景代码
* @returns {string} 中文描述,如果未知则返回原始代码
*/
export const getSceneDescription = (sceneCode) => {
return WECOM_SCENE_TYPES[sceneCode] || sceneCode;
};
/**
* 获取入口类型的中文描述
* @param {string} entryCode 入口代码
* @returns {string} 中文描述,如果未知则返回原始代码
*/
export const getEntryDescription = (entryCode) => {
const entryMap = {
'contact': '联系人',
'single_chat': '单聊',
'group_chat': '群聊',
'chat': '聊天',
'single_kf': '微信客服',
'chain_single_chat': '上下游单聊',
'chain_group_chat': '上下游群聊',
'internal_group': '内部群'
};
return entryMap[entryCode] || entryCode;
};
/**
* 格式化页面场景信息
* @param {Object} context 页面场景对象
* @returns {Object} 格式化后的场景信息
*/
export const formatPageContext = (context) => {
if (!context) return null;
// 使用 scene 字段,如果不存在则使用 entry 字段作为后备
const sceneCode = context.scene || context.entry;
return {
...context,
entryDisplay: getEntryDescription(context.entry),
sceneDisplay: getSceneDescription(sceneCode)
};
};

View File

@@ -0,0 +1,97 @@
/**
* 工作流步骤常量
* 用于统一管理工作流步骤的名称和配置
*/
export const WORKFLOW_STEPS = {
auth: {
id: 'auth',
name: '获取认证信息',
description: '获取企业微信 Access Token 和 JSAPI Ticket'
},
register: {
id: 'register',
name: '注册企业微信SDK',
description: '初始化智能回复SDK,准备生成个性化回复'
},
customer: {
id: 'customer',
name: '获取客户ID',
description: '获取当前企业微信会话的客户ID'
},
tags: {
id: 'tags',
name: '获取客户标签',
description: '根据客户ID查询客户标签属性'
},
history: {
id: 'history',
name: '获取会话历史',
description: '获取客户的历史会话记录'
},
generate: {
id: 'generate',
name: '生成智能回复',
description: '基于客户标签和历史会话生成个性化的智能回复'
},
send: {
id: 'send',
name: '发送消息到企业微信',
description: '将生成的回复发送到当前企业微信会话'
}
};
/**
* 获取步骤名称映射
* @returns {Object} 步骤ID到名称的映射
*/
export const getStepNames = () => {
const names = {};
Object.values(WORKFLOW_STEPS).forEach(step => {
names[step.id] = step.name;
});
return names;
};
/**
* 获取步骤状态样式
* @param {string} status 步骤状态
* @returns {Object} 包含样式类和显示文本的对象
*/
export const getStepStatusConfig = (status) => {
switch (status) {
case 'success':
return {
className: 'bg-green-100 text-green-800',
text: '成功'
};
case 'error':
return {
className: 'bg-red-100 text-red-800',
text: '失败'
};
case 'loading':
return {
className: 'bg-blue-100 text-blue-800',
text: '进行中'
};
default:
return {
className: 'bg-gray-100 text-gray-800',
text: '待执行'
};
}
};
/**
* 获取所有步骤ID的数组
* @param {boolean} includeSend 是否包含发送步骤
* @returns {Array} 步骤ID数组
*/
export const getStepIds = (includeSend = false) => {
const steps = ['auth', 'register', 'customer', 'tags', 'history', 'generate'];
if (includeSend) {
steps.push('send');
}
return steps;
};

View File

@@ -0,0 +1,375 @@
import { useState, useEffect, useCallback } from 'react';
import * as ww from '@wecom/jssdk';
/**
* 企业微信 JSSDK Hook
* 提供企业微信 JSSDK 的初始化、注册和 API 调用功能
*/
export const useWeCom = () => {
const [sdkReady, setSdkReady] = useState(false);
const [sdkError, setSdkError] = useState(null);
const [isRegistering, setIsRegistering] = useState(false);
// 从环境变量获取配置
const corpId = import.meta.env.VITE_WECOM_CORP_ID;
const agentId = parseInt(import.meta.env.VITE_WECOM_AGENT_ID, 10);
// 检查环境变量
useEffect(() => {
if (!corpId || !agentId) {
setSdkError(new Error('企业微信配置缺失,请检查 .env 文件中的 VITE_WECOM_CORP_ID 和 VITE_WECOM_AGENT_ID'));
}
}, [corpId, agentId]);
// 初始化 JSSDK
const initializeSdk = useCallback(async () => {
if (sdkReady || sdkError) return;
try {
console.log('初始化企业微信 JSSDK...');
console.log('SDK 版本:', ww.SDK_VERSION);
console.log('企业ID:', corpId);
console.log('应用ID:', agentId);
// 检查 SDK 是否可用
if (typeof ww === 'undefined' || ww.SDK_VERSION == null) {
throw new Error('企业微信 JSSDK 未加载成功');
}
setSdkReady(true);
return true;
} catch (error) {
console.error('初始化企业微信 JSSDK 失败:', error);
setSdkError(error);
return false;
}
}, [corpId, agentId, sdkReady, sdkError]);
// 注册 JSSDK
const registerSdk = useCallback(() => {
return new Promise((resolve, reject) => {
if (!sdkReady || isRegistering) return false;
setIsRegistering(true);
setSdkError(null);
console.log('开始注册企业微信 JSSDK...');
// 清理 URL,移除 # 及其后面的部分
const url = window.location.href;
const cleanUrl = url.split('#')[0];
console.log('注册 URL:', cleanUrl);
// 生成签名的函数
const getConfigSignature = async () => {
const timestamp = Math.floor(Date.now() / 1000);
const nonceStr = Math.random().toString(36).substring(2, 15);
return { timestamp, nonceStr, signature: 'temp_corp_signature' };
};
const getAgentConfigSignature = async (url) => {
try {
const cleanUrl = url.split('#')[0];
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://mp.yizhezhe.com/api';
const apiUrl = `${apiBaseUrl}/auth/signature`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: cleanUrl,
timestamp: Math.floor(Date.now() / 1000),
nonceStr: Math.random().toString(36).substring(2, 15)
})
});
const result = await response.json();
if (result.success) {
return result.data;
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('调用签名 API 失败:', error);
// 返回测试签名
const timestamp = Math.floor(Date.now() / 1000);
const nonceStr = Math.random().toString(36).substring(2, 15);
return { timestamp, nonceStr, signature: 'test_signature' };
}
};
// 注册 JSSDK
try {
ww.register({
corpId,
agentId,
jsApiList: ['getContext', 'getCurExternalContact', 'sendChatMessage'],
getConfigSignature,
getAgentConfigSignature,
onConfigSuccess: (res) => {
console.log('JSSDK 注册成功:', res);
setSdkError(null);
setIsRegistering(false);
},
onConfigFail: (err) => {
console.error('JSSDK 注册失败:', err);
let registrationError = new Error(`JSSDK 注册失败: ${err.errMsg || '未知错误'}`);
setSdkError(registrationError);
setIsRegistering(false);
}
});
console.log('企业微信 JSSDK 注册调用完成');
resolve(true);
} catch (error) {
console.error('注册企业微信 JSSDK 时发生错误:', error);
setSdkError(error);
setIsRegistering(false);
reject(error);
}
});
}, [sdkReady, isRegistering, corpId, agentId]);
// 获取页面场景
const getContext = useCallback(() => {
return new Promise((resolve, reject) => {
if (!sdkReady) {
reject(new Error('JSSDK 未就绪'));
return;
}
console.log('开始调用 ww.getContext()...');
console.log('ww 对象:', ww);
console.log('ww.getContext 类型:', typeof ww.getContext);
// 添加超时机制
const timeoutId = setTimeout(() => {
console.warn('获取页面场景超时(3秒),可能JSSDK未正确注册或回调未触发');
reject(new Error('获取页面场景超时:可能JSSDK未正确注册或回调未触发'));
}, 3000);
try {
ww.getContext({
success: (res) => {
clearTimeout(timeoutId);
console.log('获取页面场景成功 - 原始数据:', res);
console.log('获取页面场景成功 - 数据类型:', typeof res);
if (res === undefined || res === null) {
console.warn('获取页面场景返回空数据,可能不在企业微信环境中');
const formattedContext = {
entry: 'unknown',
isValidEntry: false,
validEntries: ['contact_profile', 'single_chat_tools', 'single_kf_tools'],
rawData: res,
message: '获取页面场景返回空数据,可能不在企业微信环境中'
};
resolve(formattedContext);
return;
}
console.log('获取页面场景成功 - 数据键名:', Object.keys(res || {}));
// 企业微信 getContext 返回的数据结构可能是:
// 1. 直接包含 entry 字段的对象
// 2. 包含在 data 字段中的对象
// 3. 其他格式
let contextData = res;
// 如果返回的数据有 data 字段,使用 data 字段
if (res && res.data) {
console.log('检测到 data 字段,使用 data 字段:', res.data);
contextData = res.data;
}
// 根据企业微信官方文档,getContext 只返回 entry 参数
// entry: 进入页面的场景类型
const entry = contextData?.entry || contextData?.entryId || 'unknown';
// 检查 entry 是否有效
const validEntries = ['contact_profile', 'single_chat_tools', 'single_kf_tools'];
const isValidEntry = validEntries.includes(entry);
const formattedContext = {
entry: entry,
isValidEntry: isValidEntry,
validEntries: validEntries,
rawData: res // 保留原始数据用于调试
};
console.log('企业微信 getContext 返回:', {
原始数据: res,
解析后的entry: entry,
是否有效: isValidEntry ? '是' : '否',
有效entry列表: validEntries
});
console.log('格式化后的页面场景数据:', formattedContext);
resolve(formattedContext);
},
fail: (err) => {
clearTimeout(timeoutId);
console.error('获取页面场景失败:', err);
console.error('失败详情 - errCode:', err?.errCode);
console.error('失败详情 - errMsg:', err?.errMsg);
reject(new Error(`获取页面场景失败: ${err?.errMsg || '未知错误'}`));
}
});
} catch (error) {
clearTimeout(timeoutId);
console.error('调用 ww.getContext 时发生异常:', error);
reject(new Error(`调用 getContext 时发生异常: ${error.message}`));
}
});
}, [sdkReady]);
// 获取当前客户ID
const getCurExternalContact = useCallback(() => {
return new Promise((resolve, reject) => {
if (!sdkReady) {
reject(new Error('JSSDK 未就绪'));
return;
}
console.log('开始获取当前客户ID...');
// 添加超时机制
const timeoutId = setTimeout(() => {
console.warn('获取当前客户ID超时(3秒),可能不在外部联系人页面');
reject(new Error('获取当前客户ID超时:可能不在外部联系人聊天页面或JSSDK回调未触发'));
}, 3000);
ww.getCurExternalContact({
success: (res) => {
clearTimeout(timeoutId);
console.log('获取当前客户ID成功:', res);
// 检查返回的数据结构
if (res && res.externalUserId) {
resolve(res);
} else {
console.warn('获取当前客户ID返回的数据格式异常:', res);
// 即使数据格式异常,也返回数据,让上层处理
resolve(res || { externalUserId: 'unknown', data: res });
}
},
fail: (err) => {
clearTimeout(timeoutId);
console.error('获取当前客户ID失败:', err);
// 根据错误类型提供更详细的错误信息
let errorMessage = `获取当前客户ID失败: ${err.errMsg || '未知错误'}`;
if (err.errCode === 10001) {
errorMessage = '获取当前客户ID失败:不在外部联系人聊天页面';
} else if (err.errCode === 10002) {
errorMessage = '获取当前客户ID失败:用户取消了操作';
}
reject(new Error(errorMessage));
},
cancel: () => {
clearTimeout(timeoutId);
console.warn('用户取消了获取当前客户ID操作');
reject(new Error('用户取消了获取当前客户ID操作'));
}
});
});
}, [sdkReady]);
// 发送消息到当前会话
const sendChatMessage = useCallback((content) => {
return new Promise((resolve, reject) => {
if (!sdkReady) {
reject(new Error('JSSDK 未就绪'));
return;
}
ww.sendChatMessage({
msgtype: 'text',
text: {
content
},
success: (res) => {
console.log('发送消息成功:', res);
resolve(res);
},
fail: (err) => {
console.error('发送消息失败:', err);
reject(new Error(`发送消息失败: ${err.errMsg || '未知错误'}`));
},
cancel: () => {
reject(new Error('用户取消了发送消息操作'));
}
});
});
}, [sdkReady]);
// 自动初始化
useEffect(() => {
if (corpId && agentId && !sdkReady && !sdkError) {
initializeSdk();
}
}, [corpId, agentId, sdkReady, sdkError, initializeSdk]);
return {
// 状态
sdkReady,
sdkError,
isRegistering,
corpId,
agentId,
// 方法
initializeSdk,
registerSdk,
getContext,
getCurExternalContact,
sendChatMessage,
// 工具方法
isConfigured: !!corpId && !!agentId
};
};
/**
* 带重试的执行函数
* @param {Function} action 要执行的函数
* @param {number} maxRetries 最大重试次数,默认3次
* @param {Function} onRetry 重试时的回调
* @returns 执行结果
*/
export const executeWithRetry = async (action, maxRetries = 3, onRetry = null) => {
let retryCount = 0;
let lastError = null;
while (retryCount <= maxRetries) {
try {
return await action();
} catch (error) {
lastError = error;
retryCount++;
if (retryCount > maxRetries) {
throw new Error(`操作失败,已重试${maxRetries}次: ${error.message}`);
}
// 指数退避等待
const delay = Math.pow(2, retryCount - 1) * 1000;
console.log(`第${retryCount}次重试,等待${delay}ms后重试...`);
if (onRetry) {
onRetry(retryCount, error, delay);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};

View File

@@ -33,3 +33,59 @@
.dark ::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* 移动端优化 */
@layer base {
html {
-webkit-tap-highlight-color: transparent;
}
body {
@apply antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
/* 触摸友好的按钮 */
@layer components {
.touch-button {
@apply active:scale-95 transition-transform duration-150;
}
/* 移动端优化输入框 */
.mobile-input {
@apply text-base;
font-size: 16px !important; /* 防止iOS缩放 */
}
}
/* 防止移动端长按菜单 */
.no-context-menu {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* 移动端优化滚动 */
.mobile-scroll {
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
/* 防止文本溢出 */
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 移动端优化模态框 */
.mobile-modal {
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}

View File

@@ -0,0 +1,590 @@
import axios from 'axios';
import { executeWithRetry } from '../hooks/useWeCom';
// API 基础配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://mp.yizhezhe.com/api';
const CUSTOMER_API_BASE = import.meta.env.VITE_CUSTOMER_API_BASE || 'https://admin.doouba.cn/api';
// 创建 axios 实例
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
const customerApiClient = axios.create({
baseURL: CUSTOMER_API_BASE,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
/**
* 认证服务
*/
export const authService = {
/**
* 获取 Access Token
* @returns {Promise} Access Token 数据
*/
getAccessToken: () => executeWithRetry(async () => {
console.log('获取 Access Token...');
const response = await apiClient.get('/auth/token');
if (response.data.success !== true) {
throw new Error(`获取 Access Token 失败: ${response.data.message}`);
}
console.log('Access Token 获取成功:', response.data);
return response.data;
}, 3, (retryCount) => {
console.log(`第${retryCount}次重试获取 Access Token...`);
}),
/**
* 获取 JSAPI Ticket
* @returns {Promise} JSAPI Ticket 数据
*/
getJsapiTicket: () => executeWithRetry(async () => {
console.log('获取 JSAPI Ticket...');
const response = await apiClient.get('/auth/ticket');
if (response.data.success !== true) {
throw new Error(`获取 JSAPI Ticket 失败: ${response.data.message}`);
}
console.log('JSAPI Ticket 获取成功:', response.data);
return response.data;
}, 3, (retryCount) => {
console.log(`第${retryCount}次重试获取 JSAPI Ticket...`);
})
};
/**
* 客户服务
*/
export const customerService = {
/**
* 获取客户标签属性
* @param {string} userId 客户ID
* @returns {Promise} 客户信息(包含标签)
*/
getCustomerTags: (userId) => executeWithRetry(async () => {
console.log(`获取客户 ${userId} 的标签属性...`);
const response = await customerApiClient.get(`/WechatCustomers/${userId}`, {
params: {
includeWechatInfo: true
}
});
if (response.data.code !== 200) {
throw new Error(`获取客户标签失败: ${response.data.msg}`);
}
console.log('客户标签获取成功:', response.data.data);
return response.data.data;
}, 3, (retryCount) => {
console.log(`第${retryCount}次重试获取客户标签...`);
}),
/**
* 根据标签和历史会话生成提示词
* @param {Object} customerInfo 客户信息
* @param {Array} messageHistory 消息历史数据
* @returns {string} 生成的提示词
*/
generatePromptFromTagsAndHistory: (customerInfo, messageHistory = []) => {
if (!customerInfo || !customerInfo.tags) {
return '你好!我是兜爸心选的客服,有什么可以帮您的吗?';
}
const { name, tags, externalUserId } = customerInfo;
const tagNames = tags.map(t => t.TagName).join('、');
const tagCategories = [...new Set(tags.map(t => t.TagCategory))].join('、');
// 1. 基础客户信息
let prompt = `【客户基本信息】\n`;
prompt += `- 姓名:${name || '未知客户'}\n`;
if (tagNames) {
prompt += `- 标签:${tagNames}\n`;
}
if (tagCategories) {
prompt += `- 标签分类:${tagCategories}\n`;
}
// 添加基于标签的个性化内容
const hasXiaohongshu = tags.some(t => t.TagName.includes('小红书'));
const hasYunying = tags.some(t => t.TagCategory === '运营人');
if (hasXiaohongshu) {
prompt += `- 渠道:小红书用户,对性价比比较关注\n`;
}
if (hasYunying) {
const yunyingTags = tags.filter(t => t.TagCategory === '运营人');
if (yunyingTags.length > 0) {
prompt += `- 运营人:${yunyingTags[0].TagName}\n`;
}
}
// 2. 历史会话摘要
prompt += `\n【历史会话摘要】(最近${messageHistory.length}条)\n`;
if (messageHistory.length > 0) {
// 按时间倒序排列(最新的在前)
const sortedHistory = [...messageHistory].sort((a, b) =>
new Date(b.msgtime) - new Date(a.msgtime)
).slice(0, 20); // 确保最多20条
sortedHistory.forEach((msg, index) => {
const time = new Date(msg.msgtime).toLocaleString('zh-CN');
const role = msg.from_user === externalUserId ? '客户' : '客服';
const content = msg.text?.content || msg.content || '无文本内容';
// 简化内容,避免过长
const simplifiedContent = content.length > 50
? content.substring(0, 50) + '...'
: content;
prompt += `${index + 1}. 时间:${time},角色:${role},内容:${simplifiedContent}\n`;
});
// 3. 对话上下文分析
prompt += `\n【对话上下文分析】\n`;
// 分析客户消息
const customerMessages = sortedHistory.filter(msg =>
msg.from_user === externalUserId
);
if (customerMessages.length > 0) {
// 提取关键词
const allContent = customerMessages.map(msg =>
msg.text?.content || msg.content || ''
).join(' ');
// 简单分析(实际可以更复杂)
prompt += `- 客户主要关注:${customerService.analyzeMainTopics(allContent)}\n`;
prompt += `- 消息频率:${customerMessages.length}条历史消息\n`;
// 检查是否有未解决的问题
const hasQuestions = customerMessages.some(msg =>
(msg.text?.content || '').includes('?') ||
(msg.text?.content || '').includes('?')
);
prompt += `- 未解决问题:${hasQuestions ? '有' : '暂无明确未解决问题'}\n`;
}
} else {
prompt += `暂无历史会话记录\n`;
}
// 4. 生成要求
prompt += `\n【生成要求】\n`;
prompt += `基于以上客户信息和历史会话,生成一段亲切、专业的回复。要求:\n`;
prompt += `1. 如果历史会话中有未解决的问题,优先回应\n`;
prompt += `2. 保持对话的连贯性和上下文一致性\n`;
prompt += `3. 体现对客户历史需求的了解和关注\n`;
prompt += `4. 根据客户标签提供个性化服务建议\n`;
prompt += `5. 语气亲切但不失专业,体现兜爸心选的品牌形象\n`;
return prompt;
},
/**
* 简单的话题分析函数
* @param {string} text 文本内容
* @returns {string} 分析出的主要话题
*/
analyzeMainTopics: (text) => {
const topics = [];
const lowerText = text.toLowerCase();
if (lowerText.includes('价格') || lowerText.includes('多少钱')) {
topics.push('价格咨询');
}
if (lowerText.includes('怎么用') || lowerText.includes('使用方法')) {
topics.push('使用指导');
}
if (lowerText.includes('发货') || lowerText.includes('物流')) {
topics.push('物流查询');
}
if (lowerText.includes('质量') || lowerText.includes('品质')) {
topics.push('质量咨询');
}
if (lowerText.includes('售后') || lowerText.includes('退换')) {
topics.push('售后服务');
}
return topics.length > 0 ? topics.join('、') : '一般咨询';
},
/**
* 格式化历史会话用于显示
* @param {Array} messageHistory 消息历史数据
* @param {string} externalUserId 客户ID(用于识别客户消息)
* @param {number} maxMessages 最大显示消息数
* @returns {string} 格式化后的历史会话文本
*/
formatHistoryForDisplay: (messageHistory = [], externalUserId = '', maxMessages = 5) => {
if (!messageHistory || messageHistory.length === 0) {
return '📜 暂无历史会话记录';
}
// 按时间倒序排列(最新的在前)
const sortedHistory = [...messageHistory].sort((a, b) =>
new Date(b.msgtime) - new Date(a.msgtime)
).slice(0, maxMessages);
let formattedText = `📜 最近对话记录(${sortedHistory.length}条):\n\n`;
sortedHistory.forEach((msg) => {
// 确定发送者角色
const role = msg.from_user === externalUserId ? '客户' : '客服';
// 获取消息内容
const content = msg.text?.content || msg.content || '无文本内容';
// 简化内容,避免过长
const simplifiedContent = content.length > 80
? content.substring(0, 80) + '...'
: content;
// 使用点状列表格式,弱化时间显示
formattedText += `• ${role}:${simplifiedContent}\n`;
});
return formattedText;
}
};
/**
* 消息服务
*/
export const messageService = {
/**
* 获取用户消息历史
* @param {string} userId 客户ID
* @param {number} page 页码
* @param {number} pageSize 每页数量
* @returns {Promise} 消息历史数据
*/
getUserMessageHistory: (userId, page = 1, pageSize = 20) => executeWithRetry(async () => {
console.log(`获取用户 ${userId} 的消息历史,页码: ${page}, 每页: ${pageSize}...`);
const response = await apiClient.get('/messages', {
params: {
from_user: userId,
page: page,
page_size: pageSize
}
});
if (!response.data.success) {
throw new Error(`获取消息历史失败: ${response.data.message || '未知错误'}`);
}
console.log('消息历史获取成功:', {
total: response.data.data?.total || 0,
messagesCount: response.data.data?.messages?.length || 0
});
return response.data.data;
}, 3, (retryCount) => {
console.log(`第${retryCount}次重试获取消息历史...`);
})
};
/**
* 大语言模型服务
*/
export const glmService = {
/**
* 发送消息到大语言模型
* @param {string} message 消息内容
* @param {string} conversationId 会话ID(可选)
* @returns {Promise} 模型回复
*/
sendMessage: (message, conversationId = null) => executeWithRetry(async () => {
console.log('发送消息到大语言模型...', { message, conversationId });
const payload = {
message,
conversation_id: conversationId
};
const response = await apiClient.post('/glm/chat', payload);
if (!response.data || !response.data.response) {
throw new Error('大语言模型返回格式错误');
}
console.log('大语言模型回复成功:', response.data);
return response.data;
}, 3, (retryCount) => {
console.log(`第${retryCount}次重试发送消息到大语言模型...`);
}),
/**
* 基于客户标签生成智能回复
* @param {Object} customerInfo 客户信息(包含标签)
* @returns {Promise} 生成的回复
*/
generateReplyFromTags: async (customerInfo) => {
const prompt = customerService.generatePromptFromTags(customerInfo);
console.log('生成的提示词:', prompt);
const result = await glmService.sendMessage(prompt);
return result.response;
},
/**
* 基于客户标签和历史会话生成智能回复
* @param {Object} customerInfo 客户信息(包含标签)
* @param {Array} messageHistory 消息历史数据
* @returns {Promise} 生成的回复
*/
generateReplyFromTagsAndHistory: async (customerInfo, messageHistory = []) => {
const prompt = customerService.generatePromptFromTagsAndHistory(customerInfo, messageHistory);
console.log('生成的提示词(包含历史):', prompt);
const result = await glmService.sendMessage(prompt);
return result.response;
}
};
/**
* 工作流服务 - 整合所有步骤
*/
export const workflowService = {
/**
* 执行工作流但不发送消息(只生成回复)
* @param {Object} weComHook 企业微信 Hook 实例
* @param {Function} onProgress 进度回调
* @returns {Promise} 工作流结果(包含生成的回复)
*/
executeWorkflowWithoutSend: async (weComHook, onProgress = null) => {
const results = {};
try {
// 1. 获取认证信息
if (onProgress) onProgress('auth', 'loading');
const [tokenResult, ticketResult] = await Promise.all([
authService.getAccessToken(),
authService.getJsapiTicket()
]);
results.auth = { token: tokenResult, ticket: ticketResult };
if (onProgress) onProgress('auth', 'success', results.auth);
// 2. 注册企业微信 JSSDK(同步调用)
if (onProgress) onProgress('register', 'loading');
let registerResult;
try {
const registered = weComHook.registerSdk();
registerResult = {
registered,
message: registered ? 'JSSDK注册调用成功' : 'JSSDK注册调用失败'
};
// 如果注册失败,抛出错误停止工作流
if (!registered) {
throw new Error('JSSDK注册失败,无法继续执行需要JSSDK的步骤');
}
} catch (error) {
console.error('JSSDK注册调用时发生错误:', error);
registerResult = {
registered: false,
error: error.message,
message: `注册调用失败: ${error.message}`
};
// 抛出错误停止工作流
throw new Error(`JSSDK注册失败: ${error.message}`);
}
results.register = registerResult;
if (onProgress) onProgress('register', 'success', registerResult);
// 3. 获取客户ID
if (onProgress) onProgress('customer', 'loading');
console.log('开始获取客户ID步骤...');
let customerResult;
try {
// 首先获取页面场景,检查是否在支持获取客户ID的页面
let contextInfo = null;
try {
contextInfo = await weComHook.getContext();
console.log('当前页面场景:', contextInfo);
// 检查是否获取到有效的页面场景数据
if (!contextInfo || contextInfo.entry === 'unknown') {
const errorMessage = `无法获取有效的页面场景信息。可能的原因:\n1. 不在企业微信环境中运行\n2. JSSDK未正确注册\n3. 当前页面不支持获取场景信息\n\n请确保在企业微信中打开此应用,并刷新页面重试。`;
console.error(errorMessage);
throw new Error(errorMessage);
}
// 检查 entry 是否有效
if (!contextInfo.isValidEntry) {
const errorMessage = `当前页面不支持获取客户ID。getCurExternalContact() 接口只在以下页面可用:\n• 外部联系人详情页面 (contact_profile)\n• 单聊工具栏页面 (single_chat_tools)\n• 单聊客服工具栏页面 (single_kf_tools)\n\n当前页面: ${contextInfo.entry}\n请在企业微信外部联系人聊天页面中打开此应用。`;
console.error(errorMessage);
throw new Error(errorMessage);
}
console.log('当前页面支持获取客户ID,entry:', contextInfo.entry);
// 在支持获取客户ID的页面,尝试获取真实客户ID
customerResult = await weComHook.getCurExternalContact()
.then(customerInfo => {
console.log('成功获取到客户ID:', customerInfo);
return {
...customerInfo,
context: contextInfo,
mock: false
};
})
.catch(error => {
console.error('获取客户ID失败:', error);
throw error; // 重新抛出错误,停止工作流
});
} catch (contextError) {
console.error('获取页面场景失败或页面不支持获取客户ID:', contextError);
throw contextError; // 重新抛出错误,停止工作流
}
} catch (error) {
console.error('获取客户ID步骤失败:', error);
// 不再返回模拟数据,而是抛出错误停止工作流
throw new Error(`获取客户ID失败: ${error.message}`);
}
results.customer = customerResult;
if (onProgress) onProgress('customer', 'success', customerResult);
// 4. 获取客户标签
if (onProgress) onProgress('tags', 'loading');
let tagsResult;
if (!customerResult.externalUserId || customerResult.mock) {
console.warn('使用模拟标签数据');
tagsResult = {
name: '模拟客户',
tags: [
{ TagName: '小红书用户', TagCategory: '渠道' },
{ TagName: '高意向客户', TagCategory: '意向度' },
{ TagName: '已购买用户', TagCategory: '客户状态' }
],
mock: true,
message: '使用模拟标签数据'
};
} else {
try {
tagsResult = await customerService.getCustomerTags(customerResult.externalUserId);
} catch (error) {
console.warn('获取客户标签失败,使用模拟数据:', error);
tagsResult = {
name: '客户(标签获取失败)',
tags: [{ TagName: '兜爸心选客户', TagCategory: '默认' }],
mock: true,
error: error.message,
message: '标签获取失败,使用模拟数据'
};
}
}
results.tags = tagsResult;
if (onProgress) onProgress('tags', 'success', tagsResult);
// 5. 获取会话历史
if (onProgress) onProgress('history', 'loading');
let historyResult;
if (!customerResult.externalUserId || customerResult.mock) {
console.warn('使用模拟历史数据');
historyResult = {
messages: [
{
msgid: 'mock_msg_001',
from_user: customerResult.externalUserId || 'mock_customer',
text: { content: '你好,我想了解一下你们的产品' },
msgtime: new Date(Date.now() - 86400000).toISOString(),
msgtype: 'text'
},
{
msgid: 'mock_msg_002',
from_user: '客服',
text: { content: '您好!欢迎咨询兜爸心选产品,请问您想了解哪方面的信息?' },
msgtime: new Date(Date.now() - 86300000).toISOString(),
msgtype: 'text'
}
],
total: 2,
mock: true,
message: '使用模拟历史数据'
};
} else {
try {
historyResult = await messageService.getUserMessageHistory(customerResult.externalUserId, 1, 20);
} catch (error) {
console.warn('获取会话历史失败,使用模拟数据:', error);
historyResult = {
messages: [],
total: 0,
mock: true,
error: error.message,
message: '历史获取失败,使用模拟数据'
};
}
}
results.history = historyResult;
if (onProgress) onProgress('history', 'success', historyResult);
// 6. 生成智能回复
if (onProgress) onProgress('generate', 'loading');
let generatedReply;
try {
// 合并客户信息和标签信息
const customerInfo = {
...customerResult,
...tagsResult,
externalUserId: customerResult.externalUserId
};
// 使用新的函数,包含历史数据
generatedReply = await glmService.generateReplyFromTagsAndHistory(
customerInfo,
historyResult.messages || []
);
} catch (error) {
console.warn('生成智能回复失败,使用默认回复:', error);
generatedReply = `您好!我是兜爸心选的智能客服。抱歉,生成个性化回复时遇到问题:${error.message}。请问您有什么需要帮助的吗?`;
}
results.generate = generatedReply;
if (onProgress) onProgress('generate', 'success', generatedReply);
return {
success: true,
steps: results,
generatedMessage: generatedReply,
timestamp: new Date().toISOString()
};
} catch (error) {
console.error('工作流执行失败:', error);
return {
success: false,
error: error.message,
steps: results,
timestamp: new Date().toISOString()
};
}
}
};
export default {
authService,
customerService,
glmService,
workflowService
};

View File

@@ -4,5 +4,10 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
base: '/chat_fe'
base: '/chat_fe',
server: {
port: 3010,
host: '0.0.0.0',
allowedHosts: ['mp.yizhezhe.com']
}
})

546
frontend/web/index.html Normal file
View File

@@ -0,0 +1,546 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业微信 JSSDK 测试</title>
<!-- 引入企业微信 JSSDK -->
<script src="https://wwcdn.weixin.qq.com/node/open/js/wecom-jssdk-2.3.3.js"></script>
</head>
<body>
<h1>企业微信 JSSDK 测试页面</h1>
<p>此页面用于测试企业微信 JSSDK 是否加载成功。</p>
<p>如果 JSSDK 加载成功,将会显示 SDK 版本号。</p>
<h2>Access Token(自动获取)</h2>
<div>
<p style="color: #666; font-style: italic;">页面加载时自动获取Access Token...</p>
<div id="tokenResult" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9;">
<h3>Access Token 结果:</h3>
<pre id="tokenData">获取中...</pre>
</div>
</div>
<h2>JSAPI Ticket(自动获取)</h2>
<div>
<p style="color: #666; font-style: italic;">页面加载时自动获取JSAPI Ticket...</p>
<div id="ticketResult" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9;">
<h3>JSAPI Ticket 结果:</h3>
<pre id="ticketData">获取中...</pre>
</div>
</div>
<h2>获取页面场景</h2>
<div>
<button id="getContextBtn">获取页面场景</button>
<div id="contextResult" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; display: none;">
<h3>页面场景结果:</h3>
<pre id="contextData"></pre>
</div>
</div>
<h2>获取当前客户ID</h2>
<div>
<button id="getCustomerBtn">获取当前客户ID</button>
<div id="customerResult" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; display: none;">
<h3>当前客户ID结果:</h3>
<pre id="customerData"></pre>
</div>
</div>
<h2>发送消息到当前会话</h2>
<div>
<div style="margin-bottom: 15px;">
<label for="messageContent" style="display: block; margin-bottom: 5px; font-weight: bold;">消息内容:</label>
<textarea id="messageContent" rows="4" style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 5px; font-family: monospace;" placeholder="请输入要发送的消息内容...">你好,这是一条测试消息!</textarea>
</div>
<button id="sendMessageBtn">发送消息到当前会话</button>
<div id="messageResult" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; display: none;">
<h3>消息发送结果:</h3>
<pre id="messageData"></pre>
</div>
</div>
<!-- 测试代码 -->
<script>
// 函数定义
async function getConfigSignature(url) {
// 根据 url 生成企业签名
// 生成方法参考 https://developer.work.weixin.qq.com/document/14924
// 暂时返回简单实现,主要关注应用签名
const timestamp = Math.floor(Date.now() / 1000);
const nonceStr = Math.random().toString(36).substring(2, 15);
return { timestamp, nonceStr, signature: 'temp_signature' };
}
async function getAgentConfigSignature(url) {
// 根据 url 生成应用签名
// 注意:企业微信要求url必须是调用JSAPI页面的完整URL,不包含#及其后面部分
try {
// 清理URL,移除#及其后面的部分
const cleanUrl = url.split('#')[0];
console.log('生成签名,原始URL:', url, '清理后URL:', cleanUrl);
// 使用线上地址
const apiBaseUrl = 'http://mp.yizhezhe.com';
const apiUrl = `${apiBaseUrl}/api/auth/signature`;
console.log('调用签名API:', apiUrl);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: cleanUrl,
timestamp: Math.floor(Date.now() / 1000),
nonceStr: Math.random().toString(36).substring(2, 15)
})
});
const result = await response.json();
if (result.success) {
console.log('签名生成成功:', result.data);
console.log('签名详情:', {
url: cleanUrl,
timestamp: result.data.timestamp,
nonceStr: result.data.nonceStr,
signature: result.data.signature
});
return result.data;
} else {
console.error('签名生成失败:', result.message);
throw new Error(result.message);
}
} catch (error) {
console.error('调用签名API失败:', error);
// 返回一个简单的签名用于测试
const timestamp = Math.floor(Date.now() / 1000);
const nonceStr = Math.random().toString(36).substring(2, 15);
return { timestamp, nonceStr, signature: 'error_signature' };
}
}
// 自动获取 Access Token 函数
async function autoFetchAccessToken() {
console.log('开始自动获取 Access Token...');
const tokenDataElement = document.getElementById('tokenData');
const tokenResultElement = document.getElementById('tokenResult');
try {
// 更新状态
tokenDataElement.textContent = '正在获取...';
tokenDataElement.style.color = '#666';
// 获取 access_token 的 API 端点
const apiUrl = 'http://mp.yizhezhe.com/api/auth/token';
console.log('正在调用 API:', apiUrl);
// 使用 fetch 调用 API
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP 错误: ${response.status}`);
}
const data = await response.json();
console.log('Access Token 获取成功:', data);
// 更新页面显示
if (data.success === true) {
tokenDataElement.textContent = JSON.stringify(data, null, 2);
tokenDataElement.style.color = '#333';
console.log(`Access Token 获取成功!${data.message}`);
console.log('Access Token 数据:', JSON.stringify(data, null, 2));
// 添加成功样式
tokenResultElement.style.borderColor = '#4CAF50';
tokenResultElement.style.backgroundColor = '#f8fff8';
} else {
tokenDataElement.textContent = `获取失败: ${data.message}\n\n返回数据: ${JSON.stringify(data, null, 2)}`;
tokenDataElement.style.color = '#d32f2f';
console.error(`获取 Access Token 失败: ${data.message}`);
// 添加错误样式
tokenResultElement.style.borderColor = '#f44336';
tokenResultElement.style.backgroundColor = '#fff8f8';
}
return data;
} catch (error) {
console.error('获取 Access Token 失败:', error);
console.error('错误详情:', error.message);
// 更新页面显示错误
tokenDataElement.textContent = `错误: ${error.message}\n\n请检查网络连接或后端服务状态。`;
tokenDataElement.style.color = '#d32f2f';
// 添加错误样式
tokenResultElement.style.borderColor = '#f44336';
tokenResultElement.style.backgroundColor = '#fff8f8';
return null;
}
}
// 自动获取 JSAPI Ticket 函数
async function autoFetchJsapiTicket() {
console.log('开始自动获取 JSAPI Ticket...');
const ticketDataElement = document.getElementById('ticketData');
const ticketResultElement = document.getElementById('ticketResult');
try {
// 更新状态
ticketDataElement.textContent = '正在获取...';
ticketDataElement.style.color = '#666';
// 获取 jsapi_ticket 的 API 端点 - 使用线上地址
const apiUrl = 'http://mp.yizhezhe.com/api/auth/ticket';
console.log('正在调用 API:', apiUrl);
// 使用 fetch 调用 API
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP 错误: ${response.status}`);
}
const data = await response.json();
console.log('JSAPI Ticket 获取成功:', data);
// 更新页面显示
if (data.success === true) {
ticketDataElement.textContent = JSON.stringify(data, null, 2);
ticketDataElement.style.color = '#333';
console.log(`JSAPI Ticket 获取成功!${data.message}`);
console.log('JSAPI Ticket 数据:', JSON.stringify(data, null, 2));
// 添加成功样式
ticketResultElement.style.borderColor = '#4CAF50';
ticketResultElement.style.backgroundColor = '#f8fff8';
} else {
ticketDataElement.textContent = `获取失败: ${data.message}\n\n返回数据: ${JSON.stringify(data, null, 2)}`;
ticketDataElement.style.color = '#d32f2f';
console.error(`获取 JSAPI Ticket 失败: ${data.message}`);
// 添加错误样式
ticketResultElement.style.borderColor = '#f44336';
ticketResultElement.style.backgroundColor = '#fff8f8';
}
return data;
} catch (error) {
console.error('获取 JSAPI Ticket 失败:', error);
console.error('错误详情:', error.message);
// 更新页面显示错误
ticketDataElement.textContent = `错误: ${error.message}\n\n请检查网络连接或后端服务状态。`;
ticketDataElement.style.color = '#d32f2f';
// 添加错误样式
ticketResultElement.style.borderColor = '#f44336';
ticketResultElement.style.backgroundColor = '#fff8f8';
return null;
}
}
// DOMContentLoaded 事件处理
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded: 开始初始化页面...');
// 1. 检查并注册 JSSDK
console.log('检查 JSSDK...');
if (typeof ww !== 'undefined') {
console.log('企业微信 JSSDK 版本:', ww.SDK_VERSION);
console.log('JSSDK 对象已加载:', ww);
// 注册 JSSDK(不等待回调,让 JSSDK 自行处理)
console.log('开始注册 JSSDK...');
ww.register({
corpId: 'ww45bb49a9d651b2bb', // 必填,当前用户企业所属企业ID,从.env文件获取
agentId: 1000061, // 必填,当前应用的AgentID,从.env文件获取
jsApiList: ['getContext', 'getCurExternalContact', 'sendChatMessage'], // 必填,需要使用的JSAPI列表
getConfigSignature, // 必填,根据url生成企业签名的回调函数
getAgentConfigSignature // 必填,根据url生成应用签名的回调函数
// 不设置 success 和 fail 回调,让按钮直接调用接口
});
console.log('JSSDK 注册调用完成');
// 2. 自动获取 Access Token 和 JSAPI Ticket
console.log('开始自动获取认证信息...');
autoFetchAccessToken();
autoFetchJsapiTicket();
} else {
console.error('错误: 企业微信 JSSDK 未加载成功!');
console.error('请检查网络或刷新页面。');
}
// 获取页面场景功能
const getContextBtn = document.getElementById('getContextBtn');
const contextResult = document.getElementById('contextResult');
const contextData = document.getElementById('contextData');
if (getContextBtn) {
console.log('找到获取页面场景按钮,绑定点击事件');
// 初始时直接启用按钮
getContextBtn.disabled = false;
getContextBtn.textContent = '获取页面场景';
getContextBtn.addEventListener('click', function() {
console.log('点击获取页面场景按钮');
// 直接调用,不检查注册状态
getContextBtn.disabled = true;
getContextBtn.textContent = '获取中...';
console.log('直接调用 ww.getContext()...');
console.log('检查 ww 对象:', typeof ww);
console.log('检查 ww.getContext:', typeof ww.getContext);
if (typeof ww === 'undefined') {
alert('错误: ww 对象未定义,JSSDK 未加载成功!');
getContextBtn.disabled = false;
getContextBtn.textContent = '获取页面场景';
return;
}
if (typeof ww.getContext !== 'function') {
alert('错误: ww.getContext 不是函数,请检查 jsApiList 是否包含 getContext');
getContextBtn.disabled = false;
getContextBtn.textContent = '获取页面场景';
return;
}
// 调用企业微信 JSSDK 的 getContext 方法
ww.getContext({
success: function(res) {
console.log('获取页面场景成功:', res);
// 显示结果
contextData.textContent = JSON.stringify(res, null, 2);
contextResult.style.display = 'block';
// 解析场景信息
let sceneInfo = '未知场景';
if (res.entry) {
sceneInfo = `入口: ${res.entry}`;
}
if (res.scene) {
sceneInfo += `, 场景: ${res.scene}`;
}
if (res.chatId) {
sceneInfo += `, 聊天ID: ${res.chatId}`;
}
alert(`获取页面场景成功!\n${sceneInfo}`);
},
fail: function(err) {
console.error('获取页面场景失败:', err);
contextData.textContent = `错误: ${JSON.stringify(err, null, 2)}`;
contextResult.style.display = 'block';
alert('获取页面场景失败: ' + (err.errMsg || '未知错误'));
},
complete: function() {
console.log('getContext 调用完成');
getContextBtn.disabled = false;
getContextBtn.textContent = '获取页面场景';
}
});
});
} else {
console.error('未找到获取页面场景按钮!');
}
// 获取当前客户ID功能
const getCustomerBtn = document.getElementById('getCustomerBtn');
const customerResult = document.getElementById('customerResult');
const customerData = document.getElementById('customerData');
if (getCustomerBtn) {
console.log('找到获取当前客户ID按钮,绑定点击事件');
// 初始时直接启用按钮
getCustomerBtn.disabled = false;
getCustomerBtn.textContent = '获取当前客户ID';
getCustomerBtn.addEventListener('click', function() {
console.log('点击获取当前客户ID按钮');
// 直接调用,不检查注册状态
getCustomerBtn.disabled = true;
getCustomerBtn.textContent = '获取中...';
console.log('准备调用 ww.getCurExternalContact()...');
console.log('检查 ww 对象:', typeof ww);
console.log('检查 ww.getCurExternalContact:', typeof ww.getCurExternalContact);
if (typeof ww === 'undefined') {
alert('错误: ww 对象未定义,JSSDK 未加载成功!');
getCustomerBtn.disabled = false;
getCustomerBtn.textContent = '获取当前客户ID';
return;
}
if (typeof ww.getCurExternalContact !== 'function') {
alert('错误: ww.getCurExternalContact 不是函数,请检查 jsApiList 是否包含 getCurExternalContact');
getCustomerBtn.disabled = false;
getCustomerBtn.textContent = '获取当前客户ID';
return;
}
// 调用企业微信 JSSDK 的 getCurExternalContact 方法
ww.getCurExternalContact({
success: function(res) {
console.log('获取当前客户ID成功:', res);
// 显示结果
customerData.textContent = JSON.stringify(res, null, 2);
customerResult.style.display = 'block';
// 解析客户信息
let customerInfo = '未知客户';
if (res.externalUserId) {
customerInfo = `客户ID: ${res.externalUserId}`;
}
if (res.nickname) {
customerInfo += `, 昵称: ${res.nickname}`;
}
if (res.avatar) {
customerInfo += `, 有头像`;
}
if (res.corpName) {
customerInfo += `, 企业: ${res.corpName}`;
}
alert(`获取当前客户ID成功!\n${customerInfo}`);
},
fail: function(err) {
console.error('获取当前客户ID失败:', err);
customerData.textContent = `错误: ${JSON.stringify(err, null, 2)}`;
customerResult.style.display = 'block';
alert('获取当前客户ID失败: ' + (err.errMsg || '未知错误'));
},
cancel: function() {
console.log('用户取消了获取当前客户ID操作');
alert('用户取消了获取当前客户ID操作');
getCustomerBtn.disabled = false;
getCustomerBtn.textContent = '获取当前客户ID';
},
complete: function() {
console.log('getCurExternalContact 调用完成');
getCustomerBtn.disabled = false;
getCustomerBtn.textContent = '获取当前客户ID';
}
});
});
} else {
console.error('未找到获取当前客户ID按钮!');
}
// 发送消息到当前会话功能
const sendMessageBtn = document.getElementById('sendMessageBtn');
const messageResult = document.getElementById('messageResult');
const messageData = document.getElementById('messageData');
const messageContent = document.getElementById('messageContent');
if (sendMessageBtn) {
console.log('找到发送消息按钮,绑定点击事件');
// 初始时直接启用按钮
sendMessageBtn.disabled = false;
sendMessageBtn.textContent = '发送消息到当前会话';
sendMessageBtn.addEventListener('click', function() {
console.log('点击发送消息按钮');
// 获取消息内容
const content = messageContent.value.trim();
if (!content) {
alert('错误: 消息内容不能为空!');
return;
}
// 直接调用,不检查注册状态
sendMessageBtn.disabled = true;
sendMessageBtn.textContent = '发送中...';
console.log('准备调用 ww.sendChatMessage()...');
console.log('检查 ww 对象:', typeof ww);
console.log('检查 ww.sendChatMessage:', typeof ww.sendChatMessage);
console.log('消息内容:', content);
if (typeof ww === 'undefined') {
alert('错误: ww 对象未定义,JSSDK 未加载成功!');
sendMessageBtn.disabled = false;
sendMessageBtn.textContent = '发送消息到当前会话';
return;
}
if (typeof ww.sendChatMessage !== 'function') {
alert('错误: ww.sendChatMessage 不是函数,请检查 jsApiList 是否包含 sendChatMessage');
sendMessageBtn.disabled = false;
sendMessageBtn.textContent = '发送消息到当前会话';
return;
}
// 调用企业微信 JSSDK 的 sendChatMessage 方法
ww.sendChatMessage({
msgtype: 'text',
text: {
content: content
},
success: function(res) {
console.log('发送消息成功:', res);
// 显示结果
messageData.textContent = JSON.stringify(res, null, 2);
messageResult.style.display = 'block';
// 解析发送结果
let resultInfo = '消息发送成功!';
if (res.errCode === 0) {
resultInfo = `消息发送成功!\n错误码: ${res.errCode}, 错误信息: ${res.errMsg || '无'}`;
} else {
resultInfo = `消息发送失败!\n错误码: ${res.errCode}, 错误信息: ${res.errMsg || '未知错误'}`;
}
alert(resultInfo);
},
fail: function(err) {
console.error('发送消息失败:', err);
messageData.textContent = `错误: ${JSON.stringify(err, null, 2)}`;
messageResult.style.display = 'block';
alert('发送消息失败: ' + (err.errMsg || '未知错误'));
},
cancel: function() {
console.log('用户取消了发送消息操作');
alert('用户取消了发送消息操作');
sendMessageBtn.disabled = false;
sendMessageBtn.textContent = '发送消息到当前会话';
},
complete: function() {
console.log('sendChatMessage 调用完成');
sendMessageBtn.disabled = false;
sendMessageBtn.textContent = '发送消息到当前会话';
}
});
});
} else {
console.error('未找到发送消息按钮!');
}
});
</script>
<footer>
<p>企业微信 JSSDK 测试 - 开发环境</p>
</footer>
</body>
</html>