fix: 恢复 rsync 误删的近期开发(agents/skills/mcp/knowledge/workflow/tenant/billing/storage/privacy/promotion/achievement/automation/code-exec/developer/distribution/im-bot/portal/casdoor 等后端模块 + 对应前端页面路由, 补回 main.py 挂载与 App.tsx 路由
This commit is contained in:
1 parent
d95b847fee
commit
e8e5ffa1a9
65 files changed
+35624
-339
No files matched your search
@@ -0,0 +1,228 @@
|
||||
// @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;
|
||||
@@ -0,0 +1,97 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { imUserAPI } from '@/api';
|
||||
|
||||
const WechatHistory: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [messages, setMessages] = useState<any[]>([]);
|
||||
const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
|
||||
setToast({ msg, type });
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
};
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await imUserAPI.history();
|
||||
if ((res as any)?.success && (res as any).data) {
|
||||
setMessages((res as any).data.messages || []);
|
||||
} else {
|
||||
showToast('加载失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('加载失败', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
|
||||
|
||||
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={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: 700, margin: 0, color: '#1A1F36' }}>微信对话记录</h1>
|
||||
<p style={{ color: '#6B7280', fontSize: '14px', margin: '6px 0 0 0' }}>
|
||||
你通过个人微信与七虎AI 的对话历史,按时间顺序记录,可随时回看(仅本人可见,按账号隔离)。
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={load} disabled={loading} style={btnStyle}>{loading ? '刷新中…' : '刷新'}</button>
|
||||
</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={{ background: '#F8F9FC', borderRadius: '12px', padding: '16px', minHeight: '420px' }}>
|
||||
{messages.length === 0 ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '380px', color: '#9CA3AF', fontSize: '14px', textAlign: 'center', padding: '0 24px' }}>
|
||||
{loading ? '加载中…' : '暂无对话记录。请先在「微信机器人」中绑定并发送消息,对话记录会显示在这里。'}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((m, i) => (
|
||||
<div key={i} style={{ display: 'flex', justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start', marginBottom: '14px' }}>
|
||||
<div style={{ maxWidth: '78%' }}>
|
||||
<div style={{ fontSize: '11px', color: '#9CA3AF', marginBottom: '4px', textAlign: m.role === 'user' ? 'right' : 'left' }}>
|
||||
{m.role === 'user' ? '我(微信)' : '七虎AI'} · {formatTime(m.created_at)}
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '10px 14px', borderRadius: '12px', fontSize: '14px', lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
||||
background: m.role === 'user' ? '#6C63FF' : '#FFFFFF',
|
||||
color: m.role === 'user' ? '#fff' : '#1A1F36',
|
||||
border: m.role === 'user' ? 'none' : '1px solid #E5E7EB',
|
||||
}}>{m.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const btnStyle: React.CSSProperties = {
|
||||
padding: '8px 20px', background: '#6C63FF', border: 'none', borderRadius: '6px',
|
||||
color: '#fff', fontSize: '14px', fontWeight: 500, cursor: 'pointer',
|
||||
};
|
||||
|
||||
export default WechatHistory;
|
||||
Reference in new issue
Block a user