TheSmallHanCat commited on
Commit
35d960d
·
1 Parent(s): 4bb28f5

fix: 文件缓存

Browse files

feat: 自动更新st接口

requirements.txt CHANGED
@@ -7,4 +7,4 @@ tomli==2.2.1
7
  bcrypt==4.2.1
8
  python-multipart==0.0.20
9
  python-dateutil==2.8.2
10
- playwright==1.48.0
 
7
  bcrypt==4.2.1
8
  python-multipart==0.0.20
9
  python-dateutil==2.8.2
10
+ playwright==1.53.0
src/api/admin.py CHANGED
@@ -878,3 +878,137 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
878
  "browser_proxy_enabled": captcha_config.browser_proxy_enabled,
879
  "browser_proxy_url": captcha_config.browser_proxy_url or ""
880
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
878
  "browser_proxy_enabled": captcha_config.browser_proxy_enabled,
879
  "browser_proxy_url": captcha_config.browser_proxy_url or ""
880
  }
881
+
882
+
883
+ # ========== Plugin Configuration Endpoints ==========
884
+
885
+ @router.get("/api/plugin/config")
886
+ async def get_plugin_config(token: str = Depends(verify_admin_token)):
887
+ """Get plugin configuration"""
888
+ plugin_config = await db.get_plugin_config()
889
+
890
+ # Get server host and port from config
891
+ from ..core.config import config
892
+ server_host = config.server_host
893
+ server_port = config.server_port
894
+
895
+ # Generate connection URL
896
+ if server_host == "0.0.0.0":
897
+ connection_url = f"http://127.0.0.1:{server_port}/api/plugin/update-token"
898
+ else:
899
+ connection_url = f"http://{server_host}:{server_port}/api/plugin/update-token"
900
+
901
+ return {
902
+ "success": True,
903
+ "config": {
904
+ "connection_token": plugin_config.connection_token,
905
+ "connection_url": connection_url
906
+ }
907
+ }
908
+
909
+
910
+ @router.post("/api/plugin/config")
911
+ async def update_plugin_config(
912
+ request: dict,
913
+ token: str = Depends(verify_admin_token)
914
+ ):
915
+ """Update plugin configuration"""
916
+ connection_token = request.get("connection_token", "")
917
+
918
+ # Generate random token if empty
919
+ if not connection_token:
920
+ connection_token = secrets.token_urlsafe(32)
921
+
922
+ await db.update_plugin_config(connection_token=connection_token)
923
+
924
+ return {
925
+ "success": True,
926
+ "message": "插件配置更新成功",
927
+ "connection_token": connection_token
928
+ }
929
+
930
+
931
+ @router.post("/api/plugin/update-token")
932
+ async def plugin_update_token(request: dict, authorization: Optional[str] = Header(None)):
933
+ """Receive token update from Chrome extension (no admin auth required, uses connection_token)"""
934
+ # Verify connection token
935
+ plugin_config = await db.get_plugin_config()
936
+
937
+ # Extract token from Authorization header
938
+ provided_token = None
939
+ if authorization:
940
+ if authorization.startswith("Bearer "):
941
+ provided_token = authorization[7:]
942
+ else:
943
+ provided_token = authorization
944
+
945
+ # Check if token matches
946
+ if not plugin_config.connection_token or provided_token != plugin_config.connection_token:
947
+ raise HTTPException(status_code=401, detail="Invalid connection token")
948
+
949
+ # Extract session token from request
950
+ session_token = request.get("session_token")
951
+
952
+ if not session_token:
953
+ raise HTTPException(status_code=400, detail="Missing session_token")
954
+
955
+ # Step 1: Convert ST to AT to get user info (including email)
956
+ try:
957
+ result = await token_manager.flow_client.st_to_at(session_token)
958
+ at = result["access_token"]
959
+ expires = result.get("expires")
960
+ user_info = result.get("user", {})
961
+ email = user_info.get("email", "")
962
+
963
+ if not email:
964
+ raise HTTPException(status_code=400, detail="Failed to get email from session token")
965
+
966
+ # Parse expiration time
967
+ from datetime import datetime
968
+ at_expires = None
969
+ if expires:
970
+ try:
971
+ at_expires = datetime.fromisoformat(expires.replace('Z', '+00:00'))
972
+ except:
973
+ pass
974
+
975
+ except Exception as e:
976
+ raise HTTPException(status_code=400, detail=f"Invalid session token: {str(e)}")
977
+
978
+ # Step 2: Check if token with this email exists
979
+ existing_token = await db.get_token_by_email(email)
980
+
981
+ if existing_token:
982
+ # Update existing token
983
+ try:
984
+ # Update token
985
+ await token_manager.update_token(
986
+ token_id=existing_token.id,
987
+ st=session_token,
988
+ at=at,
989
+ at_expires=at_expires
990
+ )
991
+
992
+ return {
993
+ "success": True,
994
+ "message": f"Token updated for {email}",
995
+ "action": "updated"
996
+ }
997
+ except Exception as e:
998
+ raise HTTPException(status_code=500, detail=f"Failed to update token: {str(e)}")
999
+ else:
1000
+ # Add new token
1001
+ try:
1002
+ new_token = await token_manager.add_token(
1003
+ st=session_token,
1004
+ remark="Added by Chrome Extension"
1005
+ )
1006
+
1007
+ return {
1008
+ "success": True,
1009
+ "message": f"Token added for {new_token.email}",
1010
+ "action": "added",
1011
+ "token_id": new_token.id
1012
+ }
1013
+ except Exception as e:
1014
+ raise HTTPException(status_code=500, detail=f"Failed to add token: {str(e)}")
src/core/database.py CHANGED
@@ -4,7 +4,7 @@ import json
4
  from datetime import datetime
