@@ -143,7 +143,7 @@ const UPLOAD_STATUS_LABELS = {
143143const APP_INFO = {
144144 name : "DL Studio" ,
145145 version : packageJson . version ,
146- updatedAt : "2026-07-07 " ,
146+ updatedAt : "2026-07-14 " ,
147147 engine : "FFmpeg / FFprobe" ,
148148 stack : "Electron + React"
149149} ;
@@ -681,6 +681,17 @@ async function loginToInfera({ identifier, password }) {
681681 } ) ;
682682}
683683
684+ async function fetchCurrentUser ( token ) {
685+ return requestInfera ( "/users/me" , { token } ) ;
686+ }
687+
688+ function hasResearchAccess ( user ) {
689+ return ( Array . isArray ( user ?. permissions ) ? user . permissions : [ ] ) . some ( ( permission ) => {
690+ const key = String ( permission ?. permissionKey || permission ?. permission_key || "" ) . trim ( ) . toLowerCase ( ) ;
691+ return key === "research.access" && permission ?. enabled === true ;
692+ } ) ;
693+ }
694+
684695async function fetchCloudRepository ( token , spaceId = CLOUD_SPACES [ 0 ] . id , options = { } ) {
685696 const items = [ ] ;
686697 let total = null ;
@@ -1180,6 +1191,7 @@ function App() {
11801191 const automationOptionsRef = useRef ( automationOptions ) ;
11811192 const automationEnqueueRunningRef = useRef ( false ) ;
11821193 const autoLoginPromptedRef = useRef ( false ) ;
1194+ const researchAccessRequestRef = useRef ( 0 ) ;
11831195 const [ repositoryState , setRepositoryState ] = useState ( {
11841196 status : "idle" ,
11851197 spaceId : cloudSpaceId ,
@@ -1200,6 +1212,7 @@ function App() {
12001212 to : "" ,
12011213 userId : ""
12021214 } ) ;
1215+ const [ researchAccessState , setResearchAccessState ] = useState ( { status : "idle" , token : "" , message : "" } ) ;
12031216 const [ researchState , setResearchState ] = useState ( {
12041217 status : "idle" ,
12051218 users : [ ] ,
@@ -1344,6 +1357,7 @@ function App() {
13441357 } ,
13451358 onExpired : ( ) => {
13461359 const previousAuth = authStateRef . current ;
1360+ researchAccessRequestRef . current += 1 ;
13471361 window . localStorage ?. removeItem ( AUTH_STORAGE_KEY ) ;
13481362 authStateRef . current = null ;
13491363 setAuthState ( null ) ;
@@ -1427,6 +1441,8 @@ function App() {
14271441 }
14281442
14291443 if ( ! authState ?. token ) {
1444+ researchAccessRequestRef . current += 1 ;
1445+ setResearchAccessState ( { status : "auth" , token : "" , message : "" } ) ;
14301446 setResearchState ( ( current ) => ( {
14311447 ...current ,
14321448 status : "auth" ,
@@ -1439,8 +1455,38 @@ function App() {
14391455 return ;
14401456 }
14411457
1458+ if (
1459+ researchAccessState . token === authState . token &&
1460+ [ "checking" , "allowed" , "forbidden" , "error" ] . includes ( researchAccessState . status )
1461+ ) {
1462+ return ;
1463+ }
1464+
1465+ loadResearchAccess ( authState ) ;
1466+ } , [ activeNav , authState ?. token , researchAccessState . status , researchAccessState . token ] ) ;
1467+
1468+ useEffect ( ( ) => {
1469+ if (
1470+ activeNav !== "Research" ||
1471+ ! authState ?. token ||
1472+ researchAccessState . status !== "allowed" ||
1473+ researchAccessState . token !== authState . token
1474+ ) {
1475+ return ;
1476+ }
1477+
14421478 loadResearchData ( researchTab , authState , researchFilters ) ;
1443- } , [ activeNav , authState ?. token , researchFilters . from , researchFilters . status , researchFilters . to , researchFilters . userId , researchTab ] ) ;
1479+ } , [
1480+ activeNav ,
1481+ authState ?. token ,
1482+ researchAccessState . status ,
1483+ researchAccessState . token ,
1484+ researchFilters . from ,
1485+ researchFilters . status ,
1486+ researchFilters . to ,
1487+ researchFilters . userId ,
1488+ researchTab
1489+ ] ) ;
14441490
14451491 useEffect ( ( ) => {
14461492 if ( ! hasProcessingJobs && ! isUploadActive ( uploadState ) && ! transferQueue . some ( ( item ) => item . status === "uploading" ) ) {
@@ -2493,7 +2539,7 @@ function App() {
24932539 if ( activeNav === "Cloud" ) {
24942540 loadCloudRepository ( nextAuth , cloudSpaceId , cloudRepositoryDateKey , cloudRepositoryStatusFilterId ) ;
24952541 } else if ( activeNav === "Research" ) {
2496- loadResearchData ( researchTab , nextAuth , researchFilters ) ;
2542+ loadResearchAccess ( nextAuth ) ;
24972543 }
24982544 } catch ( error ) {
24992545 setLoginStatus ( {
@@ -2504,10 +2550,12 @@ function App() {
25042550 }
25052551
25062552 function logout ( ) {
2553+ researchAccessRequestRef . current += 1 ;
25072554 window . localStorage ?. removeItem ( AUTH_STORAGE_KEY ) ;
25082555 authStateRef . current = null ;
25092556 setAuthState ( null ) ;
25102557 setLoginStatus ( { status : "idle" , message : "" } ) ;
2558+ setResearchAccessState ( { status : "auth" , token : "" , message : "" } ) ;
25112559 setRepositoryState ( {
25122560 status : "auth" ,
25132561 spaceId : cloudSpaceId ,
@@ -2522,6 +2570,57 @@ function App() {
25222570 } ) ;
25232571 }
25242572
2573+ async function loadResearchAccess ( authOverride = authState ) {
2574+ const token = authOverride ?. token ;
2575+ if ( ! token ) {
2576+ researchAccessRequestRef . current += 1 ;
2577+ setResearchAccessState ( { status : "auth" , token : "" , message : "" } ) ;
2578+ return ;
2579+ }
2580+
2581+ const requestId = researchAccessRequestRef . current + 1 ;
2582+ researchAccessRequestRef . current = requestId ;
2583+ setResearchAccessState ( { status : "checking" , token, message : "" } ) ;
2584+ try {
2585+ const currentUser = await fetchCurrentUser ( token ) ;
2586+ if ( researchAccessRequestRef . current !== requestId ) {
2587+ return ;
2588+ }
2589+
2590+ const resolvedToken = authStateRef . current ?. token || token ;
2591+ if ( ! hasResearchAccess ( currentUser ) ) {
2592+ setResearchAccessState ( {
2593+ status : "forbidden" ,
2594+ token : resolvedToken ,
2595+ message : "当前账号未被授予 Research 访问权限,请联系管理员。"
2596+ } ) ;
2597+ setResearchState ( ( current ) => ( {
2598+ ...current ,
2599+ status : "idle" ,
2600+ users : [ ] ,
2601+ resources : [ ] ,
2602+ resourcesTotal : 0 ,
2603+ chats : [ ] ,
2604+ chatsTotal : 0 ,
2605+ statistics : null ,
2606+ message : ""
2607+ } ) ) ;
2608+ return ;
2609+ }
2610+
2611+ setResearchAccessState ( { status : "allowed" , token : resolvedToken , message : "" } ) ;
2612+ } catch ( error ) {
2613+ if ( researchAccessRequestRef . current !== requestId || ! authStateRef . current ?. token ) {
2614+ return ;
2615+ }
2616+ setResearchAccessState ( {
2617+ status : "error" ,
2618+ token : authStateRef . current . token ,
2619+ message : error . message || "无法验证 Research 访问权限"
2620+ } ) ;
2621+ }
2622+ }
2623+
25252624 async function loadResearchData ( tabOverride = researchTab , authOverride = authState , filtersOverride = researchFilters , options = { } ) {
25262625 const token = authOverride ?. token ;
25272626 const append = Boolean ( options . append ) ;
@@ -2579,9 +2678,17 @@ function App() {
25792678 } ) ) ;
25802679 }
25812680 } catch ( error ) {
2681+ const permissionDenied = isResearchPermissionDenied ( error ) ;
2682+ if ( permissionDenied ) {
2683+ setResearchAccessState ( {
2684+ status : "forbidden" ,
2685+ token : authStateRef . current ?. token || token ,
2686+ message : "当前账号未被授予 Research 访问权限,请联系管理员。"
2687+ } ) ;
2688+ }
25822689 setResearchState ( ( current ) => ( {
25832690 ...current ,
2584- status : "error" ,
2691+ status : permissionDenied ? "forbidden" : "error" ,
25852692 message : getResearchErrorMessage ( error )
25862693 } ) ) ;
25872694 }
@@ -3106,6 +3213,7 @@ function App() {
31063213 )
31073214 ) : activeNav === "Research" ? (
31083215 < ResearchPage
3216+ accessState = { researchAccessState }
31093217 authState = { authState }
31103218 filters = { researchFilters }
31113219 onExportFilteredChats = { exportFilteredResearchChats }
@@ -3117,6 +3225,7 @@ function App() {
31173225 onLoadMoreChats = { loadMoreResearchChats }
31183226 onLoadMoreResources = { loadMoreResearchResources }
31193227 onRefresh = { ( ) => loadResearchData ( researchTab , authState , researchFilters ) }
3228+ onRetryAccess = { ( ) => loadResearchAccess ( authState ) }
31203229 onSelectLoadedChats = { selectLoadedResearchChats }
31213230 onSelectLoadedResources = { selectLoadedResearchResources }
31223231 onTabChange = { setResearchTab }
@@ -4456,6 +4565,7 @@ function EngineQueryEntryBody({ entry, mediaProxyUrl }) {
44564565}
44574566
44584567function ResearchPage ( {
4568+ accessState,
44594569 authState,
44604570 filters,
44614571 onExportFilteredChats,
@@ -4467,6 +4577,7 @@ function ResearchPage({
44674577 onLoadMoreChats,
44684578 onLoadMoreResources,
44694579 onRefresh,
4580+ onRetryAccess,
44704581 onSelectLoadedChats,
44714582 onSelectLoadedResources,
44724583 onTabChange,
@@ -4539,6 +4650,64 @@ function ResearchPage({
45394650 ) ;
45404651 }
45414652
4653+ const accessStatus = state . status === "forbidden" ? "forbidden" : accessState ?. status ;
4654+
4655+ if ( accessStatus === "idle" || accessStatus === "checking" ) {
4656+ return (
4657+ < section className = "research-login-page" >
4658+ < div className = "research-login-card" >
4659+ < div className = "research-login-icon" >
4660+ < LockKeyhole size = { 24 } />
4661+ </ div >
4662+ < div className = "research-login-copy" >
4663+ < h1 > 正在检查 Research 权限</ h1 >
4664+ < p > 正在读取当前账号的访问权限。</ p >
4665+ </ div >
4666+ </ div >
4667+ </ section >
4668+ ) ;
4669+ }
4670+
4671+ if ( accessStatus === "forbidden" ) {
4672+ return (
4673+ < section className = "research-login-page" >
4674+ < div className = "research-login-card" >
4675+ < div className = "research-login-icon" >
4676+ < LockKeyhole size = { 24 } />
4677+ </ div >
4678+ < div className = "research-login-copy" >
4679+ < h1 > 无 Research 访问权限</ h1 >
4680+ < p > { accessState ?. message || "当前账号未被授予 Research 访问权限,请联系管理员。" } </ p >
4681+ </ div >
4682+ < button className = "primary-button" onClick = { onRetryAccess } type = "button" >
4683+ < RotateCcw size = { 16 } />
4684+ < span > 重新检查</ span >
4685+ </ button >
4686+ </ div >
4687+ </ section >
4688+ ) ;
4689+ }
4690+
4691+ if ( accessStatus === "error" ) {
4692+ return (
4693+ < section className = "research-login-page" >
4694+ < div className = "research-login-card" >
4695+ < div className = "research-login-icon" >
4696+ < TriangleAlert size = { 24 } />
4697+ </ div >
4698+ < div className = "research-login-copy" >
4699+ < h1 > 无法验证 Research 权限</ h1 >
4700+ < p > { accessState ?. message || "请检查网络后重试。" } </ p >
4701+ </ div >
4702+ < button className = "primary-button" onClick = { onRetryAccess } type = "button" >
4703+ < RotateCcw size = { 16 } />
4704+ < span > 重试</ span >
4705+ </ button >
4706+ </ div >
4707+ </ section >
4708+ ) ;
4709+ }
4710+
45424711 return (
45434712 < section className = "research-page" >
45444713 < div className = "research-tabs" >
@@ -7085,13 +7254,18 @@ function toggleId(ids, id) {
70857254 return current . includes ( normalizedId ) ? current . filter ( ( item ) => item !== normalizedId ) : [ ...current , normalizedId ] ;
70867255}
70877256
7257+ function isResearchPermissionDenied ( error ) {
7258+ const message = String ( error ?. message || "" ) ;
7259+ return Number ( error ?. status || error ?. statusCode ) === 403 || message . includes ( "403" ) || / r e s e a r c h a c c e s s d e n i e d / i. test ( message ) ;
7260+ }
7261+
70887262function getResearchErrorMessage ( error ) {
70897263 const message = String ( error ?. message || "" ) ;
7090- if ( message . includes ( "401" ) || message . toLowerCase ( ) . includes ( "research admin login required" ) ) {
7264+ if ( isResearchPermissionDenied ( error ) ) {
70917265 return "当前账号没有 Research 访问权限,请确认已授予 research.access" ;
70927266 }
7093- if ( message . includes ( "403 " ) ) {
7094- return "当前账号无权访问所选 Research 数据 " ;
7267+ if ( message . includes ( "401" ) || message . toLowerCase ( ) . includes ( "research admin login required ") ) {
7268+ return "当前账号没有 Research 访问权限,请确认已授予 research.access " ;
70957269 }
70967270 return message || "无法读取 Research 数据" ;
70977271}
0 commit comments