mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
feat(pairing): add pairing management UI to AdapterSettings page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
14a8322355
commit
2523ce0cd7
@ -124,6 +124,20 @@ export const en = {
|
||||
'settings.adapters.saved': 'Saved',
|
||||
'settings.adapters.saving': 'Saving...',
|
||||
'settings.adapters.startHint': 'After saving, run adapter: cd adapters && bun run {platform}',
|
||||
'settings.adapters.pairing': 'Pairing',
|
||||
'settings.adapters.pairingDesc': 'Generate a pairing code and send it to the Bot via IM private chat to bind your identity.',
|
||||
'settings.adapters.generateCode': 'Generate Code',
|
||||
'settings.adapters.regenerateCode': 'Regenerate',
|
||||
'settings.adapters.codeExpired': 'Expired',
|
||||
'settings.adapters.codeExpiresIn': 'Expires in',
|
||||
'settings.adapters.minutes': 'min',
|
||||
'settings.adapters.pairingCodeHint': 'Send this code to the Bot in Feishu or Telegram private chat.',
|
||||
'settings.adapters.pairedUsers': 'Paired Users',
|
||||
'settings.adapters.noPairedUsers': 'No paired users yet',
|
||||
'settings.adapters.unbind': 'Unbind',
|
||||
'settings.adapters.unbindConfirm': 'Are you sure you want to unbind this user? They will need to re-pair to use the bot.',
|
||||
'settings.adapters.platform.telegram': 'Telegram',
|
||||
'settings.adapters.platform.feishu': 'Feishu',
|
||||
|
||||
// Settings > General
|
||||
'settings.general.languageTitle': 'Language',
|
||||
|
||||
@ -126,6 +126,20 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.adapters.saved': '已保存',
|
||||
'settings.adapters.saving': '保存中...',
|
||||
'settings.adapters.startHint': '保存后运行适配器:cd adapters && bun run {platform}',
|
||||
'settings.adapters.pairing': '配对管理',
|
||||
'settings.adapters.pairingDesc': '生成配对码后在 IM 私聊中发送给 Bot,完成身份绑定。',
|
||||
'settings.adapters.generateCode': '生成配对码',
|
||||
'settings.adapters.regenerateCode': '重新生成',
|
||||
'settings.adapters.codeExpired': '已过期',
|
||||
'settings.adapters.codeExpiresIn': '有效期剩余',
|
||||
'settings.adapters.minutes': '分钟',
|
||||
'settings.adapters.pairingCodeHint': '请在飞书或 Telegram 私聊中发送此配对码给 Bot。',
|
||||
'settings.adapters.pairedUsers': '已配对用户',
|
||||
'settings.adapters.noPairedUsers': '暂无已配对用户',
|
||||
'settings.adapters.unbind': '解绑',
|
||||
'settings.adapters.unbindConfirm': '确定要解绑该用户吗?解绑后需重新配对才能使用。',
|
||||
'settings.adapters.platform.telegram': 'Telegram',
|
||||
'settings.adapters.platform.feishu': '飞书',
|
||||
|
||||
// Settings > General
|
||||
'settings.general.languageTitle': '语言',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAdapterStore } from '../stores/adapterStore'
|
||||
import { useTranslation } from '../i18n'
|
||||
import { Input } from '../components/shared/Input'
|
||||
@ -7,7 +7,7 @@ import { DirectoryPicker } from '../components/shared/DirectoryPicker'
|
||||
|
||||
export function AdapterSettings() {
|
||||
const t = useTranslation()
|
||||
const { config, isLoading, fetchConfig, updateConfig } = useAdapterStore()
|
||||
const { config, isLoading, fetchConfig, updateConfig, generatePairingCode, removePairedUser } = useAdapterStore()
|
||||
|
||||
// Server
|
||||
const [serverUrl, setServerUrl] = useState('')
|
||||
@ -29,6 +29,10 @@ export function AdapterSettings() {
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle')
|
||||
const [saveError, setSaveError] = useState('')
|
||||
|
||||
// Pairing
|
||||
const [pairingCode, setPairingCode] = useState<string | null>(null)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig()
|
||||
}, [])
|
||||
@ -94,6 +98,35 @@ export function AdapterSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!confirm(t('settings.adapters.unbindConfirm'))) return
|
||||
await removePairedUser(platform, userId)
|
||||
await fetchConfig()
|
||||
}, [removePairedUser, fetchConfig, t])
|
||||
|
||||
// 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 (
|
||||
<div className="flex items-center justify-center py-12 text-[var(--color-text-tertiary)]">
|
||||
@ -110,6 +143,75 @@ export function AdapterSettings() {
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">{t('settings.adapters.description')}</p>
|
||||
</div>
|
||||
|
||||
{/* Pairing */}
|
||||
<section className="rounded-xl border border-[var(--color-border)] overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-[var(--color-surface-hover)] border-b border-[var(--color-border)]">
|
||||
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-secondary)]">link</span>
|
||||
<span className="text-sm font-semibold text-[var(--color-text-primary)]">{t('settings.adapters.pairing')}</span>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">{t('settings.adapters.pairingDesc')}</p>
|
||||
|
||||
{/* Generate code */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={handleGenerateCode} loading={isGenerating}>
|
||||
{pairingCode || isPairingActive ? t('settings.adapters.regenerateCode') : t('settings.adapters.generateCode')}
|
||||
</Button>
|
||||
{pairingCode && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-2xl font-bold tracking-[0.3em] text-[var(--color-brand)]">
|
||||
{pairingCode}
|
||||
</span>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">
|
||||
{t('settings.adapters.codeExpiresIn')} 60 {t('settings.adapters.minutes')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!pairingCode && isPairingActive && (
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">
|
||||
{t('settings.adapters.codeExpiresIn')} {minutesLeft} {t('settings.adapters.minutes')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{pairingCode && (
|
||||
<p className="text-xs text-[var(--color-text-tertiary)]">{t('settings.adapters.pairingCodeHint')}</p>
|
||||
)}
|
||||
|
||||
{/* Paired users list */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-[var(--color-text-primary)] mb-2">{t('settings.adapters.pairedUsers')}</h4>
|
||||
{allPairedUsers.length === 0 ? (
|
||||
<p className="text-sm text-[var(--color-text-tertiary)]">{t('settings.adapters.noPairedUsers')}</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{allPairedUsers.map((user) => (
|
||||
<div
|
||||
key={`${user.platform}-${user.userId}`}
|
||||
className="flex items-center justify-between px-3 py-2 rounded-lg bg-[var(--color-surface-hover)]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-[var(--color-surface)] text-[var(--color-text-secondary)]">
|
||||
{t(`settings.adapters.platform.${user.platform}`)}
|
||||
</span>
|
||||
<span className="text-sm text-[var(--color-text-primary)]">{user.displayName}</span>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">
|
||||
{new Date(user.pairedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUnbind(user.platform, user.userId)}
|
||||
className="text-xs text-[var(--color-error)] hover:underline cursor-pointer"
|
||||
>
|
||||
{t('settings.adapters.unbind')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Server URL */}
|
||||
<Input
|
||||
label={t('settings.adapters.serverUrl')}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user