5
  from typing import Optional, List
6
  from pathlib import Path
7
- from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project, CaptchaConfig
8
 
9
 
10
  class Database:
@@ -167,6 +167,15 @@ class Database:
167
  VALUES (1, ?, ?, ?)
168
  """, (captcha_method, yescaptcha_api_key, yescaptcha_base_url))
169
 
 
 
 
 
 
 
 
 
 
170
  async def check_and_migrate_db(self, config_dict: dict = None):
171
  """Check database integrity and perform migrations if needed
172
 
@@ -216,6 +225,18 @@ class Database:
216
  )
217
  """)
218
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  # ========== Step 2: Add missing columns to existing tables ==========
220
  # Check and add missing columns to tokens table
221
  if await self._table_exists(db, "tokens"):
@@ -463,6 +484,16 @@ class Database:
463
  )
464
  """)
465
 
 
 
 
 
 
 
 
 
 
 
466
  # Create indexes
467
  await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
468
  await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
@@ -572,6 +603,16 @@ class Database:
572
  return Token(**dict(row))
573
  return None
574
 
 
 
 
 
 
 
 
 
 
 
575
  async def get_all_tokens(self) -> List[Token]:
576
  """Get all tokens"""
577
  async with aiosqlite.connect(self.db_path) as db:
@@ -1174,3 +1215,35 @@ class Database:
1174
  """, (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
1175
 
1176
  await db.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from datetime import datetime
5
  from typing import Optional, List
6
  from pathlib import Path
7
+ from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project, CaptchaConfig, PluginConfig
8
 
9
 
10
  class Database:
 
167
  VALUES (1, ?, ?, ?)
168
  """, (captcha_method, yescaptcha_api_key, yescaptcha_base_url))
169
 
170
+ # Ensure plugin_config has a row
171
+ cursor = await db.execute("SELECT COUNT(*) FROM plugin_config")
172
+ count = await cursor.fetchone()
173
+ if count[0] == 0:
174
+ await db.execute("""
175
+ INSERT INTO plugin_config (id, connection_token)
176
+ VALUES (1, '')
177
+ """)
178
+
179
  async def check_and_migrate_db(self, config_dict: dict = None):
180
  """Check database integrity and perform migrations if needed
181
 
 
225
  )
226
  """)
227
 
228
+ # Check and create plugin_config table if missing
229
+ if not await self._table_exists(db, "plugin_config"):
230
+ print(" ✓ Creating missing table: plugin_config")
231
+ await db.execute("""
232
+ CREATE TABLE plugin_config (
233
+ id INTEGER PRIMARY KEY DEFAULT 1,
234
+ connection_token TEXT DEFAULT '',
235
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
236
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
237
+ )
238
+ """)
239
+
240
  # ========== Step 2: Add missing columns to existing tables ==========
