import { useState, useEffect, useCallback } from 'react' import { useAdapterStore } from '../stores/adapterStore' import { useTranslation } from '../i18n' import { Input } from '../components/shared/Input' import { Button } from '../components/shared/Button' import { DirectoryPicker } from '../components/shared/DirectoryPicker' import { ConfirmDialog } from '../components/shared/ConfirmDialog' import QRCode from 'qrcode' type ImTab = 'feishu' | 'wechat' | 'dingtalk' | 'telegram' type ImPlatform = 'telegram' | 'feishu' | 'wechat' | 'dingtalk' export function AdapterSettings() { const t = useTranslation() const { config, isLoading, fetchConfig, updateConfig, generatePairingCode, startWechatLogin, pollWechatLogin, removePairedUser, beginDingtalkRegistration, pollDingtalkRegistration, unbindWechatAccount, unbindDingtalkBot, } = useAdapterStore() // Active IM tab —— Feishu 默认展示,在前 const [activeIm, setActiveIm] = useState('feishu') // Server —— serverUrl 不再暴露在 UI 里(见下方 Server URL 注释), // 桌面端用 Tauri env var 注入动态端口。 const [defaultProjectDir, setDefaultProjectDir] = useState('') // Telegram const [tgBotToken, setTgBotToken] = useState('') const [tgAllowedUsers, setTgAllowedUsers] = useState('') // Feishu const [fsAppId, setFsAppId] = useState('') const [fsAppSecret, setFsAppSecret] = useState('') const [fsEncryptKey, setFsEncryptKey] = useState('') const [fsVerificationToken, setFsVerificationToken] = useState('') const [fsAllowedUsers, setFsAllowedUsers] = useState('') const [fsStreamingCard, setFsStreamingCard] = useState(false) // WeChat const [wcAllowedUsers, setWcAllowedUsers] = useState('') const [wechatQrUrl, setWechatQrUrl] = useState(null) const [wechatSessionKey, setWechatSessionKey] = useState(null) const [wechatStatus, setWechatStatus] = useState('') const [isWechatBinding, setIsWechatBinding] = useState(false) const [isUnbindingWechatAccount, setIsUnbindingWechatAccount] = useState(false) // DingTalk const [dtClientId, setDtClientId] = useState('') const [dtClientSecret, setDtClientSecret] = useState('') const [dtAllowedUsers, setDtAllowedUsers] = useState('') const [dtEndpoint, setDtEndpoint] = useState('') const [dtRegistration, setDtRegistration] = useState<{ deviceCode: string verificationUriComplete: string qrDataUrl?: string intervalSeconds: number expiresAt: number } | null>(null) const [dtAuthStatus, setDtAuthStatus] = useState<'idle' | 'waiting' | 'bound' | 'error'>('idle') const [dtAuthError, setDtAuthError] = useState('') const [isStartingDtAuth, setIsStartingDtAuth] = useState(false) const [isUnbindingDtBot, setIsUnbindingDtBot] = useState(false) const [isSaving, setIsSaving] = useState(false) const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle') const [saveError, setSaveError] = useState('') // Pairing const [pairingCode, setPairingCode] = useState(null) const [isGenerating, setIsGenerating] = useState(false) const [pendingUnbind, setPendingUnbind] = useState<{ platform: ImPlatform; userId: string | number } | null>(null) const [isUnbinding, setIsUnbinding] = useState(false) useEffect(() => { fetchConfig() }, []) // Sync form state when config is loaded useEffect(() => { setDefaultProjectDir(config.defaultProjectDir ?? '') setTgBotToken(config.telegram?.botToken ?? '') setTgAllowedUsers(config.telegram?.allowedUsers?.join(', ') ?? '') setFsAppId(config.feishu?.appId ?? '') setFsAppSecret(config.feishu?.appSecret ?? '') setFsEncryptKey(config.feishu?.encryptKey ?? '') setFsVerificationToken(config.feishu?.verificationToken ?? '') setFsAllowedUsers(config.feishu?.allowedUsers?.join(', ') ?? '') setFsStreamingCard(config.feishu?.streamingCard ?? false) setWcAllowedUsers(config.wechat?.allowedUsers?.join(', ') ?? '') setDtClientId(config.dingtalk?.clientId ?? '') setDtClientSecret(config.dingtalk?.clientSecret ?? '') setDtAllowedUsers(config.dingtalk?.allowedUsers?.join(', ') ?? '') setDtEndpoint(config.dingtalk?.endpoint ?? '') }, [config]) useEffect(() => { if (!wechatSessionKey) return let cancelled = false let timer: number | null = null const poll = async () => { try { const result = await pollWechatLogin(wechatSessionKey) if (cancelled) return if (result.connected) { setWechatStatus(t('settings.adapters.wechatBindSuccess')) setWechatQrUrl(null) setWechatSessionKey(null) setIsWechatBinding(false) return } if (result.message) { setWechatStatus(result.message) } } catch (err) { if (!cancelled) setWechatStatus(err instanceof Error ? err.message : 'WeChat bind failed') } if (!cancelled) { timer = window.setTimeout(() => void poll(), 1200) } } timer = window.setTimeout(() => void poll(), 1200) return () => { cancelled = true if (timer != null) window.clearTimeout(timer) } }, [wechatSessionKey, pollWechatLogin, t]) useEffect(() => { if (!dtRegistration || dtAuthStatus !== 'waiting') return let cancelled = false const poll = async () => { if (Date.now() > dtRegistration.expiresAt) { setDtAuthStatus('error') setDtAuthError(t('settings.adapters.dingtalkAuthExpired')) setDtRegistration(null) return } try { const result = await pollDingtalkRegistration(dtRegistration.deviceCode) if (cancelled) return if (result.status === 'SUCCESS') { setDtAuthStatus('bound') setDtRegistration(null) setDtAuthError('') await fetchConfig() } else if (result.status === 'FAIL' || result.status === 'EXPIRED') { setDtAuthStatus('error') setDtAuthError(result.failReason || t('settings.adapters.dingtalkAuthFailed')) setDtRegistration(null) } } catch (err) { if (!cancelled) { setDtAuthStatus('error') setDtAuthError(err instanceof Error ? err.message : t('settings.adapters.dingtalkAuthFailed')) } } } const timer = window.setInterval(poll, Math.max(1, dtRegistration.intervalSeconds) * 1000) void poll() return () => { cancelled = true window.clearInterval(timer) } }, [dtRegistration, dtAuthStatus, pollDingtalkRegistration, fetchConfig, t]) async function handleSave() { setIsSaving(true) setSaveStatus('idle') setSaveError('') try { const patch: Record = {} if (defaultProjectDir) patch.defaultProjectDir = defaultProjectDir const tgUsers = tgAllowedUsers .split(',') .map((s) => s.trim()) .filter(Boolean) .map(Number) .filter((n) => !isNaN(n)) patch.telegram = { botToken: tgBotToken || undefined, allowedUsers: tgUsers.length ? tgUsers : [], } const fsUsers = fsAllowedUsers .split(',') .map((s) => s.trim()) .filter(Boolean) patch.feishu = { appId: fsAppId || undefined, appSecret: fsAppSecret || undefined, encryptKey: fsEncryptKey || undefined, verificationToken: fsVerificationToken || undefined, allowedUsers: fsUsers.length ? fsUsers : [], streamingCard: fsStreamingCard, } const wcUsers = wcAllowedUsers .split(',') .map((s) => s.trim()) .filter(Boolean) patch.wechat = { ...config.wechat, allowedUsers: wcUsers.length ? wcUsers : [], } const dtUsers = dtAllowedUsers .split(',') .map((s) => s.trim()) .filter(Boolean) patch.dingtalk = { clientId: dtClientId || undefined, clientSecret: dtClientSecret || undefined, allowedUsers: dtUsers.length ? dtUsers : [], endpoint: dtEndpoint || undefined, } await updateConfig(patch) setSaveStatus('saved') setTimeout(() => setSaveStatus('idle'), 2000) } catch (err) { setSaveStatus('error') setSaveError(err instanceof Error ? err.message : 'Save failed') } finally { setIsSaving(false) } } const handleGenerateCode = useCallback(async () => { setIsGenerating(true) try { const code = await generatePairingCode() setPairingCode(code) } catch (err) { console.error('Failed to generate pairing code:', err) } finally { setIsGenerating(false) } }, [generatePairingCode]) const handleWechatBind = useCallback(async () => { setIsWechatBinding(true) setWechatStatus('') try { const result = await startWechatLogin() if (!result.qrcodeUrl) { throw new Error(result.message || 'WeChat QR URL missing') } const qrDataUrl = await QRCode.toDataURL(result.qrcodeUrl, { errorCorrectionLevel: 'M', margin: 1, scale: 8, }) setWechatQrUrl(qrDataUrl) setWechatSessionKey(result.sessionKey) setWechatStatus(result.message) } catch (err) { setWechatStatus(err instanceof Error ? err.message : 'WeChat bind failed') setIsWechatBinding(false) } }, [startWechatLogin]) const handleStartDingtalkAuth = useCallback(async () => { setIsStartingDtAuth(true) setDtAuthStatus('idle') setDtAuthError('') try { const begin = await beginDingtalkRegistration() setDtRegistration({ deviceCode: begin.deviceCode, verificationUriComplete: begin.verificationUriComplete, qrDataUrl: begin.qrDataUrl, intervalSeconds: begin.intervalSeconds, expiresAt: Date.now() + begin.expiresInSeconds * 1000, }) setDtAuthStatus('waiting') } catch (err) { setDtAuthStatus('error') setDtAuthError(err instanceof Error ? err.message : t('settings.adapters.dingtalkAuthFailed')) } finally { setIsStartingDtAuth(false) } }, [beginDingtalkRegistration, t]) const handleUnbindWechatAccount = useCallback(async () => { setIsUnbindingWechatAccount(true) setWechatStatus('') try { await unbindWechatAccount() await fetchConfig() setWechatQrUrl(null) setWechatSessionKey(null) setWechatStatus(t('settings.adapters.wechatUnbound')) } catch (err) { setWechatStatus(err instanceof Error ? err.message : t('settings.adapters.wechatUnbindFailed')) } finally { setIsUnbindingWechatAccount(false) setIsWechatBinding(false) } }, [unbindWechatAccount, fetchConfig, t]) const handleUnbindDingtalkBot = useCallback(async () => { setIsUnbindingDtBot(true) setDtAuthError('') try { await unbindDingtalkBot() setDtAuthStatus('idle') setDtRegistration(null) await fetchConfig() } catch (err) { setDtAuthStatus('error') setDtAuthError(err instanceof Error ? err.message : t('settings.adapters.dingtalkUnbindFailed')) } finally { setIsUnbindingDtBot(false) } }, [unbindDingtalkBot, fetchConfig, t]) const handleUnbind = useCallback(async (platform: ImPlatform, userId: string | number) => { setPendingUnbind({ platform, userId }) }, []) const confirmUnbind = useCallback(async () => { if (!pendingUnbind) return setIsUnbinding(true) try { await removePairedUser(pendingUnbind.platform, pendingUnbind.userId) await fetchConfig() setPendingUnbind(null) } finally { setIsUnbinding(false) } }, [pendingUnbind, removePairedUser, fetchConfig]) // Collect all paired users across platforms const allPairedUsers = [ ...(config.telegram?.pairedUsers ?? []).map((u) => ({ ...u, platform: 'telegram' as const })), ...(config.feishu?.pairedUsers ?? []).map((u) => ({ ...u, platform: 'feishu' as const })), ...(config.wechat?.pairedUsers ?? []).map((u) => ({ ...u, platform: 'wechat' as const })), ...(config.dingtalk?.pairedUsers ?? []).map((u) => ({ ...u, platform: 'dingtalk' as const })), ] // Check pairing expiry const pairingExpiry = config.pairing?.expiresAt const isPairingActive = pairingExpiry ? Date.now() < pairingExpiry : false const minutesLeft = pairingExpiry ? Math.max(0, Math.ceil((pairingExpiry - Date.now()) / 60000)) : 0 if (isLoading) { return (
progress_activity Loading...
) } return (
{/* Description */}

{t('settings.adapters.description')}

{/* Pairing */}
link {t('settings.adapters.pairing')}

{t('settings.adapters.pairingDesc')}

{/* Generate code */}
{pairingCode && (
{pairingCode} {t('settings.adapters.codeExpiresIn')} 60 {t('settings.adapters.minutes')}
)} {!pairingCode && isPairingActive && ( {t('settings.adapters.codeExpiresIn')} {minutesLeft} {t('settings.adapters.minutes')} )}
{pairingCode && (

{t('settings.adapters.pairingCodeHint')}

)} {/* Paired users list */}

{t('settings.adapters.pairedUsers')}

{allPairedUsers.length === 0 ? (

{t('settings.adapters.noPairedUsers')}

) : (
{allPairedUsers.map((user) => (
{t(`settings.adapters.platform.${user.platform}`)} {user.displayName} {new Date(user.pairedAt).toLocaleDateString()}
))}
)}
{/* Server URL —— 之前是个手填字段,但桌面端 Tauri 启动 adapter sidecar 时已经把 server 的动态端口通过 ADAPTER_SERVER_URL env var 注进去了, loadConfig() 里 env 优先级高于这里的 file value,所以这个字段在桌面 运行时完全不会被读到。用户也根本不知道该填什么端口(每次启动随机)。 Standalone 模式(直接 bun run adapters/...)保留 file 字段兜底就够了。 */} {/* Default Project */}

{t('settings.adapters.defaultProjectHint')}

{/* IM Adapter Tabs —— Feishu 默认在前,Telegram 在后 */}
setActiveIm('feishu')} /> setActiveIm('wechat')} /> setActiveIm('dingtalk')} /> setActiveIm('telegram')} />
{activeIm === 'feishu' && (
setFsAppId(e.target.value)} placeholder={t('settings.adapters.appIdPlaceholder')} /> setFsAppSecret(e.target.value)} placeholder={t('settings.adapters.appSecretPlaceholder')} />
setFsEncryptKey(e.target.value)} placeholder={t('settings.adapters.encryptKeyPlaceholder')} /> setFsVerificationToken(e.target.value)} placeholder={t('settings.adapters.verificationTokenPlaceholder')} />
setFsAllowedUsers(e.target.value)} placeholder={t('settings.adapters.fsAllowedUsersPlaceholder')} />

