@@ -132,6 +132,7 @@
< tr class = "border-b border-border" >
< th class = "h-10 px-3 text-left align-middle font-medium text-muted-foreground" > 邮箱< / th >
< th class = "h-10 px-3 text-left align-middle font-medium text-muted-foreground" > 状态< / th >
< th class = "h-10 px-3 text-left align-middle font-medium text-muted-foreground" > Client ID< / th >
< th class = "h-10 px-3 text-left align-middle font-medium text-muted-foreground" > 过期时间< / th >
< th class = "h-10 px-3 text-left align-middle font-medium text-muted-foreground" > 账户类型< / th >
< th class = "h-10 px-3 text-left align-middle font-medium text-muted-foreground" > Sora2< / th >
@@ -420,6 +421,13 @@
< p id = "addRTRefreshHint" class = "text-xs text-green-600 font-medium hidden" > ✓ RT已被刷新, 已填入更新后的RT< / p >
< / div >
<!-- Client ID -->
< div class = "space-y-2" >
< label class = "text-sm font-medium" > Client ID < span class = "text-muted-foreground text-xs" > - 可选< / span > < / label >
< input id = "addTokenClientId" class = "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono" placeholder = "留空使用默认值" >
< p class = "text-xs text-muted-foreground" > 用于 RT 刷新,留空使用默认 Client ID< / p >
< / div >
<!-- Remark -->
< div class = "space-y-2" >
< label class = "text-sm font-medium" > 备注 < span class = "text-muted-foreground text-xs" > - 可选< / span > < / label >
@@ -509,6 +517,13 @@
< p id = "editRTRefreshHint" class = "text-xs text-green-600 font-medium hidden" > ✓ RT已被刷新, 已填入更新后的RT< / p >
< / div >
<!-- Client ID -->
< div class = "space-y-2" >
< label class = "text-sm font-medium" > Client ID < span class = "text-muted-foreground text-xs" > - 可选< / span > < / label >
< input id = "editTokenClientId" class = "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono" placeholder = "留空使用默认值" >
< p class = "text-xs text-muted-foreground" > 用于 RT 刷新,留空使用默认 Client ID< / p >
< / div >
<!-- Remark -->
< div class = "space-y-2" >
< label class = "text-sm font-medium" > 备注 < span class = "text-muted-foreground text-xs" > - 可选< / span > < / label >
@@ -633,18 +648,19 @@
formatSora2 = ( t ) => { if ( t . sora2 _supported === true ) { const remaining = t . sora2 _total _count - t . sora2 _redeemed _count ; const tooltipText = ` 邀请码: ${ t . sora2 _invite _code || '无' } \n 可用次数: ${ remaining } / ${ t . sora2 _total _count } \n 已用次数: ${ t . sora2 _redeemed _count } ` ; return ` <div class="inline-flex items-center gap-1"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs bg-green-50 text-green-700 cursor-pointer" title=" ${ tooltipText } " onclick="copySora2Code(' ${ t . sora2 _invite _code || '' } ')">支持</span><span class="text-xs text-muted-foreground" title=" ${ tooltipText } "> ${ remaining } / ${ t . sora2 _total _count } </span></div> ` } else if ( t . sora2 _supported === false ) { return ` <span class="inline-flex items-center rounded px-2 py-0.5 text-xs bg-gray-100 text-gray-700 cursor-pointer" title="点击使用邀请码激活" onclick="openSora2Modal( ${ t . id } )">不支持</span> ` } else { return '-' } } ,
formatPlanTypeWithTooltip = ( t ) => { const tooltipText = t . subscription _end ? ` 套餐到期: ${ new Date ( t . subscription _end ) . toLocaleDateString ( 'zh-CN' , { year : 'numeric' , month : '2-digit' , day : '2-digit' } ).replace(/ \/ /g,'-')} ${ new Date ( t . subscription _end ) . toLocaleTimeString ( 'zh-CN' , { hour : '2-digit' , minute : '2-digit' , hour12 : false } )} ` : '' ; return ` <span class="inline-flex items-center rounded px-2 py-0.5 text-xs bg-blue-50 text-blue-700 cursor-pointer" title=" ${ tooltipText || t . plan _title || '-' } "> ${ formatPlanType ( t . plan _type ) } </span> ` } ,
formatSora2Remaining = ( t ) => { if ( t . sora2 _supported === true ) { const remaining = t . sora2 _remaining _count || 0 ; return ` <span class="text-xs"> ${ remaining } </span> ` } else { return '-' } } ,
renderTokens = ( ) => { const tb = $ ( 'tokenTableBody' ) ; tb . innerHTML = allTokens . map ( t => { const imageDisplay = t . image _enabled ? ` ${ t . image _count || 0 } ` : '-' ; const videoDisplay = ( t . video _enabled && t . sora2 _supported ) ? ` ${ t . video _count || 0 } ` : '- ' ; return ` <tr><td class="py-2.5 px-3"> ${ t . email } </td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${ t . is _active ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-700' } "> ${ t . is _active ? '活跃' : '禁用' } </span></td><td class="py-2.5 px-3 text-xs"> ${ formatExpiry ( t . expiry _time ) } </td><td class="py-2.5 px-3 text-xs"> ${ formatPlanTypeWithTooltip ( t ) } </td><td class="py-2.5 px-3 text-xs"> ${ formatSora2 ( t ) } </td><td class="py-2.5 px-3"> ${ formatSora2Remaining ( t ) } </td><td class="py-2.5 px-3"> ${ imageDisplay } </td><td class="py-2.5 px-3"> ${ videoDisplay } </td><td class="py-2.5 px-3"> ${ t . error _count || 0 } </td><td class="py-2.5 px-3 text-xs text-muted-foreground"> ${ t . remark || '-' } </td><td class="py-2.5 px-3 text-right"><button onclick="testToken( ${ t . id } )" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs mr-1">测试</button><button onclick="openEditModal( ${ t . id } )" class="inline-flex items-center justify-center rounded-md hover:bg-green-50 hover:text-green-700 h-7 px-2 text-xs mr-1">编辑</button><button onclick="toggleToken( ${ t . id } , ${ t . is _active } )" class="inline-flex items-center justify-center rounded-md hover:bg-accent h-7 px-2 text-xs mr-1"> ${ t . is _active ? '禁用' : '启用' } </button><button onclick="deleteToken( ${ t . id } )" class="inline-flex items-center justify-center rounded-md hover:bg-destructive/10 hover:text-destructive h-7 px-2 text-xs">删除</button></td></tr> ` } ) . join ( '' ) } ,
formatClientId = ( clientId ) => { if ( ! clientId ) return '-' ; const short = clientId . substring ( 0 , 8 ) + '... ' ; return ` <span class="text-xs font-mono cursor-pointer hover:text-primary" title=" ${ clientId } " onclick="navigator.clipboard.writeText(' ${ clientId } ').then(()=>showToast('已复制','success'))"> ${ short } </span> ` } ,
renderTokens = ( ) => { const tb = $ ( 'tokenTableBody' ) ; tb . innerHTML = allTokens . map ( t => { const imageDisplay = t . image _enabled ? ` ${ t . image _count || 0 } ` : '-' ; const videoDisplay = ( t . video _enabled && t . sora2 _supported ) ? ` ${ t . video _count || 0 } ` : '-' ; return ` <tr><td class="py-2.5 px-3"> ${ t . email } </td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${ t . is _active ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-700' } "> ${ t . is _active ? '活跃' : '禁用' } </span></td><td class="py-2.5 px-3"> ${ formatClientId ( t . client _id ) } </td><td class="py-2.5 px-3 text-xs"> ${ formatExpiry ( t . expiry _time ) } </td><td class="py-2.5 px-3 text-xs"> ${ formatPlanTypeWithTooltip ( t ) } </td><td class="py-2.5 px-3 text-xs"> ${ formatSora2 ( t ) } </td><td class="py-2.5 px-3"> ${ formatSora2Remaining ( t ) } </td><td class="py-2.5 px-3"> ${ imageDisplay } </td><td class="py-2.5 px-3"> ${ videoDisplay } </td><td class="py-2.5 px-3"> ${ t . error _count || 0 } </td><td class="py-2.5 px-3 text-xs text-muted-foreground"> ${ t . remark || '-' } </td><td class="py-2.5 px-3 text-right"><button onclick="testToken( ${ t . id } )" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs mr-1">测试</button><button onclick="openEditModal( ${ t . id } )" class="inline-flex items-center justify-center rounded-md hover:bg-green-50 hover:text-green-700 h-7 px-2 text-xs mr-1">编辑</button><button onclick="toggleToken( ${ t . id } , ${ t . is _active } )" class="inline-flex items-center justify-center rounded-md hover:bg-accent h-7 px-2 text-xs mr-1"> ${ t . is _active ? '禁用' : '启用' } </button><button onclick="deleteToken( ${ t . id } )" class="inline-flex items-center justify-center rounded-md hover:bg-destructive/10 hover:text-destructive h-7 px-2 text-xs">删除</button></td></tr> ` } ) . join ( '' ) } ,
refreshTokens = async ( ) => { await loadTokens ( ) ; await loadStats ( ) } ,
openAddModal = ( ) => $ ( 'addModal' ) . classList . remove ( 'hidden' ) ,
closeAddModal = ( ) => { $ ( 'addModal' ) . classList . add ( 'hidden' ) ; $ ( 'addTokenAT' ) . value = '' ; $ ( 'addTokenST' ) . value = '' ; $ ( 'addTokenRT' ) . value = '' ; $ ( 'addTokenRemark' ) . value = '' ; $ ( 'addTokenImageEnabled' ) . checked = true ; $ ( 'addTokenVideoEnabled' ) . checked = true ; $ ( 'addTokenImageConcurrency' ) . value = '-1' ; $ ( 'addTokenVideoConcurrency' ) . value = '-1' ; $ ( 'addRTRefreshHint' ) . classList . add ( 'hidden' ) } ,
openEditModal = ( id ) => { const token = allTokens . find ( t => t . id === id ) ; if ( ! token ) return showToast ( 'Token不存在' , 'error' ) ; $ ( 'editTokenId' ) . value = token . id ; $ ( 'editTokenAT' ) . value = token . token || '' ; $ ( 'editTokenST' ) . value = token . st || '' ; $ ( 'editTokenRT' ) . value = token . rt || '' ; $ ( 'editTokenRemark' ) . value = token . remark || '' ; $ ( 'editTokenImageEnabled' ) . checked = token . image _enabled !== false ; $ ( 'editTokenVideoEnabled' ) . checked = token . video _enabled !== false ; $ ( 'editTokenImageConcurrency' ) . value = token . image _concurrency || '-1' ; $ ( 'editTokenVideoConcurrency' ) . value = token . video _concurrency || '-1' ; $ ( 'editModal' ) . classList . remove ( 'hidden' ) } ,
closeEditModal = ( ) => { $ ( 'editModal' ) . classList . add ( 'hidden' ) ; $ ( 'editTokenId' ) . value = '' ; $ ( 'editTokenAT' ) . value = '' ; $ ( 'editTokenST' ) . value = '' ; $ ( 'editTokenRT' ) . value = '' ; $ ( 'editTokenRemark' ) . value = '' ; $ ( 'editTokenImageEnabled' ) . checked = true ; $ ( 'editTokenVideoEnabled' ) . checked = true ; $ ( 'editTokenImageConcurrency' ) . value = '' ; $ ( 'editTokenVideoConcurrency' ) . value = '' ; $ ( 'editRTRefreshHint' ) . classList . add ( 'hidden' ) } ,
submitEditToken = async ( ) => { const id = parseInt ( $ ( 'editTokenId' ) . value ) , at = $ ( 'editTokenAT' ) . value . trim ( ) , st = $ ( 'editTokenST' ) . value . trim ( ) , rt = $ ( 'editTokenRT' ) . value . trim ( ) , remark = $ ( 'editTokenRemark' ) . value . trim ( ) , imageEnabled = $ ( 'editTokenImageEnabled' ) . checked , videoEnabled = $ ( 'editTokenVideoEnabled' ) . checked , imageConcurrency = $ ( 'editTokenImageConcurrency' ) . value ? parseInt ( $ ( 'editTokenImageConcurrency' ) . value ) : null , videoConcurrency = $ ( 'editTokenVideoConcurrency' ) . value ? parseInt ( $ ( 'editTokenVideoConcurrency' ) . value ) : null ; if ( ! id ) return showToast ( 'Token ID无效' , 'error' ) ; if ( ! at ) return showToast ( '请输入 Access Token' , 'error' ) ; const btn = $ ( 'editTokenBtn' ) , btnText = $ ( 'editTokenBtnText' ) , btnSpinner = $ ( 'editTokenBtnSpinner' ) ; btn . disabled = true ; btnText . textContent = '保存中...' ; btnSpinner . classList . remove ( 'hidden' ) ; try { const r = await apiRequest ( ` /api/tokens/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( { token : at , st : st || null , rt : rt || null , remark : remark || null , image _enabled : imageEnabled , video _enabled : videoEnabled , image _concurrency : imageConcurrency , video _concurrency : videoConcurrency } ) } ) ; if ( ! r ) { btn . disabled = false ; btnText . textContent = '保存' ; btnSpinner . classList . add ( 'hidden' ) ; return } const d = await r . json ( ) ; if ( d . success ) { closeEditModal ( ) ; await refreshTokens ( ) ; showToast ( 'Token更新成功' , 'success' ) } else { showToast ( '更新失败: ' + ( d . detail || d . message || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '更新失败: ' + e . message , 'error' ) } finally { btn . disabled = false ; btnText . textContent = '保存' ; btnSpinner . classList . add ( 'hidden' ) } } ,
closeAddModal = ( ) => { $ ( 'addModal' ) . classList . add ( 'hidden' ) ; $ ( 'addTokenAT' ) . value = '' ; $ ( 'addTokenST' ) . value = '' ; $ ( 'addTokenRT' ) . value = '' ; $ ( 'addTokenClientId' ) . value = '' ; $ ( ' addTokenRemark' ) . value = '' ; $ ( 'addTokenImageEnabled' ) . checked = true ; $ ( 'addTokenVideoEnabled' ) . checked = true ; $ ( 'addTokenImageConcurrency' ) . value = '-1' ; $ ( 'addTokenVideoConcurrency' ) . value = '-1' ; $ ( 'addRTRefreshHint' ) . classList . add ( 'hidden' ) } ,
openEditModal = ( id ) => { const token = allTokens . find ( t => t . id === id ) ; if ( ! token ) return showToast ( 'Token不存在' , 'error' ) ; $ ( 'editTokenId' ) . value = token . id ; $ ( 'editTokenAT' ) . value = token . token || '' ; $ ( 'editTokenST' ) . value = token . st || '' ; $ ( 'editTokenRT' ) . value = token . rt || '' ; $ ( 'editTokenClientId' ) . value = token . client _id || '' ; $ ( ' editTokenRemark' ) . value = token . remark || '' ; $ ( 'editTokenImageEnabled' ) . checked = token . image _enabled !== false ; $ ( 'editTokenVideoEnabled' ) . checked = token . video _enabled !== false ; $ ( 'editTokenImageConcurrency' ) . value = token . image _concurrency || '-1' ; $ ( 'editTokenVideoConcurrency' ) . value = token . video _concurrency || '-1' ; $ ( 'editModal' ) . classList . remove ( 'hidden' ) } ,
closeEditModal = ( ) => { $ ( 'editModal' ) . classList . add ( 'hidden' ) ; $ ( 'editTokenId' ) . value = '' ; $ ( 'editTokenAT' ) . value = '' ; $ ( 'editTokenST' ) . value = '' ; $ ( 'editTokenRT' ) . value = '' ; $ ( 'editTokenClientId' ) . value = '' ; $ ( ' editTokenRemark' ) . value = '' ; $ ( 'editTokenImageEnabled' ) . checked = true ; $ ( 'editTokenVideoEnabled' ) . checked = true ; $ ( 'editTokenImageConcurrency' ) . value = '' ; $ ( 'editTokenVideoConcurrency' ) . value = '' ; $ ( 'editRTRefreshHint' ) . classList . add ( 'hidden' ) } ,
submitEditToken = async ( ) => { const id = parseInt ( $ ( 'editTokenId' ) . value ) , at = $ ( 'editTokenAT' ) . value . trim ( ) , st = $ ( 'editTokenST' ) . value . trim ( ) , rt = $ ( 'editTokenRT' ) . value . trim ( ) , clientId = $ ( 'editTokenClientId' ) . value . trim ( ) , remark= $ ( 'editTokenRemark' ) . value . trim ( ) , imageEnabled = $ ( 'editTokenImageEnabled' ) . checked , videoEnabled = $ ( 'editTokenVideoEnabled' ) . checked , imageConcurrency = $ ( 'editTokenImageConcurrency' ) . value ? parseInt ( $ ( 'editTokenImageConcurrency' ) . value ) : null , videoConcurrency = $ ( 'editTokenVideoConcurrency' ) . value ? parseInt ( $ ( 'editTokenVideoConcurrency' ) . value ) : null ; if ( ! id ) return showToast ( 'Token ID无效' , 'error' ) ; if ( ! at ) return showToast ( '请输入 Access Token' , 'error' ) ; const btn = $ ( 'editTokenBtn' ) , btnText = $ ( 'editTokenBtnText' ) , btnSpinner = $ ( 'editTokenBtnSpinner' ) ; btn . disabled = true ; btnText . textContent = '保存中...' ; btnSpinner . classList . remove ( 'hidden' ) ; try { const r = await apiRequest ( ` /api/tokens/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( { token : at , st : st || null , rt : rt || null , client _id : clientId || null , remark: remark || null , image _enabled : imageEnabled , video _enabled : videoEnabled , image _concurrency : imageConcurrency , video _concurrency : videoConcurrency } ) } ) ; if ( ! r ) { btn . disabled = false ; btnText . textContent = '保存' ; btnSpinner . classList . add ( 'hidden' ) ; return } const d = await r . json ( ) ; if ( d . success ) { closeEditModal ( ) ; await refreshTokens ( ) ; showToast ( 'Token更新成功' , 'success' ) } else { showToast ( '更新失败: ' + ( d . detail || d . message || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '更新失败: ' + e . message , 'error' ) } finally { btn . disabled = false ; btnText . textContent = '保存' ; btnSpinner . classList . add ( 'hidden' ) } } ,
convertST2AT = async ( ) => { const st = $ ( 'addTokenST' ) . value . trim ( ) ; if ( ! st ) return showToast ( '请先输入 Session Token' , 'error' ) ; try { showToast ( '正在转换 ST→AT...' , 'info' ) ; const r = await apiRequest ( '/api/tokens/st2at' , { method : 'POST' , body : JSON . stringify ( { st : st } ) } ) ; if ( ! r ) return ; const d = await r . json ( ) ; if ( d . success && d . access _token ) { $ ( 'addTokenAT' ) . value = d . access _token ; showToast ( '转换成功! AT已自动填入' , 'success' ) } else { showToast ( '转换失败: ' + ( d . message || d . detail || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '转换失败: ' + e . message , 'error' ) } } ,
convertRT2AT = async ( ) => { const rt = $ ( 'addTokenRT' ) . value . trim ( ) ; if ( ! rt ) return showToast ( '请先输入 Refresh Token' , 'error' ) ; const hint = $ ( 'addRTRefreshHint' ) ; hint . classList . add ( 'hidden' ) ; try { showToast ( '正在转换 RT→AT...' , 'info' ) ; const r = await apiRequest ( '/api/tokens/rt2at' , { method : 'POST' , body : JSON . stringify ( { rt : rt } ) } ) ; if ( ! r ) return ; const d = await r . json ( ) ; if ( d . success && d . access _token ) { $ ( 'addTokenAT' ) . value = d . access _token ; if ( d . refresh _token ) { $ ( 'addTokenRT' ) . value = d . refresh _token ; hint . classList . remove ( 'hidden' ) ; showToast ( '转换成功! AT已自动填入, RT已被刷新并更新' , 'success' ) } else { showToast ( '转换成功! AT已自动填入' , 'success' ) } } else { showToast ( '转换失败: ' + ( d . message || d . detail || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '转换失败: ' + e . message , 'error' ) } } ,
convertEditST2AT = async ( ) => { const st = $ ( 'editTokenST' ) . value . trim ( ) ; if ( ! st ) return showToast ( '请先输入 Session Token' , 'error' ) ; try { showToast ( '正在转换 ST→AT...' , 'info' ) ; const r = await apiRequest ( '/api/tokens/st2at' , { method : 'POST' , body : JSON . stringify ( { st : st } ) } ) ; if ( ! r ) return ; const d = await r . json ( ) ; if ( d . success && d . access _token ) { $ ( 'editTokenAT' ) . value = d . access _token ; showToast ( '转换成功! AT已自动填入' , 'success' ) } else { showToast ( '转换失败: ' + ( d . message || d . detail || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '转换失败: ' + e . message , 'error' ) } } ,
convertEditRT2AT = async ( ) => { const rt = $ ( 'editTokenRT' ) . value . trim ( ) ; if ( ! rt ) return showToast ( '请先输入 Refresh Token' , 'error' ) ; const hint = $ ( 'editRTRefreshHint' ) ; hint . classList . add ( 'hidden' ) ; try { showToast ( '正在转换 RT→AT...' , 'info' ) ; const r = await apiRequest ( '/api/tokens/rt2at' , { method : 'POST' , body : JSON . stringify ( { rt : rt } ) } ) ; if ( ! r ) return ; const d = await r . json ( ) ; if ( d . success && d . access _token ) { $ ( 'editTokenAT' ) . value = d . access _token ; if ( d . refresh _token ) { $ ( 'editTokenRT' ) . value = d . refresh _token ; hint . classList . remove ( 'hidden' ) ; showToast ( '转换成功! AT已自动填入, RT已被刷新并更新' , 'success' ) } else { showToast ( '转换成功! AT已自动填入' , 'success' ) } } else { showToast ( '转换失败: ' + ( d . message || d . detail || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '转换失败: ' + e . message , 'error' ) } } ,
submitAddToken = async ( ) => { const at = $ ( 'addTokenAT' ) . value . trim ( ) , st = $ ( 'addTokenST' ) . value . trim ( ) , rt = $ ( 'addTokenRT' ) . value . trim ( ) , remark = $ ( 'addTokenRemark' ) . value . trim ( ) , imageEnabled = $ ( 'addTokenImageEnabled' ) . checked , videoEnabled = $ ( 'addTokenVideoEnabled' ) . checked , imageConcurrency = parseInt ( $ ( 'addTokenImageConcurrency' ) . value ) || ( - 1 ) , videoConcurrency = parseInt ( $ ( 'addTokenVideoConcurrency' ) . value ) || ( - 1 ) ; if ( ! at ) return showToast ( '请输入 Access Token 或使用 ST/RT 转换' , 'error' ) ; const btn = $ ( 'addTokenBtn' ) , btnText = $ ( 'addTokenBtnText' ) , btnSpinner = $ ( 'addTokenBtnSpinner' ) ; btn . disabled = true ; btnText . textContent = '添加中...' ; btnSpinner . classList . remove ( 'hidden' ) ; try { const r = await apiRequest ( '/api/tokens' , { method : 'POST' , body : JSON . stringify ( { token : at , st : st || null , rt : rt || null , remark : remark || null , image _enabled : imageEnabled , video _enabled : videoEnabled , image _concurrency : imageConcurrency , video _concurrency : videoConcurrency } ) } ) ; if ( ! r ) { btn . disabled = false ; btnText . textContent = '添加' ; btnSpinner . classList . add ( 'hidden' ) ; return } if ( r . status === 409 ) { const d = await r . json ( ) ; const msg = d . detail || 'Token 已存在' ; btn . disabled = false ; btnText . textContent = '添加' ; btnSpinner . classList . add ( 'hidden' ) ; if ( confirm ( msg + '\n\n是否删除旧 Token 后重新添加?' ) ) { const existingToken = allTokens . find ( t => t . token === at ) ; if ( existingToken ) { const deleted = await deleteToken ( existingToken . id , true ) ; if ( deleted ) { showToast ( '正在重新添加...' , 'info' ) ; setTimeout ( ( ) => submitAddToken ( ) , 500 ) } else { showToast ( '删除旧 Token 失败' , 'error' ) } } } return } const d = await r . json ( ) ; if ( d . success ) { closeAddModal ( ) ; await refreshTokens ( ) ; showToast ( 'Token添加成功' , 'success' ) } else { showToast ( '添加失败: ' + ( d . detail || d . message || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '添加失败: ' + e . message , 'error' ) } finally { btn . disabled = false ; btnText . textContent = '添加' ; btnSpinner . classList . add ( 'hidden' ) } } ,
submitAddToken = async ( ) => { const at = $ ( 'addTokenAT' ) . value . trim ( ) , st = $ ( 'addTokenST' ) . value . trim ( ) , rt = $ ( 'addTokenRT' ) . value . trim ( ) , clientId = $ ( 'addTokenClientId' ) . value . trim ( ) , remark= $ ( 'addTokenRemark' ) . value . trim ( ) , imageEnabled = $ ( 'addTokenImageEnabled' ) . checked , videoEnabled = $ ( 'addTokenVideoEnabled' ) . checked , imageConcurrency = parseInt ( $ ( 'addTokenImageConcurrency' ) . value ) || ( - 1 ) , videoConcurrency = parseInt ( $ ( 'addTokenVideoConcurrency' ) . value ) || ( - 1 ) ; if ( ! at ) return showToast ( '请输入 Access Token 或使用 ST/RT 转换' , 'error' ) ; const btn = $ ( 'addTokenBtn' ) , btnText = $ ( 'addTokenBtnText' ) , btnSpinner = $ ( 'addTokenBtnSpinner' ) ; btn . disabled = true ; btnText . textContent = '添加中...' ; btnSpinner . classList . remove ( 'hidden' ) ; try { const r = await apiRequest ( '/api/tokens' , { method : 'POST' , body : JSON . stringify ( { token : at , st : st || null , rt : rt || null , client _id : clientId || null , remark: remark || null , image _enabled : imageEnabled , video _enabled : videoEnabled , image _concurrency : imageConcurrency , video _concurrency : videoConcurrency } ) } ) ; if ( ! r ) { btn . disabled = false ; btnText . textContent = '添加' ; btnSpinner . classList . add ( 'hidden' ) ; return } if ( r . status === 409 ) { const d = await r . json ( ) ; const msg = d . detail || 'Token 已存在' ; btn . disabled = false ; btnText . textContent = '添加' ; btnSpinner . classList . add ( 'hidden' ) ; if ( confirm ( msg + '\n\n是否删除旧 Token 后重新添加?' ) ) { const existingToken = allTokens . find ( t => t . token === at ) ; if ( existingToken ) { const deleted = await deleteToken ( existingToken . id , true ) ; if ( deleted ) { showToast ( '正在重新添加...' , 'info' ) ; setTimeout ( ( ) => submitAddToken ( ) , 500 ) } else { showToast ( '删除旧 Token 失败' , 'error' ) } } } return } const d = await r . json ( ) ; if ( d . success ) { closeAddModal ( ) ; await refreshTokens ( ) ; showToast ( 'Token添加成功' , 'success' ) } else { showToast ( '添加失败: ' + ( d . detail || d . message || '未知错误' ) , 'error' ) } } catch ( e ) { showToast ( '添加失败: ' + e . message , 'error' ) } finally { btn . disabled = false ; btnText . textContent = '添加' ; btnSpinner . classList . add ( 'hidden' ) } } ,
testToken = async ( id ) => { try { showToast ( '正在测试Token...' , 'info' ) ; const r = await apiRequest ( ` /api/tokens/ ${ id } /test ` , { method : 'POST' } ) ; if ( ! r ) return ; const d = await r . json ( ) ; if ( d . success && d . status === 'success' ) { let msg = ` Token有效! 用户: ${ d . email || '未知' } ` ; if ( d . sora2 _supported ) { const remaining = d . sora2 _total _count - d . sora2 _redeemed _count ; msg += ` \n Sora2: 支持 ( ${ remaining } / ${ d . sora2 _total _count } ) ` ; if ( d . sora2 _remaining _count !== undefined ) { msg += ` \n 可用次数: ${ d . sora2 _remaining _count } ` } } showToast ( msg , 'success' ) ; await refreshTokens ( ) } else { showToast ( ` Token无效: ${ d . message || '未知错误' } ` , 'error' ) } } catch ( e ) { showToast ( '测试失败: ' + e . message , 'error' ) } } ,
toggleToken = async ( id , isActive ) => { const action = isActive ? 'disable' : 'enable' ; try { const r = await apiRequest ( ` /api/tokens/ ${ id } / ${ action } ` , { method : 'POST' } ) ; if ( ! r ) return ; const d = await r . json ( ) ; d . success ? ( await refreshTokens ( ) , showToast ( isActive ? 'Token已禁用' : 'Token已启用' , 'success' ) ) : showToast ( '操作失败' , 'error' ) } catch ( e ) { showToast ( '操作失败: ' + e . message , 'error' ) } } ,
toggleTokenStatus = async ( id , active ) => { try { const r = await apiRequest ( ` /api/tokens/ ${ id } /status ` , { method : 'PUT' , body : JSON . stringify ( { is _active : active } ) } ) ; if ( ! r ) return ; const d = await r . json ( ) ; d . success ? ( await refreshTokens ( ) , showToast ( '状态更新成功' , 'success' ) ) : showToast ( '更新失败' , 'error' ) } catch ( e ) { showToast ( '更新失败: ' + e . message , 'error' ) } } ,