import { useEffect, useState } from 'react' import { SpinLoading, Empty, Toast } from 'antd-mobile' const CORPID = 'wwd483c2fba24ae30a' const AGENTID = '1000021' const REDIRECT_URI = encodeURIComponent('https://ai.9artedu.com/sidebar/summary.html') interface SummaryItem { id?: number msgTime?: string msgType?: number fromUser?: string toUser?: string roomId?: string messageContext?: string summary?: string createdAt?: number staffId?: string staffName?: string } interface SummarySection { title: string items: string[] } interface ParsedSummary { sections: SummarySection[] tags: string[] raw: string } function SummaryApp() { const [loading, setLoading] = useState(true) const [list, setList] = useState([]) const [error, setError] = useState('') const [needLogin, setNeedLogin] = useState(false) useEffect(() => { const init = async () => { const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') // 1. URL 中有 code → 先登录 if (code) { const cleanUrl = window.location.pathname + window.location.hash window.history.replaceState({}, document.title, cleanUrl) const ok = await tryCodeLogin(code) if (!ok) { setLoading(false) setNeedLogin(true) setError('登录失败,请重试') return } } // 2. 检查员工登录态 const staffId = urlParams.get('staffId') || localStorage.getItem('staff_user_id') || '' if (!staffId) { setLoading(false) setNeedLogin(true) setError('未获取到员工ID,请先登录') return } // 3. 加载每日总结 await loadSummary(staffId) } init() }, []) const noCacheFetch = async (url: string, options?: RequestInit) => { const sep = url.includes('?') ? '&' : '?' const nocacheUrl = `${url}${sep}_t=${Date.now()}` return fetch(nocacheUrl, { ...options, headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', ...options?.headers } }) } const tryCodeLogin = async (code: string): Promise => { try { const res = await noCacheFetch(`/api/v1/auth/login?code=${encodeURIComponent(code)}`, { method: 'POST' }) const data = await res.json() if (data.code === 0 && data.data) { const info = data.data localStorage.setItem('staff_token', info.token) localStorage.setItem('staff_user_id', info.userId) localStorage.setItem('staff_user_name', info.userName || '') localStorage.setItem('staff_avatar', info.avatar || '') localStorage.setItem('staff_role', info.role || 'SEAT') console.log('OAuth 登录成功:', info.userId, info.userName) return true } console.error('OAuth login failed:', data.message) } catch (e) { console.error('Code login error:', e) } return false } const redirectToWeComOAuth = () => { const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${CORPID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=snsapi_base&agentid=${AGENTID}&state=summary#wechat_redirect` console.log('跳转企微 OAuth:', oauthUrl) window.location.href = oauthUrl } const initWxConfigAndGetCustomerId = (): Promise => { return new Promise((resolve) => { const wx = (window as any).wx if (!wx || !wx.config) { console.log('非企微环境,跳过 JS-SDK') resolve('') return } const currentUrl = window.location.href.split('#')[0] noCacheFetch(`/api/v1/auth/signature?url=${encodeURIComponent(currentUrl)}`) .then(res => res.json()) .then(corpData => { if (corpData.code !== 0 || !corpData.data) { resolve('') return } const cfg = corpData.data wx.config({ beta: true, debug: false, appId: cfg.corpId, timestamp: cfg.timestamp, nonceStr: cfg.nonceStr, signature: cfg.signature, jsApiList: ['agentConfig', 'getContext', 'getCurExternalContact'] }) wx.ready(() => { noCacheFetch(`/api/v1/auth/signature?type=agent&agentId=${AGENTID}&url=${encodeURIComponent(currentUrl)}`) .then(res => res.json()) .then(agentData => { if (agentData.code !== 0 || !agentData.data) { resolve('') return } const ac = agentData.data wx.agentConfig({ corpid: ac.corpId, agentid: ac.agentId, timestamp: ac.timestamp, nonceStr: ac.nonceStr, signature: ac.signature, jsApiList: ['getContext', 'getCurExternalContact'], success: () => { wx.invoke('getCurExternalContact', {}, (res: any) => { if (res.err_msg === 'getCurExternalContact:ok' && res.userId) { console.log('JS-SDK 获取客户ID:', res.userId) resolve(res.userId) } else { console.warn('getCurExternalContact failed:', res) resolve('') } }) }, fail: (err: any) => { console.warn('agentConfig fail:', err) resolve('') } }) }) .catch((e: any) => { console.error('agentConfig fetch error:', e) resolve('') }) }) wx.error((err: any) => { console.error('wx.config error:', err) resolve('') }) }) .catch((e: any) => { console.error('initWxConfig error:', e) resolve('') }) }) } const loadSummary = async (staffId: string) => { const urlParams = new URLSearchParams(window.location.search) let customerId = urlParams.get('customerId') || '' // 优先通过企微 JS-SDK 获取当前客户ID const sdkCustomerId = await initWxConfigAndGetCustomerId() if (sdkCustomerId) { customerId = sdkCustomerId localStorage.setItem('current_customer_id', sdkCustomerId) } else { customerId = customerId || localStorage.getItem('current_customer_id') || '' } if (!customerId) { setLoading(false) setError('缺少客户ID参数') Toast.show({ content: '缺少客户ID参数', icon: 'fail' }) return } try { const res = await fetch( `/api/v1/archive/daily-summary/all?customerId=${encodeURIComponent(customerId)}&staffId=${encodeURIComponent(staffId)}&_t=${Date.now()}`, { headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' } } ) const data = await res.json() if (data.code === 0 && Array.isArray(data.data)) { setList(data.data) } else { const msg = data.message || '加载失败' setError(msg) Toast.show({ content: msg, icon: 'fail' }) } } catch (e) { console.error('加载每日总结失败:', e) setError('网络错误') Toast.show({ content: '网络错误', icon: 'fail' }) } finally { setLoading(false) } } if (loading) { return (
加载每日总结中...
) } if (needLogin) { return (
{error || '未获取到员工ID,请先登录'}
) } if (error) { return (
{error}
) } if (list.length === 0) { return (
) } return (
📋 每日总结
共 {list.length} 条 · 按时间倒序
{list.map((item, index) => ( ))}
) } function SummaryCard({ item }: { item: SummaryItem }) { const parsed = parseSummary(item.summary) const hasStructure = parsed.sections.length > 0 || parsed.tags.length > 0 return (
{/* 头部 */}
{item.msgTime || formatDate(item.createdAt)}
{item.staffName || item.staffId || '未知老师'}
{hasStructure ? ( <> {parsed.sections.map((section, idx) => ( ))} {parsed.tags.length > 0 && } ) : (
{item.summary || item.messageContext || '无内容'}
)}
) } function SectionBlock({ section }: { section: SummarySection }) { const config = getSectionConfig(section.title) const showTitle = displaySectionTitle(section.title) return (
{config.icon} {showTitle}
{section.items.map((text, idx) => (
{section.title.includes('核心诉求') ? ( ) : section.title.includes('处理结果') ? ( ) : section.title.includes('待办') ? ( ) : ( )}
{text}
))}
) } function TagList({ tags }: { tags: string[] }) { return (
🏷️ 关键标签
{tags.map((tag, idx) => ( #{tag} ))}
) } function getSectionConfig(title: string) { if (title.includes('核心诉求')) { return { icon: '📝', titleColor: '#1677ff' } } if (title.includes('处理结果')) { return { icon: '✅', titleColor: '#52c41a' } } if (title.includes('待办') || title.includes('后续')) { return { icon: '⏰', titleColor: '#fa8c16' } } return { icon: '•', titleColor: '#666' } } function displaySectionTitle(title: string) { if (title.includes('核心诉求')) return '学员咨询' if (title.includes('处理结果')) return '老师解答' if (title.includes('待办')) return '跟进建议' return title } function NumberBadge({ n }: { n: number }) { return (
{n}
) } function GreenCheck() { return (
✓
) } function OrangeClock() { return (
🕐
) } function Dot({ color }: { color: string }) { return (
) } function CalendarIcon() { return ( ) } function UserIcon() { return ( ) } function parseSummary(text?: string): ParsedSummary { if (!text) return { sections: [], tags: [], raw: '' } const result: ParsedSummary = { sections: [], tags: [], raw: text } let content = text // 提取关键标签:支持 "**关键标签**:#a#b" 或 "关键标签:#a#b" 或单独行 const tagPatterns = [ /(?:\*\*)?关键标签(?:\*\*)?[::]\s*(.+?)(?=\n\s*\*\*|\n\s*$|$)/s, /(?:\*\*)?标签(?:\*\*)?[::]\s*(.+?)(?=\n\s*\*\*|\n\s*$|$)/s ] for (const pattern of tagPatterns) { const tagMatch = content.match(pattern) if (tagMatch) { result.tags = parseTags(tagMatch[1]) content = content.replace(tagMatch[0], '') break } } // 按 **标题** 分隔区块,标题可能有空格 const sectionRegex = /\*\*\s*(核心诉求|处理结果|待办事项|待处理|后续跟进|关键标签)\s*\*\*/g const parts: { title: string; text: string }[] = [] let lastIndex = 0 let lastTitle = '' let match while ((match = sectionRegex.exec(content)) !== null) { if (lastTitle) { parts.push({ title: lastTitle, text: content.slice(lastIndex, match.index) }) } lastTitle = match[1] lastIndex = match.index + match[0].length } if (lastTitle) { parts.push({ title: lastTitle, text: content.slice(lastIndex) }) } for (const part of parts) { if (part.title === '关键标签') { const tags = parseTags(part.text) if (tags.length) result.tags = tags continue } const items = parseItems(part.text) if (items.length > 0) { result.sections.push({ title: part.title, items }) } } // 没有任何结构化区块时,尝试整段按行解析列表 if (result.sections.length === 0 && result.tags.length === 0) { const items = parseItems(content) if (items.length > 1) { result.sections.push({ title: '总结', items }) } } return result } function parseItems(text: string): string[] { const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0) const items: string[] = [] for (const line of lines) { // 匹配 1. / 1. / - / * / • 开头的列表项 const match = line.match(/^(?:\d+[..]\s*|[\-*•]\s+)(.+)$/) if (match) { items.push(cleanText(match[1])) } else if (items.length > 0 && !line.match(/^\*\*/)) { // 续行(非标题) items[items.length - 1] += ' ' + cleanText(line) } } // 如果整段没有列表项,把段落本身作为一条内容(兼容纯文本区块) if (items.length === 0 && text.trim().length > 0) { items.push(cleanText(text.trim())) } return items } function parseTags(text: string): string[] { const s = text.trim() if (!s) return [] // 支持 #标签1#标签2 或 标签1#标签2 或 #标签1 #标签2 return s.split(/[#\s]+/) .map(t => t.trim()) .filter(t => t.length > 0 && t !== '、' && t !== ',') } function cleanText(s: string): string { return s.replace(/\*\*/g, '').trim() } function formatDate(timestamp?: number) { if (!timestamp) return '-' const date = new Date(timestamp) return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` } export default SummaryApp