import { useEffect, useState, useRef } from 'react' import { Card } from 'antd-mobile' import styles from './feishu-env-test.module.css' // 声明飞书 JSAPI 类型 declare global { interface Window { tt?: any ttjsBridge?: any h5sdk?: { ready: (callback: () => void) => void config: (config: any) => void error?: (handler: (error: any) => void) => void } bridge?: any _tt?: any Lark?: any Feishu?: any } } interface EnvInfo { currentUrl: string origin: string pathname: string searchParams: Record userAgent: string isInFeishu: boolean ttExists: boolean ttType: string ttMethods: string[] globalObjects: Record } interface LogEntry { time: string message: string type: 'info' | 'success' | 'error' | 'warning' } export default function FeishuEnvTest() { const [envInfo, setEnvInfo] = useState(null) const [ttReady, setTtReady] = useState(false) const [authCode, setAuthCode] = useState('') const [testResult, setTestResult] = useState('') const [checkingCount, setCheckingCount] = useState(0) const [logs, setLogs] = useState([]) const [appId, setAppId] = useState('') const pollingRef = useRef(null) // 添加日志 const addLog = (message: string, type: 'info' | 'success' | 'error' | 'warning' = 'info') => { const now = new Date() const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}.${now.getMilliseconds().toString().padStart(3, '0')}` setLogs(prev => [...prev, { time, message, type }]) } useEffect(() => { addLog('=== 页面加载完成 ===', 'info') checkEnvironment() // 移除自动轮询,避免不停地检查 }, []) const checkEnvironment = () => { addLog('🔍 开始检查环境...', 'info') const ua = navigator.userAgent const url = new URL(window.location.href) const searchParams: Record = {} url.searchParams.forEach((value, key) => { searchParams[key] = value }) // 检查 UA const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark') addLog(`📱 User Agent 检测: ${isInFeishu ? '✅ 飞书环境' : '❌ 非飞书环境'}`, isInFeishu ? 'success' : 'warning') // 检查 URL addLog(`🔗 当前 URL: ${window.location.href}`, 'info') addLog(`🔗 域名: ${window.location.origin}`, 'info') addLog(`🔗 路径: ${window.location.pathname}`, 'info') // 检查参数 if (Object.keys(searchParams).length > 0) { addLog(`📋 URL 参数: ${JSON.stringify(searchParams)}`, 'info') } // 检查所有可能的飞书相关全局对象 addLog('🔍 检查全局对象...', 'info') const globalObjects: Record = {} const keysToCheck = ['tt', 'ttjsBridge', 'h5sdk', 'bridge', '_tt', 'Lark', 'Feishu'] keysToCheck.forEach(key => { if ((window as any)[key] !== undefined) { globalObjects[key] = typeof (window as any)[key] addLog(` ✓ window.${key} = ${typeof (window as any)[key]}`, 'success') } else { addLog(` ✗ window.${key} = undefined`, 'info') } }) // 检查 tt 对象 let ttType = 'undefined' let ttMethods: string[] = [] if (window.tt) { ttType = typeof window.tt addLog(`✅ tt 对象存在!类型: ${ttType}`, 'success') try { ttMethods = Object.keys(window.tt).filter(k => typeof window.tt[k] === 'function') addLog(` 可用方法 (${ttMethods.length} 个): ${ttMethods.slice(0, 5).join(', ')}${ttMethods.length > 5 ? '...' : ''}`, 'info') } catch (e) { addLog(` 无法获取方法列表: ${(e as Error).message}`, 'warning') } } else { addLog('❌ tt 对象不存在', 'error') } // 检查 h5sdk 对象 if (window.h5sdk) { addLog(`✅ h5sdk 对象存在!类型: ${typeof window.h5sdk}`, 'success') try { const h5sdkMethods = Object.keys(window.h5sdk).filter(k => typeof window.h5sdk[k] === 'function') addLog(` h5sdk 可用方法 (${h5sdkMethods.length} 个): ${h5sdkMethods.join(', ')}`, 'info') addLog(` has ready(): ${typeof window.h5sdk.ready === 'function' ? '✅ 是' : '❌ 否'}`, 'info') addLog(` has config(): ${typeof window.h5sdk.config === 'function' ? '✅ 是' : '❌ 否'}`, 'info') addLog(` has error(): ${typeof window.h5sdk.error === 'function' ? '✅ 是' : '❌ 否'}`, 'info') } catch (e) { addLog(` 无法获取 h5sdk 方法列表: ${(e as Error).message}`, 'warning') } } else { addLog('❌ h5sdk 对象不存在', 'warning') } setEnvInfo({ currentUrl: window.location.href, origin: window.location.origin, pathname: window.location.pathname, searchParams, userAgent: ua, isInFeishu, ttExists: !!window.tt, ttType, ttMethods, globalObjects, }) setCheckingCount(prev => prev + 1) addLog(`✅ 环境检查完成 (第 ${checkingCount + 1} 次)`, 'info') } const testAuthCode = () => { addLog('🚀 开始测试免登...', 'info') if (!window.tt) { addLog('❌ tt 对象不存在,无法测试免登', 'error') setTestResult('❌ tt 对象不存在,无法测试') return } if (!appId) { addLog('❌ 请先输入 appId', 'error') setTestResult('❌ 请先输入 appId') return } addLog(`📞 调用 tt.requestAuthCode (appId: ${appId.substring(0, 10)}...)`, 'info') setTestResult('正在调用 tt.requestAuthCode...') try { window.tt.requestAuthCode({ appId: appId, success: (res: any) => { addLog(`✅ requestAuthCode 成功!`, 'success') addLog(` 授权码: ${res.code?.substring(0, 30)}...`, 'info') addLog(` 完整响应: ${JSON.stringify(res)}`, 'info') setAuthCode(res.code || '无授权码') setTestResult('✅ 成功获取授权码: ' + (res.code?.substring(0, 20) || '无') + '...') }, fail: (err: any) => { addLog(`❌ requestAuthCode 失败: ${JSON.stringify(err)}`, 'error') setTestResult('❌ 获取授权码失败: ' + JSON.stringify(err)) }, }) } catch (e: any) { addLog(`❌ 调用异常: ${e.message}`, 'error') setTestResult('❌ 调用异常: ' + e.message) } } // 测试 h5sdk.ready() const testH5sdkReady = () => { addLog('🚀 开始测试 h5sdk.ready()...', 'info') if (!window.h5sdk) { addLog('❌ h5sdk 对象不存在,无法测试', 'error') setTestResult('❌ h5sdk 对象不存在') return } if (typeof window.h5sdk.ready !== 'function') { addLog('❌ h5sdk.ready 不是函数', 'error') setTestResult('❌ h5sdk.ready 不可用') return } addLog('📞 调用 h5sdk.ready()...', 'info') setTestResult('正在调用 h5sdk.ready()...') const timeoutId = setTimeout(() => { addLog('⚠️ h5sdk.ready() 5秒内未触发回调', 'warning') setTestResult('⚠️ h5sdk.ready() 超时') }, 5000) try { window.h5sdk.ready(() => { clearTimeout(timeoutId) addLog('✅ h5sdk.ready() 回调已触发!', 'success') addLog(` tt 对象状态: ${window.tt ? '✅ 存在' : '❌ 不存在'}`, window.tt ? 'success' : 'error') if (window.tt) { const ttMethods = Object.keys(window.tt).filter(k => typeof window.tt[k] === 'function') addLog(` tt 可用方法 (${ttMethods.length} 个): ${ttMethods.slice(0, 5).join(', ')}${ttMethods.length > 5 ? '...' : ''}`, 'info') } setTestResult('✅ h5sdk.ready() 成功,tt 对象' + (window.tt ? '已注入' : '仍未注入')) setTtReady(!!window.tt) checkEnvironment() }) // 设置错误处理 if (window.h5sdk.error) { window.h5sdk.error((error: any) => { clearTimeout(timeoutId) addLog(`❌ h5sdk error: ${JSON.stringify(error)}`, 'error') setTestResult('❌ h5sdk 报错: ' + JSON.stringify(error)) }) } } catch (e: any) { clearTimeout(timeoutId) addLog(`❌ 调用异常: ${e.message}`, 'error') setTestResult('❌ 调用异常: ' + e.message) } } // 开始轮询检查 tt 对象 const startPolling = () => { if (pollingRef.current) { addLog('⚠️ 轮询已在运行中', 'warning') return } addLog('⏱️ 开始轮询检查 tt 对象(最多 10 秒)...', 'info') setTestResult('开始轮询检查 tt 对象(最多 10 秒)...') let attempts = 0 const maxAttempts = 100 pollingRef.current = setInterval(() => { attempts++ addLog(` 🔍 第 ${attempts} 次检查 tt 对象...`, 'info') if (window.tt) { addLog(`✅ tt 对象已注入!(第 ${attempts} 次检查)`, 'success') if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null } checkEnvironment() setTestResult(`✅ tt 对象已注入(第 ${attempts} 次检查)`) setTtReady(true) } else if (attempts >= maxAttempts) { addLog(`❌ 超时:tt 对象仍未注入(已检查 ${attempts} 次)`, 'error') if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null } setTestResult('❌ 超时:tt 对象仍未注入') } }, 100) } // 停止轮询 const stopPolling = () => { if (pollingRef.current) { clearInterval(pollingRef.current) pollingRef.current = null addLog('⏹️ 已停止轮询', 'info') } } // 清空日志 const clearLogs = () => { setLogs([]) addLog('📝 日志已清空', 'info') } if (!envInfo) { return
加载中...
} return (

🔍 飞书环境检测

用于诊断飞书免登问题

{/* 操作按钮 */}
setAppId(e.target.value)} placeholder="例如: cli_xxxxxxxxxxxxx" style={{ width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: '6px', fontSize: '14px', boxSizing: 'border-box' }} />
已检测次数: {checkingCount} | 轮询状态: {pollingRef.current ? '运行中' : '已停止'}
{/* 实时日志 */}
{logs.length === 0 ? (
暂无日志
) : ( logs.map((log, index) => (
[{log.time}] {log.message}
)) )}
{/* tt 对象状态 */} {envInfo.ttExists ? (
✅ tt 对象存在
类型: {envInfo.ttType}
{envInfo.ttMethods.length > 0 && (
可用方法: {envInfo.ttMethods.slice(0, 10).join(', ')} {envInfo.ttMethods.length > 10 && '...'}
)}
) : (
❌ tt 对象不存在
如果在飞书客户端中打开,请尝试"开始轮询"按钮
)}
{/* 环境检测 */}
当前 URL: {envInfo.currentUrl}
域名: {envInfo.origin}
路径: {envInfo.pathname}
在飞书中: {envInfo.isInFeishu ? '✅ 是' : '❌ 否'}
URL 参数: {Object.keys(envInfo.searchParams).length > 0 ? JSON.stringify(envInfo.searchParams) : '(无)'}
{/* User Agent */}
{envInfo.userAgent}
{/* 全局对象 */} {Object.keys(envInfo.globalObjects).length > 0 ? (
{Object.entries(envInfo.globalObjects).map(([key, type]) => (
window.{key} {type}
))}
) : (
⚠️ 未检测到飞书相关全局对象
)}
{/* 免登测试结果 */} {testResult && (
{testResult}
)} {/* 授权码 */} {authCode && (
{authCode}
)} {/* 诊断建议 */}
如果 tt 对象不存在:
  • 确认从飞书工作台应用图标进入,而非直接打开链接
  • 确认飞书开放平台 H5 应用地址配置正确
  • 尝试点击"开始轮询"按钮等待 tt 对象注入
如果 UA 检测不是飞书:
  • 可能飞书客户端的 UA 已更新
  • 检查 User Agent 中是否包含 "Lark" 或 "feishu" 关键词
如果免登测试失败:
  • 检查飞书应用是否获得必要权限
  • 检查后端飞书配置是否正确
  • 查看浏览器控制台详细错误日志
) }