import { useCallback, useEffect, useState } from 'react' import { View, Text, ScrollView, StyleSheet, TextInput, Pressable, Image, Linking, RefreshControl, } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import { useBind } from '@/context/BindContext' import { apiFetch } from '@/lib/api' import { theme } from '@/constants/theme' type WechatConfigView = { configured: boolean proxyUrl: string apiKeyMasked: string deviceType: string proxy: string webhookHost: string webhookPort: number webhookPath: string accountId: string session: { wcId?: string; nickName?: string } } export default function WeChatScreen() { const { bind } = useBind() const [cfg, setCfg] = useState(null) const [msg, setMsg] = useState('') const [busy, setBusy] = useState(false) const [apiKeyInput, setApiKeyInput] = useState('') const [proxyUrlInput, setProxyUrlInput] = useState('') const [deviceType, setDeviceType] = useState('ipad') const [proxyLine, setProxyLine] = useState('10') const [wId, setWId] = useState('') const [qrUrl, setQrUrl] = useState('') const [poll, setPoll] = useState(false) const [proxyStatus, setProxyStatus] = useState | null>(null) const [refreshing, setRefreshing] = useState(false) const load = useCallback(async () => { if (!bind) { setCfg(null) return } setMsg('') try { const r = await apiFetch(bind, '/chatbot/wechat/config') if (!r.ok) throw new Error(String(r.status)) const j = (await r.json()) as WechatConfigView setCfg(j) setProxyUrlInput(j.proxyUrl || '') setDeviceType(j.deviceType || 'ipad') setProxyLine(j.proxy || '10') setApiKeyInput('') } catch (e) { setCfg(null) setMsg(e instanceof Error ? e.message : String(e)) } }, [bind]) useEffect(() => { void load() }, [load]) useEffect(() => { if (!poll || !wId || !bind) return const tick = async () => { try { const r = await apiFetch(bind, '/chatbot/wechat/login/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wId }), }) const j = (await r.json()) as { status?: string wcId?: string nickName?: string verifyUrl?: string error?: string } if (!r.ok) { setMsg(j.error || String(r.status)) setPoll(false) return } if (j.status === 'logged_in') { setPoll(false) setMsg(`已登录:${j.nickName || ''}(${j.wcId || ''})`) void load() return } if (j.status === 'need_verify' && j.verifyUrl) { setMsg(`需要辅助验证:${j.verifyUrl}`) } } catch (e) { setMsg(e instanceof Error ? e.message : String(e)) setPoll(false) } } const id = setInterval(() => void tick(), 5000) void tick() return () => clearInterval(id) }, [poll, wId, bind, load]) const saveConfig = async () => { if (!bind) return setBusy(true) setMsg('') try { const body: Record = { proxyUrl: proxyUrlInput.trim(), deviceType, proxy: proxyLine.trim(), } if (apiKeyInput.trim()) body.apiKey = apiKeyInput.trim() const r = await apiFetch(bind, '/chatbot/wechat/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!r.ok) { const t = await r.text() throw new Error(t || String(r.status)) } await load() } catch (e) { setMsg(e instanceof Error ? e.message : String(e)) } finally { setBusy(false) } } const loadProxyStatus = async () => { if (!bind) return setBusy(true) setMsg('') try { const r = await apiFetch(bind, '/chatbot/wechat/proxy/status') const j = await r.json() if (!r.ok) { setMsg(typeof j.error === 'string' ? j.error : JSON.stringify(j)) setProxyStatus(null) return } setProxyStatus(j as Record) } catch (e) { setMsg(e instanceof Error ? e.message : String(e)) } finally { setBusy(false) } } const startQr = async () => { if (!bind) return setBusy(true) setMsg('') setQrUrl('') setWId('') try { const r = await apiFetch(bind, '/chatbot/wechat/qr/start', { method: 'POST' }) const j = (await r.json()) as { wId?: string; qrCodeUrl?: string; error?: string } if (!r.ok) { setMsg(j.error || String(r.status)) return } if (!j.wId || !j.qrCodeUrl) { setMsg('未返回二维码') return } setWId(j.wId) setQrUrl(j.qrCodeUrl) setPoll(true) } catch (e) { setMsg(e instanceof Error ? e.message : String(e)) } finally { setBusy(false) } } const onRefresh = useCallback(async () => { setRefreshing(true) await load() setRefreshing(false) }, [load]) return ( void onRefresh()} tintColor={theme.accent} /> ) : undefined } > 微信 与桌面端「微信」页同源:openclaw-wechat 代理 API。 {!bind && ( 未绑定 PC:配对后可读写配置与扫码登录。 )} 微信 Chatbot proxyUrl(代理服务根地址) apiKey(留空则不修改已保存密钥) deviceType setDeviceType('ipad')} disabled={!bind || busy} > ipad setDeviceType('mac')} disabled={!bind || busy} > mac proxy 参数 void saveConfig()}> 保存配置 void load()}> 刷新 void loadProxyStatus()} > 查询代理状态 {proxyStatus ? ( {JSON.stringify(proxyStatus, null, 2)} ) : null} {cfg?.session?.wcId ? ( 本地记录:{cfg.session.nickName || '—'}({cfg.session.wcId}) ) : null} void startQr()} > 获取登录二维码 {!cfg?.configured && bind ? ( 请先填写并保存 apiKey 与 proxyUrl。 ) : null} {qrUrl ? ( 请使用微信扫描下方二维码。 void Linking.openURL(qrUrl)}> 打开二维码链接 ) : null} {!!msg && ( {msg} )} ) } const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: theme.bg }, scroll: { padding: 16, paddingBottom: 40 }, h1: { fontSize: 20, fontWeight: '700', color: theme.text, marginBottom: 6 }, lead: { fontSize: 14, color: theme.muted, marginBottom: 14, lineHeight: 22 }, banner: { padding: 12, borderRadius: 10, backgroundColor: 'rgba(100,149,237,0.1)', borderWidth: 1, borderColor: 'rgba(100,149,237,0.25)', marginBottom: 14, }, bannerText: { fontSize: 13, color: theme.text, lineHeight: 20 }, card: { padding: 14, borderRadius: 12, backgroundColor: theme.bgCard, borderWidth: 1, borderColor: 'rgba(100,149,237,0.28)', }, cardTitle: { fontSize: 16, fontWeight: '600', color: theme.text, marginBottom: 12 }, label: { fontSize: 12, color: theme.muted, marginBottom: 6 }, input: { borderWidth: 1, borderColor: theme.border, borderRadius: 10, padding: 10, color: theme.text, marginBottom: 12, backgroundColor: theme.bg, }, inputDisabled: { opacity: 0.55 }, row: { flexDirection: 'row', gap: 10, marginBottom: 12 }, chip: { paddingVertical: 8, paddingHorizontal: 14, borderRadius: 8, borderWidth: 1, borderColor: theme.border, backgroundColor: theme.bgElevated, }, chipOn: { borderColor: theme.accent, backgroundColor: 'rgba(91,157,255,0.12)' }, chipText: { color: theme.text, fontWeight: '600' }, btnRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 12 }, btnSec: { paddingVertical: 10, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1, borderColor: theme.border, backgroundColor: theme.bgElevated, }, btnSecText: { color: theme.text, fontWeight: '600', fontSize: 13 }, btnPri: { marginTop: 8, paddingVertical: 12, borderRadius: 10, backgroundColor: '#3d7eff', alignItems: 'center', }, btnPriText: { color: '#fff', fontWeight: '700' }, disabled: { opacity: 0.45 }, pre: { fontFamily: 'monospace', fontSize: 11, color: '#c8cdd5', marginBottom: 12, lineHeight: 16, }, hint: { fontSize: 13, color: theme.muted, marginTop: 8, lineHeight: 20 }, qrBlock: { marginTop: 16, alignItems: 'center' }, qrImg: { width: 224, height: 224, marginVertical: 12, backgroundColor: '#fff' }, link: { color: theme.accent, fontSize: 14, textDecorationLine: 'underline' }, msg: { marginTop: 12, fontSize: 14 }, msgOk: { color: theme.success }, msgErr: { color: theme.danger }, })