import { useCallback, useEffect, useId, useRef, useState } from 'react' import { createPortal } from 'react-dom' const QINGAGENT_FEEDBACK_URL = 'https://qingagent.com/feedback/plugin' const QINGAGENT_REPO_URL = 'https://github.com/void2anything/dsh-qingagent' const QINGAGENT_UPDATE_COMMAND = 'npx @deepseek-ai/dsh plugin --profile web add dsh-qingagent@latest' const UPDATE_SEEN_PREFIX = 'dsh-qingagent.update-seen.v1.' interface ClientUpdateCheck { current: string latest: string hasUpdate: boolean } export function QingBrandBadge() { const [update, setUpdate] = useState(null) const [seenVersion, setSeenVersion] = useState(null) const [cardOpen, setCardOpen] = useState(false) const [updateOpen, setUpdateOpen] = useState(false) const [copyStatus, setCopyStatus] = useState<'idle' | 'copied' | 'selected'>('idle') const rootRef = useRef(null) // hover 宽限:触发区与卡片之间有视觉间隙,鼠标穿过时会短暂离开根元素; // 立即关闭会导致用户根本点不到卡里的按钮。延迟关闭 + 重新进入即取消。 const closeTimerRef = useRef>() const openCardNow = () => { if (closeTimerRef.current) clearTimeout(closeTimerRef.current) if (!updateOpen) setCardOpen(true) } const closeCardSoon = () => { if (updateOpen) return if (closeTimerRef.current) clearTimeout(closeTimerRef.current) closeTimerRef.current = setTimeout(() => setCardOpen(false), 260) } useEffect(() => () => { if (closeTimerRef.current) clearTimeout(closeTimerRef.current) }, []) const updateTriggerRef = useRef(null) const updatePopoverRef = useRef(null) const commandRef = useRef(null) const [panelRoot, setPanelRoot] = useState(null) useEffect(() => { setPanelRoot(rootRef.current?.closest('[data-qingagent-doc-panel]') ?? null) }, []) const cardId = useId() const updatePopoverId = useId() const updateTitleId = useId() useEffect(() => { let cancelled = false void fetch('/qingagent-bridge/update-check') .then(async (response) => { if (!response.ok) return null const payload = await response.json() as Partial if ( typeof payload.current !== 'string' || typeof payload.latest !== 'string' || typeof payload.hasUpdate !== 'boolean' ) return null return payload as ClientUpdateCheck }) .then((payload) => { if (cancelled || !payload) return setSeenVersion(readUpdateSeen(payload.latest) ? payload.latest : null) setUpdate(payload) }) .catch(() => undefined) return () => { cancelled = true } }, []) const closeUpdate = useCallback((restoreFocus = false) => { setUpdateOpen(false) if (restoreFocus) updateTriggerRef.current?.focus() }, []) useEffect(() => { if (!updateOpen) return const handleOutsidePointer = (event: MouseEvent) => { const target = event.target as Node if (updatePopoverRef.current?.contains(target) || updateTriggerRef.current?.contains(target)) return setCardOpen(false) closeUpdate() } const handleKeyDown = (event: KeyboardEvent) => { if (event.key !== 'Escape') return event.preventDefault() closeUpdate(true) } document.addEventListener('mousedown', handleOutsidePointer) document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('mousedown', handleOutsidePointer) document.removeEventListener('keydown', handleKeyDown) } }, [closeUpdate, updateOpen]) const hasUnseenUpdate = Boolean(update?.hasUpdate && update.latest !== seenVersion) const currentVersion = update?.current.trim() const openUpdate = () => { if (!update?.hasUpdate) return writeUpdateSeen(update.latest) setSeenVersion(update.latest) setCopyStatus('idle') setUpdateOpen(true) } const copyUpdateCommand = async () => { try { if (!navigator.clipboard?.writeText) throw new Error('clipboard unavailable') await navigator.clipboard.writeText(QINGAGENT_UPDATE_COMMAND) setCopyStatus('copied') } catch { commandRef.current?.focus() commandRef.current?.select() setCopyStatus('selected') } } return (
{ if (!updateOpen && !rootRef.current?.contains(event.relatedTarget as Node | null)) setCardOpen(false) }} > {cardOpen ? (
{update?.hasUpdate ? ( ) : null} 觉得不错?给个 Star {currentVersion ? ( {`v${currentVersion}`} ) : null}
) : null} {updateOpen && update && panelRoot ? createPortal(
, panelRoot, ) : null}
) } function readUpdateSeen(version: string): boolean { try { return window.localStorage.getItem(UPDATE_SEEN_PREFIX + version) === '1' } catch { return false } } function writeUpdateSeen(version: string): void { try { window.localStorage.setItem(UPDATE_SEEN_PREFIX + version, '1') } catch { // 存储不可用不影响本次会话内清除角标和展示更新指令。 } }