diff --git a/uds-auth/lib/client.js b/uds-auth/lib/client.js index 4df6fca8..2990ebae 100644 --- a/uds-auth/lib/client.js +++ b/uds-auth/lib/client.js @@ -486,8 +486,7 @@ window.__ModuleLoader__.load({ } function clearSessionIfAnonymous(sessions) { - // Wait for first /api/me so we do not wipe a real session during bootstrap. - if (!isUdsAuthReady()) return + // No startup /api/me — treat as anonymous until QR-triggered me sets logged-in. if (isLoggedInInUi()) return try { if (sessions && typeof sessions.clear === 'function') sessions.clear() @@ -983,7 +982,7 @@ function reloadAfterLogin() { const [user, setUser] = useState(null) - const [loading, setLoading] = useState(true) + const [loading, setLoading] = useState(false) const [config, setConfig] = useState({ loginSystemCode: '100000455558', originSystemCode: '' }) const [qrStatus, setQrStatus] = useState('') const [qrImg, setQrImg] = useState('') @@ -1050,7 +1049,7 @@ function reloadAfterLogin() { if (!me?.authenticated || !me?.data) { setUser(null) window.dispatchEvent(new Event('uds-auth-changed')) - return + return false } const d = me.data setUser({ @@ -1063,9 +1062,11 @@ function reloadAfterLogin() { permissions: d.permissions || {}, }) window.dispatchEvent(new Event('uds-auth-changed')) + return true } catch { setUser(null) window.dispatchEvent(new Event('uds-auth-changed')) + return false } finally { document.documentElement.setAttribute('data-uds-auth-ready', '1') setLoading(false) @@ -1078,8 +1079,23 @@ function reloadAfterLogin() { setQrFailed(false) setQrStatus(t('ui.qrGenerating')) setQrImg('') + // Me check in background — never block QR paint on /api/me. + void refreshUser().then((loggedIn) => { + if (!loggedIn) return + stopQr() + setQrStatus('') + setQrImg('') + setOpen(false) + }).catch(() => {}) try { const started = await fetchJson('/uds-auth/qr-start') + // Already signed in while QR was starting — drop the code. + if (document.documentElement.getAttribute('data-uds-logged-in') === '1') { + stopQr() + setQrStatus('') + setQrImg('') + return + } const { qrCodeStr, qrCodeKey, qrCodeValue, loginSystemCode, originSystemCode } = started qrRef.current.key = qrCodeKey qrRef.current.value = qrCodeValue @@ -1198,7 +1214,7 @@ function reloadAfterLogin() { }, [fbUser, fbPass, refreshUser]) useEffect(() => { - refreshUser() + // Do NOT call /api/me on mount — only when loading the login QR (startQr). fetchJson('/uds-auth/config.get') .then((data) => { if (data?.value) setConfig((prev) => ({ ...prev, ...data.value })) }) .catch(() => {}) @@ -1206,7 +1222,7 @@ function reloadAfterLogin() { .then((st) => setFallbackEnabled(!!st.enabled)) .catch(() => { /* keep default true — do not hide emergency login */ }) return () => { stopQr() } - }, [refreshUser, stopQr]) + }, [stopQr]) useEffect(() => { if (!open || user) return undefined @@ -1248,7 +1264,7 @@ function reloadAfterLogin() { const displayName = user ? (user.userName || user.name || (t('ui.userPrefix') + user.empNo)) - : (loading ? '...' : t('ui.notLoggedIn')) + : t('ui.notLoggedIn') const initials = String(displayName).slice(0, 2).toUpperCase() const label = roleLabel(user?.role) const perms = user?.permissions || {} @@ -1405,6 +1421,30 @@ function reloadAfterLogin() { localeHost = ctx translate = makeTranslator(ctx) + // --- Critical path: login badge must paint first --- + // Sync ACL + CSS before any React mount so 「未登录」is styled and locked immediately. + { + const root = document.documentElement + if (!root.getAttribute('data-uds-logged-in')) root.setAttribute('data-uds-logged-in', '0') + if (!root.getAttribute('data-uds-auth-ready')) root.setAttribute('data-uds-auth-ready', '0') + if (!root.getAttribute('data-uds-can-create-ws')) root.setAttribute('data-uds-can-create-ws', '0') + if (!root.getAttribute('data-uds-can-settings')) root.setAttribute('data-uds-can-settings', '0') + } + if (!document.getElementById('uds-auth-client-css')) { + const tag = document.createElement('style') + tag.id = 'uds-auth-client-css' + tag.setAttribute('data-plugin', name) + tag.textContent = CSS + document.head.appendChild(tag) + } + // Register login entry before heavy gates / settings section. + ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ + name: 'sidebar.footer.action', + id: 'uds-auth-login', + order: 100, + label: 'UAC', + }, AuthBadge)) + ctx.effect(() => { if (!ctx.locale || typeof ctx.locale.register !== 'function') return undefined try { @@ -1431,68 +1471,18 @@ function reloadAfterLogin() { }) } catch { /* connection may be unavailable */ } + // Settings gear: CSS hides it when data-uds-can-settings=0 (no slot override — + // registering sidebar.settings early used to crash / delay the whole plugin). - // Default ACL attrs before /api/me — anonymous stays locked until AuthBadge confirms. - ctx.effect(() => { - const root = document.documentElement - // Cookie alone is not enough; lock until /api/me sets real identity. - if (!getEmpNo()) { - root.setAttribute('data-uds-logged-in', '0') - } else if (!root.getAttribute('data-uds-logged-in')) { - // Optimistic cookie presence; AuthBadge will correct to 0/1. - root.setAttribute('data-uds-logged-in', '0') - } - if (!root.getAttribute('data-uds-auth-ready')) { - root.setAttribute('data-uds-auth-ready', '0') - } - if (!root.getAttribute('data-uds-can-create-ws')) { - root.setAttribute('data-uds-can-create-ws', '0') - } - if (!root.getAttribute('data-uds-can-settings')) { - root.setAttribute('data-uds-can-settings', '0') - } - return undefined - }, 'uds-auth: bootstrap-acl-attrs') - - - ctx.effect(() => { - const tag = document.createElement('style') - tag.id = 'uds-auth-client-css' - tag.setAttribute('data-plugin', name) - tag.textContent = CSS - document.head.appendChild(tag) - return () => tag.remove() - }, 'uds-auth: styles') - - ctx.effect(() => { - let settingsGateDispose = null - const syncSettingsGate = async () => { - let canSettings = false - try { - const me = await fetchJson('/uds-auth/api/me') - canSettings = !!(me?.data?.permissions?.canAccessSettings) - } catch { canSettings = false } - if (canSettings) { - if (settingsGateDispose) { try { settingsGateDispose() } catch {} settingsGateDispose = null } - return - } - if (settingsGateDispose) return - settingsGateDispose = ctx.slots.register({ - name: 'sidebar.settings', - id: 'uds-auth-settings-gate', - order: 9999, - }, function UdsAuthSettingsGate() { return null }) - } - syncSettingsGate() - const mePoll = setInterval(syncSettingsGate, 15000) - const onFocus = () => { syncSettingsGate() } - window.addEventListener('focus', onFocus) - return () => { - clearInterval(mePoll) - window.removeEventListener('focus', onFocus) - if (settingsGateDispose) { try { settingsGateDispose() } catch {} } - } - }, 'uds-auth: settings-gate') + // Settings page section — after login badge; not on the critical path for first paint. + ctx.slots.inject('settings.section', () => ctx.slots.register({ + name: 'settings.section', + id: 'uds-auth', + order: 22, + label: () => t('ui.settingsTitle'), + locale: LOCALE_NS, + icon: 'users', + }, AuthSettingsSection)) // Defense in depth: never show Host session rows while UDS cookies are absent. @@ -1660,27 +1650,37 @@ function reloadAfterLogin() { disposers = [] } // Deny folder pick by default — no race with native directory picker. - installGate() - const sync = async () => { - let canCreate = false - try { - const me = await fetchJson('/uds-auth/api/me') - canCreate = !!(me && me.data && me.data.permissions && me.data.permissions.canCreateWorkspace) - } catch { canCreate = false } + const sync = () => { + // Attrs come from QR-triggered /api/me via AuthBadge — do not poll me here. + const canCreate = document.documentElement.getAttribute('data-uds-can-create-ws') === '1' if (canCreate) clearGate() else installGate() } + const injectOffs = [ + 'sidebar.workspaces.directoryFlow', + 'conversation.hero.workspace.directoryFlow', + ].map((slotName) => { + try { + return ctx.slots.inject(slotName, () => { sync() }) + } catch { + return null + } + }) sync() - const timer = setInterval(sync, 15000) - const onFocus = () => { sync() } const onAuth = () => { sync() } - window.addEventListener('focus', onFocus) window.addEventListener('uds-auth-changed', onAuth) + const mo = new MutationObserver(sync) + mo.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-uds-can-create-ws', 'data-uds-logged-in'], + }) return () => { - clearInterval(timer) - window.removeEventListener('focus', onFocus) window.removeEventListener('uds-auth-changed', onAuth) + mo.disconnect() clearGate() + for (const off of injectOffs) { + try { if (typeof off === 'function') off() } catch { /* ignore */ } + } } }, 'uds-auth: directory-flow-gate') @@ -1796,40 +1796,26 @@ function reloadAfterLogin() { workspaceDispose = null } } - // Deny open-workspace by default until /api/me proves canCreateWorkspace. - if (!canOpenWorkspace()) installWorkspaceLock() + // Deny open-workspace by default until QR-triggered /api/me proves canCreateWorkspace. freezeChoosers() - const syncWorkspaceSlot = async () => { - let canCreate = document.documentElement.getAttribute('data-uds-can-create-ws') === '1' - try { - const me = await fetchJson('/uds-auth/api/me') - const user = me && me.authenticated && me.data ? me.data : null - const perms = user && user.permissions ? user.permissions : {} - canCreate = !!perms.canCreateWorkspace - document.documentElement.setAttribute('data-uds-logged-in', user ? '1' : '0') - document.documentElement.setAttribute('data-uds-can-create-ws', canCreate ? '1' : '0') - document.documentElement.setAttribute( - 'data-uds-can-settings', - user && perms.canAccessSettings ? '1' : '0', - ) - } catch { - if (!getEmpNo()) { - canCreate = false - document.documentElement.setAttribute('data-uds-logged-in', '0') - document.documentElement.setAttribute('data-uds-can-create-ws', '0') - } - } + const syncWorkspaceSlot = () => { + // Use AuthBadge DOM attrs only — no default /api/me. + const canCreate = document.documentElement.getAttribute('data-uds-can-create-ws') === '1' if (canCreate) clearWorkspaceLock() else installWorkspaceLock() freezeChoosers() } + let workspaceInjectOff = null + try { + workspaceInjectOff = ctx.slots.inject('conversation.hero.workspace', () => { + syncWorkspaceSlot() + }) + } catch { /* ignore */ } syncWorkspaceSlot() - const timer = setInterval(syncWorkspaceSlot, 10000) const onAuth = () => { syncWorkspaceSlot() } window.addEventListener('uds-auth-changed', onAuth) - window.addEventListener('focus', onAuth) - const mo = new MutationObserver(() => { freezeChoosers() }) + const mo = new MutationObserver(() => { freezeChoosers(); syncWorkspaceSlot() }) mo.observe(document.documentElement, { childList: true, subtree: true, @@ -1838,14 +1824,13 @@ function reloadAfterLogin() { }) return () => { - clearInterval(timer) mo.disconnect() document.removeEventListener('click', block, true) document.removeEventListener('pointerdown', block, true) document.removeEventListener('mousedown', block, true) window.removeEventListener('uds-auth-changed', onAuth) - window.removeEventListener('focus', onAuth) clearWorkspaceLock() + try { if (typeof workspaceInjectOff === 'function') workspaceInjectOff() } catch { /* ignore */ } } }, 'uds-auth: workspace-click-lock') ctx.effect(() => { @@ -1941,27 +1926,6 @@ function reloadAfterLogin() { window.removeEventListener('uds-auth-changed', onAuth) } }, 'uds-auth: session-only-sidebar') - - - - - - // sidebar.footer.action; layout effect lays footArea as one row: Settings | UAC login - ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ - name: 'sidebar.footer.action', - id: 'uds-auth-login', - order: 100, - label: 'UAC', - }, AuthBadge)) - - ctx.slots.inject('settings.section', () => ctx.slots.register({ - name: 'settings.section', - id: 'uds-auth', - order: 22, - label: () => t('ui.settingsTitle'), - locale: LOCALE_NS, - icon: 'users', - }, AuthSettingsSection)) } exports.name = name diff --git a/uds-auth/lib/index.js b/uds-auth/lib/index.js index 0abe6565..9d24f9a2 100644 --- a/uds-auth/lib/index.js +++ b/uds-auth/lib/index.js @@ -724,7 +724,14 @@ function handleRequest(req, res) { // 基础端点 if (url === '/api/me' && method === 'GET') { if (ctx2.empNo) { - try { ctx2.provisionedWorkspace = await ensureUserWorkspace(ctx2.empNo) } catch { ctx2.provisionedWorkspace = null } + // Never block login badge / ACL on workspaceRegistry (can wait up to 30s). + // Serve cached mapping immediately; provision in background. + try { + ctx2.provisionedWorkspace = _userWorkspaces?.get?.(ctx2.empNo) || null + } catch { + ctx2.provisionedWorkspace = null + } + void ensureUserWorkspace(ctx2.empNo).catch(() => {}) } await _apiHandlers.getCurrentUser(ctx2); return } @@ -788,7 +795,7 @@ function notifyWorkspaceRegistryReady(registry) { } /** Wait until workspaceRegistry inject fires (or timeout). */ -function whenWorkspaceRegistry(timeoutMs = 30000) { +function whenWorkspaceRegistry(timeoutMs = 2500) { if (_workspaceRegistry) return Promise.resolve(_workspaceRegistry) return new Promise((resolve) => { const entry = { @@ -808,7 +815,7 @@ async function ensureUserWorkspace(empNo) { try { let registry = _workspaceRegistry if (!registry) { - registry = await whenWorkspaceRegistry(30000) + registry = await whenWorkspaceRegistry(2500) } const root = _currentConfig?.workspaceRoot if (!registry) { diff --git a/uds-auth/lib/uds/user-search.js b/uds-auth/lib/uds/user-search.js index 6f62fce5..e1a70468 100644 --- a/uds-auth/lib/uds/user-search.js +++ b/uds-auth/lib/uds/user-search.js @@ -21,7 +21,7 @@ export function directRequest(url, opts = {}) { if (body && headers['Content-Length'] == null && headers['content-length'] == null) { headers['Content-Length'] = body.length } - const timeoutMs = opts.timeoutMs ?? 8000 + const timeoutMs = opts.timeoutMs ?? 3000 // Fresh Agent — never inherit globalAgent (often patched by proxy bootstraps). const agent = new lib.Agent({ keepAlive: false }) @@ -71,7 +71,7 @@ export async function searchUserByEmpNoToken({ empNoHeader = 'X-Emp-No', authValueHeader = 'X-Auth-Value', origin, - timeoutMs = 8000, + timeoutMs = 3000, }) { if (!userSearchUrl || !empNo || !token) { return { ok: false, reason: 'missing_args' } diff --git a/uds-auth/package.json b/uds-auth/package.json index eddd77aa..9059dd20 100644 --- a/uds-auth/package.json +++ b/uds-auth/package.json @@ -65,7 +65,6 @@ "immediately": true, "inject": [ "@deepseek-ai/dsh-client-ui-slots", - "@deepseek-ai/dsh-client-ui-layout", "@deepseek-ai/dsh-client-locale" ] }