Files
qihuaiflow/frontend/src/pages/front/WechatBot.tsx
T

229 lines
11 KiB
TypeScript
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.
// @ts-nocheck
import React, { useState, useEffect, useCallback } from 'react';
import { imUserAPI } from '@/api';
const WechatBot: React.FC = () => {
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<any>({ connected: false, configured: false });
const [qrcode, setQrcode] = useState<any>(null);
const [polling, setPolling] = useState(false);
const [reconnecting, setReconnecting] = useState(false);
const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 3000);
};
// 加载当前用户的连接状态
const loadStatus = useCallback(async () => {
try {
const res = await imUserAPI.getStatus();
if ((res as any)?.success) setStatus(res.data || { connected: false, configured: false });
} catch (e) {
// 静默失败
}
}, []);
useEffect(() => {
loadStatus();
}, [loadStatus]);
// 获取扫码二维码(iLink 协议免 AppID)
const handleQrcode = async () => {
setLoading(true);
try {
const res = await imUserAPI.qrcode();
if ((res as any)?.success && (res as any).data) {
setQrcode((res as any).data);
setPolling(true);
showToast('二维码已生成,请用手机微信扫码');
} else {
showToast((res as any).error || '获取二维码失败', 'error');
}
} catch (e) {
showToast('获取二维码失败', 'error');
} finally {
setLoading(false);
}
};
// 轮询扫码状态
const handlePoll = useCallback(async () => {
if (!qrcode?.qrcode) return;
try {
const res = await imUserAPI.qrcodeStatus(qrcode.qrcode);
if ((res as any)?.success && (res as any).data) {
const d = (res as any).data;
if (d.status === 'confirmed' || d.connected) {
setPolling(false);
setReconnecting(false);
setStatus({ ...status, connected: true, configured: true });
showToast('个人微信已连接!');
loadStatus();
} else if (d.status === 'expired') {
setPolling(false);
showToast('二维码已过期,请重新获取', 'error');
}
}
} catch (e) {
// 忽略轮询错误
}
}, [qrcode, status, loadStatus]);
useEffect(() => {
if (polling && qrcode?.qrcode) {
const t = setInterval(() => handlePoll(), 3000);
return () => clearInterval(t);
}
}, [polling, qrcode, handlePoll]);
// 断开连接
const handleDisconnect = async () => {
try {
await imUserAPI.disconnect();
setPolling(false);
setQrcode(null);
setReconnecting(false);
setStatus({ connected: false, configured: false });
showToast('已断开个人微信连接');
loadStatus();
} catch (e) {
showToast('断开失败', 'error');
}
};
const formatTime = (t: string) => (t ? new Date(t).toLocaleString('zh-CN') : '-');
return (
<div style={{ minHeight: '100%', background: '#FFFFFF', color: '#1A1F36', padding: '32px' }}>
<div style={{ maxWidth: '860px', margin: '0 auto' }}>
{/* Header */}
<div style={{ marginBottom: '24px' }}>
<h1 style={{ fontSize: '24px', fontWeight: 700, margin: 0, color: '#1A1F36' }}>我的微信机器人</h1>
<p style={{ color: '#6B7280', fontSize: '14px', margin: '6px 0 0 0' }}>
绑定你自己的个人微信,由七虎AI 大脑自动应答。基于腾讯官方 iLink 协议,扫码授权即可,无需任何开放平台凭证。
</p>
</div>
{/* Toast */}
{toast && (
<div style={{
position: 'fixed', top: '20px', right: '20px', zIndex: 9999,
padding: '12px 24px', borderRadius: '8px',
background: toast.type === 'success' ? '#10b981' : '#ef4444',
color: '#fff', fontSize: '14px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}>{toast.msg}</div>
)}
{/* 状态条 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#F8F9FC', borderRadius: '12px', padding: '16px 24px', marginBottom: '16px' }}>
<div>
<h3 style={{ margin: 0, fontSize: '16px', color: '#1A1F36' }}>个人微信(iLink 协议)</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '13px', color: '#6B7280' }}>
直接对接腾讯官方 iLink Bot HTTP API,扫码授权后由七虎AI 自动回复。
</p>
</div>
<span style={{
padding: '4px 12px', borderRadius: '4px', fontSize: '12px',
background: status.connected ? '#10b98120' : status.status === 'expired' ? '#ef444420' : '#6b728020',
color: status.connected ? '#10b981' : status.status === 'expired' ? '#ef4444' : '#6b7280',
}}>
{status.connected ? '● 已连接' : status.status === 'expired' ? '⚠ 会话已过期' : status.configured ? '○ 已配置' : '○ 未绑定'}
</span>
</div>
{/* 扫码绑定 */}
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginBottom: '16px' }}>
{status.status === 'expired' && (
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', color: '#b91c1c', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px', fontSize: '13px', lineHeight: '1.7' }}>
⚠ <b>微信会话已失效</b>:检测到与微信的连接已过期(通常是服务更新/重启后导致),消息无法再送达。请点击下方「获取扫码二维码」<b>重新扫码授权</b>即可恢复,历史对话记录仍保留。
</div>
)}
<h4 style={{ marginTop: 0, fontSize: '15px', color: '#1A1F36', marginBottom: '12px' }}>扫码授权绑定</h4>
{status.connected && !reconnecting ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ fontSize: '13px', color: '#10b981' }}>已绑定,你的个人微信正在自动收发消息并由七虎AI 应答。</div>
<div>
<button onClick={() => { setReconnecting(true); handleQrcode(); }} style={secondaryBtnStyle}>重新连接(重扫二维码)</button>
</div>
<div style={{ fontSize: '12px', color: '#9CA3AF' }}>若发现消息收不到,可点此重新扫码以重建与微信的会话。</div>
</div>
) : !qrcode ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '12px' }}>
<div style={{ fontSize: '13px', color: '#6B7280' }}>
点击「获取扫码二维码」,使用手机微信扫描二维码并确认授权,即可将你的个人微信接入七虎AI。整个过程无需申请任何开放平台凭证。
</div>
<button onClick={handleQrcode} disabled={loading} style={primaryBtnStyle}>
{loading ? '生成中…' : '获取扫码二维码'}
</button>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
{qrcode.qrcode_img ? (
<img src={qrcode.qrcode_img} alt="微信扫码" style={{ width: '220px', height: '220px', background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB' }} />
) : (
<div style={{ width: '220px', height: '220px', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB', fontSize: '12px', color: '#9CA3AF', textAlign: 'center', padding: '12px' }}>
二维码图片生成失败,<a href={qrcode.qrcode_img_content} target="_blank" rel="noreferrer" style={{color:'#6C63FF'}}>请点击此处在新标签页打开</a>。
</div>
)}
<div style={{ fontSize: '13px', color: polling ? '#f59e0b' : '#6B7280' }}>
{polling ? '等待扫码中…(每 3 秒自动检测一次)' : '已停止轮询'}
</div>
<div style={{ display: 'flex', gap: '12px' }}>
{polling && (
<button onClick={() => setPolling(false)} style={secondaryBtnStyle}>取消轮询</button>
)}
<button onClick={handleDisconnect} style={{ ...secondaryBtnStyle, color: '#ef4444', borderColor: '#ef4444' }}>断开连接</button>
</div>
</div>
)}
</div>
{/* 已连接信息 */}
{status.connected && (
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginBottom: '16px' }}>
<h4 style={{ marginTop: 0, fontSize: '15px', color: '#1A1F36', marginBottom: '12px' }}>连接信息</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<DetailItem label="绑定账号" value={status.username || '-'} />
<DetailItem label="连接时间" value={formatTime(status.connected_at)} />
<DetailItem label="iLink Bot ID" value={status.ilink_bot_id || '-'} />
<DetailItem label="Base URL" value={status.baseurl || '-'} />
</div>
</div>
)}
{/* 说明 */}
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginTop: '16px' }}>
<h4 style={{ marginTop: 0, fontSize: '14px', color: '#888' }}>接入说明</h4>
<ul style={{ fontSize: '13px', color: '#6B7280', lineHeight: '1.8', paddingLeft: '20px', margin: 0 }}>
<li>每个账号独立绑定自己的个人微信,数据严格按账号隔离,互不影响</li>
<li>个人微信通过腾讯官方 iLink Bot HTTP API(微信 ClawBot 插件)接入,消息经七虎AI 大脑生成回复</li>
<li>采用官方合法协议,无需 Hook / 逆向,封号风险远低于历史方案,但仍请遵守微信平台规范</li>
<li>消息收发基于长轮询(getupdates)+ 官方 sendmessage,回复自动关联对话窗口(context_token)</li>
</ul>
</div>
</div>
</div>
);
};
const DetailItem: React.FC<{ label: string; value: any }> = ({ label, value }) => (
<div style={{ padding: '8px 12px', background: '#FFFFFF', borderRadius: '6px' }}>
<div style={{ fontSize: '12px', color: '#666' }}>{label}</div>
<div style={{ fontSize: '14px', marginTop: '2px', wordBreak: 'break-all' }}>{value || '-'}</div>
</div>
);
const primaryBtnStyle: React.CSSProperties = {
padding: '8px 20px', background: '#6C63FF', border: 'none', borderRadius: '6px',
color: '#fff', fontSize: '14px', fontWeight: 500, cursor: 'pointer',
};
const secondaryBtnStyle: React.CSSProperties = {
padding: '8px 20px', background: '#F0F2F8', border: '1px solid #F0F2F8', borderRadius: '6px',
color: '#1A1F36', fontSize: '14px', fontWeight: 500, cursor: 'pointer',
};
export default WechatBot;