241
  # Check and add missing columns to tokens table
242
  if await self._table_exists(db, "tokens"):
 
484
  )
485
  """)
486
 
487
+ # Plugin config table
488
+ await db.execute("""
489
+ CREATE TABLE IF NOT EXISTS plugin_config (
490
+ id INTEGER PRIMARY KEY DEFAULT 1,
491
+ connection_token TEXT DEFAULT '',
492
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
493
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
494
+ )
495
+ """)
496
+
497
  # Create indexes
498
  await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
499
  await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
 
603
  return Token(**dict(row))
604
  return None
605
 
606
+ async def get_token_by_email(self, email: str) -> Optional[Token]:
607
+ """Get token by email"""
608
+ async with aiosqlite.connect(self.db_path) as db:
609
+ db.row_factory = aiosqlite.Row
610
+ cursor = await db.execute("SELECT * FROM tokens WHERE email = ?", (email,))
611
+ row = await cursor.fetchone()
612
+ if row:
613
+ return Token(**dict(row))
614
+ return None
615
+
616
  async def get_all_tokens(self) -> List[Token]:
617
  """Get all tokens"""
618
  async with aiosqlite.connect(self.db_path) as db:
 
1215
  """, (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
1216
 
1217
  await db.commit()
1218
+
1219
+ # Plugin config operations
1220
+ async def get_plugin_config(self) -> PluginConfig:
1221
+ """Get plugin configuration"""
1222
+ async with aiosqlite.connect(self.db_path) as db:
1223
+ db.row_factory = aiosqlite.Row
1224
+ cursor = await db.execute("SELECT * FROM plugin_config WHERE id = 1")
1225
+ row = await cursor.fetchone()
1226
+ if row:
1227
+ return PluginConfig(**dict(row))
1228
+ return PluginConfig()
1229
+
1230
+ async def update_plugin_config(self, connection_token: str):
1231
+ """Update plugin configuration"""
1232
+ async with aiosqlite.connect(self.db_path) as db:
1233
+ db.row_factory = aiosqlite.Row
1234
+ cursor = await db.execute("SELECT * FROM plugin_config WHERE id = 1")
1235
+ row = await cursor.fetchone()
1236
+
1237
+ if row:
1238
+ await db.execute("""
1239
+ UPDATE plugin_config
1240
+ SET connection_token = ?, updated_at = CURRENT_TIMESTAMP
1241
+ WHERE id = 1
1242
+ """, (connection_token,))
1243
+ else:
1244
+ await db.execute("""
1245
+ INSERT INTO plugin_config (id, connection_token)
1246
+ VALUES (1, ?)
1247
+ """, (connection_token,))
1248
+
1249
+ await db.commit()
src/core/models.py CHANGED
@@ -158,6 +158,14 @@ class CaptchaConfig(BaseModel):
158
  updated_at: Optional[datetime] = None
159
 
160
 
 
 
 
 
 
 
 
 
161
  # OpenAI Compatible Request Models
162
  class ChatMessage(BaseModel):
163
  """Chat message"""
 
158
  updated_at: Optional[datetime] = None
159
 
160
 
161
+ class PluginConfig(BaseModel):
162
+ """Plugin connection configuration"""
163
+ id: int = 1
164
+ connection_token: str = "" # 插件连接token
165
+ created_at: Optional[datetime] = None
166
+ updated_at: Optional[datetime] = None
167
+
168
+
169
  # OpenAI Compatible Request Models
170
  class ChatMessage(BaseModel):
171
  """Chat message"""
src/services/file_cache.py CHANGED
@@ -131,28 +131,130 @@ class FileCache:
131
  # Download file
132
  debug_logger.log_info(f"Downloading file from: {url}")
133
 
 
 
 
 
 
 
 
 
134
  try:
135
- # Get proxy if available
136
- proxy_url = None
137
- if self.proxy_manager:
138
- proxy_config = await self.proxy_manager.get_proxy_config()
139
- if proxy_config and proxy_config.enabled and proxy_config.proxy_url:
140
- proxy_url = proxy_config.proxy_url
141
-
142
- # Download with proxy support
143
  async with AsyncSession() as session:
144
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
145
- response = await session.get(url, timeout=60, proxies=proxies)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- if response.status_code != 200:
148
- raise Exception(f"Download failed: HTTP {response.status_code}")
149
 
150
- # Save to cache
151
- with open(file_path, 'wb') as f:
152
- f.write(response.content)
153
 
154
- debug_logger.log_info(f"File cached: {filename} ({len(response.content)} bytes)")
155
- return filename
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  except Exception as e:
158
  debug_logger.log_error(
 
131
  # Download file
132
  debug_logger.log_info(f"Downloading file from: {url}")
133
 
134
+ # Get proxy if available
135
+ proxy_url = None
136
+ if self.proxy_manager:
137
+ proxy_config = await self.proxy_manager.get_proxy_config()
138
+ if proxy_config and proxy_config.enabled and proxy_config.proxy_url:
139
+ proxy_url = proxy_config.proxy_url
140
+
141
+ # Try method 1: curl_cffi with browser impersonation
142
  try:
 
 
 
 
 
 
 
 
143
  async with AsyncSession() as session:
144
  proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
145
+ headers = {
146
+ "Accept": "*/*",
147
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
148
+ "Accept-Encoding": "gzip, deflate, br",
149
+ "Connection": "keep-alive",
150
+ "Sec-Fetch-Dest": "document",
151
+ "Sec-Fetch-Mode": "navigate",
152
+ "Sec-Fetch-Site": "none",
153
+ "Upgrade-Insecure-Requests": "1"
154
+ }
155
+ response = await session.get(
156
+ url,
157
+ timeout=60,
158
+ proxies=proxies,
159
+ headers=headers,
160
+ impersonate="chrome120",
161
+ verify=False
162
+ )
163
+
164
+ if response.status_code == 200:
165
+ with open(file_path, 'wb') as f:
166
+ f.write(response.content)
167
+ debug_logger.log_info(f"File cached (curl_cffi): {filename} ({len(response.content)} bytes)")
168
+ return filename
169
+ else:
170
+ debug_logger.log_warning(f"curl_cffi failed with HTTP {response.status_code}, trying wget...")
171
 
172
+ except Exception as e:
173
+ debug_logger.log_warning(f"curl_cffi failed: {str(e)}, trying wget...")
174
 
175
+ # Try method 2: wget command
176
+ try:
177
+ import subprocess
178
 
179
+ wget_cmd = [
180
+ "wget",
181
+ "-q", # Quiet mode
182
+ "-O", str(file_path), # Output file
183
+ "--timeout=60",
184
+ "--tries=3",
185
+ "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
186
+ "--header=Accept: */*",
187
+ "--header=Accept-Language: zh-CN,zh;q=0.9,en;q=0.8",
188
+ "--header=Connection: keep-alive"
189
+ ]
190
+
191
+ # Add proxy if configured
192
+ if proxy_url:
193
+ # wget uses environment variables for proxy
194
+ env = os.environ.copy()
195
+ env['http_proxy'] = proxy_url
196
+ env['https_proxy'] = proxy_url
197
+ else:
198
+ env = None
199
+
200
+ # Add URL
201
+ wget_cmd.append(url)
202
+
203
+ # Execute wget
204
+ result = subprocess.run(wget_cmd, capture_output=True, timeout=90, env=env)
205
+
206
+ if result.returncode == 0 and file_path.exists():
207
+ file_size = file_path.stat().st_size
208
+ if file_size > 0:
209
+ debug_logger.log_info(f"File cached (wget): {filename} ({file_size} bytes)")
210
+ return filename
211
+ else:
212
+ raise Exception("Downloaded file is empty")
213
+ else:
214
+ error_msg = result.stderr.decode('utf-8', errors='ignore') if result.stderr else "Unknown error"
215
+ debug_logger.log_warning(f"wget failed: {error_msg}, trying curl...")
216
+
217
+ except FileNotFoundError:
218
+ debug_logger.log_warning("wget not found, trying curl...")
219
+ except Exception as e:
220
+ debug_logger.log_warning(f"wget failed: {str(e)}, trying curl...")
221
+
222
+ # Try method 3: system curl command
223
+ try:
224
+ import subprocess
225
+
226
+ curl_cmd = [
227
+ "curl",
228
+ "-L", # Follow redirects
229
+ "-s", # Silent mode
230
+ "-o", str(file_path), # Output file
231
+ "--max-time", "60",
232
+ "-H", "Accept: */*",
233
+ "-H", "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8",
234
+ "-H", "Connection: keep-alive",
235
+ "-A", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
236
+ ]
237
+
238
+ # Add proxy if configured
239
+ if proxy_url:
240
+ curl_cmd.extend(["-x", proxy_url])
241
+
242
+ # Add URL
243
+ curl_cmd.append(url)
244
+
245
+ # Execute curl
246
+ result = subprocess.run(curl_cmd, capture_output=True, timeout=90)
247
+
248
+ if result.returncode == 0 and file_path.exists():
249
+ file_size = file_path.stat().st_size
250
+ if file_size > 0:
251
+ debug_logger.log_info(f"File cached (curl): {filename} ({file_size} bytes)")
252
+ return filename
253
+ else:
254
+ raise Exception("Downloaded file is empty")
255
+ else:
256
+ error_msg = result.stderr.decode('utf-8', errors='ignore') if result.stderr else "Unknown error"
257
+ raise Exception(f"curl command failed: {error_msg}")
258
 
259
  except Exception as e:
260
  debug_logger.log_error(
static/manage.html CHANGED
@@ -319,6 +319,35 @@
319
  </div>
320
  </div>
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  <!-- 生成超时配置 -->
323
  <div class="rounded-lg border border-border bg-background p-6">
324
  <h3 class="text-lg font-semibold mb-4">生成超时配置</h3>
@@ -637,13 +666,17 @@
637
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
638
  loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
639
  saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,apiKey=$('cfgYescaptchaApiKey').value.trim(),baseUrl=$('cfgYescaptchaBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,apiKey,baseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:apiKey,yescaptcha_base_url:baseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
 
 
 
 
640
  toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
641
  loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
642
  loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
643
  refreshLogs=async()=>{await loadLogs()},
644
  showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
645
  logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
646
- switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadCacheConfig();loadGenerationTimeout();loadCaptchaConfig();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
647
  window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();loadATAutoRefreshConfig()});
648
  </script>
649
  </body>
 
319
  </div>
320
  </div>
321
 
322
+ <!-- 插件连接配置 -->
323
+ <div class="rounded-lg border border-border bg-background p-6">
324
+ <h3 class="text-lg font-semibold mb-4">插件连接配置</h3>
325
+ <div class="space-y-4">
326
+ <div>
327
+ <label class="text-sm font-semibold mb-2 block">连接接口</label>
328
+ <div class="flex gap-2">
329
+ <input id="cfgPluginConnectionUrl" type="text" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm" placeholder="加载中...">
330
+ <button onclick="copyConnectionUrl()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
331
+ </div>
332
+ <p class="text-xs text-muted-foreground mt-1">Chrome扩展插件需要配置此接口地址</p>
333
+ </div>
334
+ <div>
335
+ <label class="text-sm font-semibold mb-2 block">连接Token</label>
336
+ <div class="flex gap-2">
337
+ <input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
338
+ <button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
339
+ </div>
340
+ <p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
341
+ </div>
342
+ <div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
343
+ <p class="text-xs text-blue-800 dark:text-blue-200">
344
+ ℹ️ <strong>使用说明:</strong>安装Chrome扩展后,将连接接口和Token配置到插件中,插件会自动提取Google Labs的cookie并更新到系统
345
+ </p>
346
+ </div>
347
+ <button onclick="savePluginConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
348
+ </div>
349
+ </div>
350
+
351
  <!-- 生成超时配置 -->
352
  <div class="rounded-lg border border-border bg-background p-6">
353
  <h3 class="text-lg font-semibold mb-4">生成超时配置</h3>
 
666
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
667
  loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
668
  saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,apiKey=$('cfgYescaptchaApiKey').value.trim(),baseUrl=$('cfgYescaptchaBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,apiKey,baseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:apiKey,yescaptcha_base_url:baseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
669
+ loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||''}}catch(e){console.error('加载插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
670
+ savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
671
+ copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
672
+ copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
673
  toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
674
  loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
675
  loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
676
  refreshLogs=async()=>{await loadLogs()},
677
  showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
678
  logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
679
+ switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadCacheConfig();loadGenerationTimeout();loadCaptchaConfig();loadPluginConfig();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
680
  window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();loadATAutoRefreshConfig()});
681
  </script>
682
  </body>