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' type ImTab = 'feishu' | 'telegram' export function AdapterSettings() { const t = useTranslation() const { config, isLoading, fetchConfig, updateConfig, generatePairingCode, removePairedUser } = 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) 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: 'telegram' | 'feishu'; 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) }, [config]) 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, } 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 handleUnbind = useCallback(async (platform: 'telegram' | 'feishu', 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 })), ] // 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('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 === '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 ( ) }