{t('settings.adapters.allowedUsersHint')}

)} {activeIm === 'wechat' && (
{config.wechat?.accountId ? t('settings.adapters.wechatConnected') : t('settings.adapters.wechatNotConnected')}

{t('settings.adapters.wechatQrHint')}

{config.wechat?.accountId && ( )}
{wechatQrUrl && (
{t('settings.adapters.wechatQrAlt')}
{wechatStatus || t('settings.adapters.wechatWaiting')}
)} {!wechatQrUrl && wechatStatus && (

{wechatStatus}

)}
setWcAllowedUsers(e.target.value)} placeholder={t('settings.adapters.wcAllowedUsersPlaceholder')} />

{t('settings.adapters.wechatAllowedUsersHint')}

)} {activeIm === 'dingtalk' && (

{t('settings.adapters.dingtalkQrTitle')}

{t('settings.adapters.dingtalkQrDesc')}

{(config.dingtalk?.clientId || dtClientId) && ( )}
{dtRegistration && (
{dtRegistration.qrDataUrl ? ( {t('settings.adapters.dingtalkQrAlt')} ) : null}

{t('settings.adapters.dingtalkWaiting')}

{dtRegistration.verificationUriComplete}
)} {dtAuthStatus === 'bound' && (

{t('settings.adapters.dingtalkBound')}

)} {dtAuthStatus === 'error' && (

{dtAuthError}

)}
setDtClientId(e.target.value)} placeholder={t('settings.adapters.dingtalkClientIdPlaceholder')} /> setDtClientSecret(e.target.value)} placeholder={t('settings.adapters.dingtalkClientSecretPlaceholder')} />
setDtEndpoint(e.target.value)} placeholder={t('settings.adapters.dingtalkEndpointPlaceholder')} />
setDtAllowedUsers(e.target.value)} placeholder={t('settings.adapters.dtAllowedUsersPlaceholder')} />

{t('settings.adapters.allowedUsersHint')}

)} {activeIm === 'telegram' && (
setTgBotToken(e.target.value)} placeholder={t('settings.adapters.botTokenPlaceholder')} />
setTgAllowedUsers(e.target.value)} placeholder={t('settings.adapters.tgAllowedUsersPlaceholder')} />

{t('settings.adapters.allowedUsersHint')}

)}
{/* Save */}
{saveStatus === 'saved' && ( check_circle {t('settings.adapters.saved')} )} {saveStatus === 'error' && ( error {saveError} )}
{ if (isUnbinding) return setPendingUnbind(null) }} onConfirm={confirmUnbind} title={t('settings.adapters.unbind')} body={t('settings.adapters.unbindConfirm')} confirmLabel={t('settings.adapters.unbind')} cancelLabel={t('common.cancel')} confirmVariant="danger" loading={isUnbinding} />
) } function ImTabButton({ label, active, onClick, }: { label: string active: boolean onClick: () => void }) { return ( ) }