mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +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.saved': 'Saved',
|
||||||
'settings.adapters.saving': 'Saving...',
|
'settings.adapters.saving': 'Saving...',
|
||||||
'settings.adapters.startHint': 'After saving, run adapter: cd adapters && bun run {platform}',
|
'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
|
||||||
'settings.general.languageTitle': 'Language',
|
'settings.general.languageTitle': 'Language',
|
||||||
|
|||||||
@ -126,6 +126,20 @@ export const zh: Record<TranslationKey, string> = {
|
|||||||
'settings.adapters.saved': '已保存',
|
'settings.adapters.saved': '已保存',
|
||||||
'settings.adapters.saving': '保存中...',
|
'settings.adapters.saving': '保存中...',
|
||||||
'settings.adapters.startHint': '保存后运行适配器:cd adapters && bun run {platform}',
|
'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
|
||||||
'settings.general.languageTitle': '语言',
|
'settings.general.languageTitle': '语言',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { useAdapterStore } from '../stores/adapterStore'
|
import { useAdapterStore } from '../stores/adapterStore'
|
||||||
import { useTranslation } from '../i18n'
|
import { useTranslation } from '../i18n'
|
||||||
import { Input } from '../components/shared/Input'
|
import { Input } from '../components/shared/Input'
|
||||||
@ -7,7 +7,7 @@ import { DirectoryPicker } from '../components/shared/DirectoryPicker'
|
|||||||
|
|
||||||
export function AdapterSettings() {
|
export function AdapterSettings() {
|
||||||
const t = useTranslation()
|
const t = useTranslation()
|
||||||
const { config, isLoading, fetchConfig, updateConfig } = useAdapterStore()
|
const { config, isLoading, fetchConfig, updateConfig, generatePairingCode, removePairedUser } = useAdapterStore()
|
||||||
|
|
||||||
// Server
|
// Server
|
||||||
const [serverUrl, setServerUrl] = useState('')
|
const [serverUrl, setServerUrl] = useState('')
|
||||||
@ -29,6 +29,10 @@ export function AdapterSettings() {
|
|||||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle')
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle')
|
||||||
const [saveError, setSaveError] = useState('')
|
const [saveError, setSaveError] = useState('')
|
||||||
|
|
||||||
|
// Pairing
|
||||||
|
const [pairingCode, setPairingCode] = useState<string | null>(null)
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig()
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12 text-[var(--color-text-tertiary)]">
|
<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>
|
<p className="text-sm text-[var(--color-text-secondary)]">{t('settings.adapters.description')}</p>
|
||||||
</div>
|
</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 */}
|
{/* Server URL */}
|
||||||
<Input
|
<Input
|
||||||
label={t('settings.adapters.serverUrl')}
|
label={t('settings.adapters.serverUrl')}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user