99 2. 查看/选择环境列表
1010 3. 创建新环境
1111 4. 启动 / 关闭浏览器环境
12+ 5. 更新动态库(从 GitHub Releases 下载)
13+
14+ 特性:
15+ - 记住最后一次使用的环境 ID 和 API Key(持久化到 ~/.brosdk-demo.json)
16+ - 支持从 GitHub Releases 自动下载并安装最新版本动态库
17+ - 下载进度条显示
1218
1319用法
1420----
2228import logging
2329import os
2430import platform
31+ import shutil
2532import sys
33+ import tarfile
2634import tempfile
2735import threading
2836import time
37+ import urllib .request
38+ import zipfile
2939from typing import Optional
3040
3141# 颜色输出支持(Windows 需要额外处理)
@@ -64,6 +74,31 @@ def _ts() -> str:
6474 return time .strftime ("%H:%M:%S" )
6575
6676
77+ # ── 持久化配置(记住最后环境 ID / API Key) ────────────────────────────────
78+
79+ _CONFIG_FILE = os .path .join (os .path .expanduser ("~" ), ".brosdk-demo.json" )
80+
81+
82+ def _load_config () -> dict :
83+ """加载持久化配置文件。"""
84+ if os .path .exists (_CONFIG_FILE ):
85+ try :
86+ with open (_CONFIG_FILE , "r" , encoding = "utf-8" ) as f :
87+ return json .load (f )
88+ except Exception :
89+ pass
90+ return {}
91+
92+
93+ def _save_config (cfg : dict ) -> None :
94+ """保存持久化配置文件。"""
95+ try :
96+ with open (_CONFIG_FILE , "w" , encoding = "utf-8" ) as f :
97+ json .dump (cfg , f , ensure_ascii = False , indent = 2 )
98+ except Exception :
99+ pass
100+
101+
67102# ── 默认库路径 ────────────────────────────────────────────────────────────────
68103
69104def _default_lib_path () -> str :
@@ -77,6 +112,173 @@ def _default_lib_path() -> str:
77112 return os .path .join (base , "libs" , "linux-x64" , "libbrosdk.so" )
78113
79114
115+ # ── Lib 更新下载 ─────────────────────────────────────────────────────────────
116+
117+ _GITHUB_RELEASES_API = "https://api.github.com/repos/browsersdk/brosdk-sdk/releases/latest"
118+
119+ # 平台 → asset 文件名模板({version} 占位)
120+ _PLATFORM_ASSET = {
121+ ("Windows" , "AMD64" ): "brosdk-{version}-windows-x64.zip" ,
122+ ("Darwin" , "ARM64" ): "brosdk-{version}-darwin-arm64.tar.gz" ,
123+ ("Darwin" , "x86_64" ): "brosdk-{version}-darwin-arm64.tar.gz" ,
124+ ("Linux" , "x86_64" ): "brosdk-{version}-linux-x64.tar.gz" ,
125+ }
126+
127+
128+ def _detect_platform_asset () -> str :
129+ """检测当前平台对应的 asset 文件名模板。"""
130+ system = platform .system ()
131+ machine = platform .machine ()
132+ key = (system , machine )
133+ if key in _PLATFORM_ASSET :
134+ return _PLATFORM_ASSET [key ]
135+ # 回退
136+ if system == "Windows" :
137+ return "brosdk-{version}-windows-x64.zip"
138+ elif system == "Darwin" :
139+ return "brosdk-{version}-darwin-arm64.tar.gz"
140+ return "brosdk-{version}-linux-x64.tar.gz"
141+
142+
143+ def _extract_zip (zip_path : str , dest_dir : str ) -> None :
144+ """解压 zip 文件到目标目录。"""
145+ with zipfile .ZipFile (zip_path , "r" ) as zf :
146+ zf .extractall (dest_dir )
147+
148+
149+ def _extract_tar (tar_path : str , dest_dir : str ) -> None :
150+ """解压 tar.gz 文件到目标目录。"""
151+ with tarfile .open (tar_path , "r:gz" ) as tf :
152+ tf .extractall (dest_dir )
153+
154+
155+ def step_update_lib (lib_path_hint : str = "" ) -> bool :
156+ """
157+ 从 GitHub Releases 下载最新版本的 brosdk 动态库。
158+
159+ :param lib_path_hint: 当前使用的 lib 路径,用于推断 libs/ 目录位置。
160+ :return: 是否成功更新。
161+ """
162+ print ()
163+ print (bold ("═══ 更新 brosdk 动态库 ═══" ))
164+
165+ # 1. 获取最新版本信息
166+ asset_template = _detect_platform_asset ()
167+ log_info ("正在查询最新版本..." )
168+ try :
169+ req = urllib .request .Request (_GITHUB_RELEASES_API )
170+ req .add_header ("User-Agent" , "brosdk-python-demo" )
171+ req .add_header ("Accept" , "application/vnd.github+json" )
172+ with urllib .request .urlopen (req , timeout = 15 ) as resp :
173+ release = json .loads (resp .read ().decode ("utf-8" ))
174+ except Exception as e :
175+ log_err (f"获取 Release 信息失败: { e } " )
176+ return False
177+
178+ tag = release .get ("tag_name" , "" )
179+ version = tag .lstrip ("v" )
180+ log_info (f"最新版本: { tag } " )
181+
182+ if not version :
183+ log_err ("无法解析版本号" )
184+ return False
185+
186+ # 2. 找到匹配当前平台的 asset
187+ asset_name = asset_template .format (version = version )
188+ asset_url = None
189+ for a in release .get ("assets" , []):
190+ if a .get ("name" , "" ) == asset_name :
191+ asset_url = a .get ("browser_download_url" , "" )
192+ break
193+
194+ if not asset_url :
195+ log_err (f"未找到适配当前平台的资产: { asset_name } " )
196+ log_info ("可用资产:" )
197+ for a in release .get ("assets" , []):
198+ print (f" - { a .get ('name' , '' )} " )
199+ return False
200+
201+ log_info (f"下载: { asset_name } " )
202+
203+ # 3. 确定 libs/ 目录
204+ project_dir = os .path .dirname (os .path .abspath (__file__ ))
205+ libs_dir = os .path .join (project_dir , "libs" )
206+ os .makedirs (libs_dir , exist_ok = True )
207+
208+ # 4. 下载到临时文件
209+ tmp_dir = tempfile .mkdtemp (prefix = "brosdk-update-" )
210+ try :
211+ tmp_file = os .path .join (tmp_dir , asset_name )
212+ log_info ("正在下载..." )
213+ try :
214+ urllib .request .urlretrieve (asset_url , tmp_file , reporthook = _download_progress )
215+ except Exception as e :
216+ log_err (f"下载失败: { e } " )
217+ return False
218+ print () # 进度条后换行
219+
220+ # 5. 解压
221+ log_info ("正在解压..." )
222+ extract_dir = os .path .join (tmp_dir , "extract" )
223+ os .makedirs (extract_dir , exist_ok = True )
224+
225+ if asset_name .endswith (".zip" ):
226+ _extract_zip (tmp_file , extract_dir )
227+ else :
228+ _extract_tar (tmp_file , extract_dir )
229+
230+ # 6. 找到解压后的动态库文件并复制到 libs/ 对应子目录
231+ lib_patterns = ["brosdk.dll" , "brosdk.dylib" , "libbrosdk.so" ]
232+ found_libs = []
233+ for root , _dirs , files in os .walk (extract_dir ):
234+ for fname in files :
235+ if fname in lib_patterns :
236+ found_libs .append (os .path .join (root , fname ))
237+
238+ if not found_libs :
239+ log_err ("解压后未找到动态库文件" )
240+ return False
241+
242+ for lib_file in found_libs :
243+ fname = os .path .basename (lib_file )
244+ # 推断平台子目录
245+ if fname == "brosdk.dll" :
246+ subdir = "windows-x64"
247+ elif fname == "brosdk.dylib" :
248+ subdir = "macos-arm64"
249+ else :
250+ subdir = "linux-x64"
251+ target_dir = os .path .join (libs_dir , subdir )
252+ os .makedirs (target_dir , exist_ok = True )
253+ target = os .path .join (target_dir , fname )
254+ shutil .copy2 (lib_file , target )
255+ log_ok (f"已安装: { os .path .relpath (target , project_dir )} " )
256+
257+ log_ok (f"动态库已更新到 { version } " )
258+ return True
259+
260+ except Exception as e :
261+ log_err (f"更新失败: { e } " )
262+ return False
263+ finally :
264+ shutil .rmtree (tmp_dir , ignore_errors = True )
265+
266+
267+ def _download_progress (block_num : int , block_size : int , total_size : int ) -> None :
268+ """下载进度回调。"""
269+ if total_size <= 0 :
270+ return
271+ downloaded = block_num * block_size
272+ pct = min (downloaded * 100 // total_size , 100 )
273+ mb_down = downloaded / (1024 * 1024 )
274+ mb_total = total_size / (1024 * 1024 )
275+ bar_len = 30
276+ filled = int (bar_len * pct / 100 )
277+ bar = "█" * filled + "░" * (bar_len - filled )
278+ sys .stdout .write (f"\r 下载中: [{ bar } ] { pct } % ({ mb_down :.1f} /{ mb_total :.1f} MB)" )
279+ sys .stdout .flush ()
280+
281+
80282# ── 主演示类 ──────────────────────────────────────────────────────────────────
81283
82284class BrosdkDemo :
@@ -95,6 +297,27 @@ def __init__(self, api_key: str = "", lib_path: str = "") -> None:
95297 self ._event_lock = threading .Lock ()
96298 self ._pending_events : list = []
97299
300+ # 从持久化配置恢复
301+ cfg = _load_config ()
302+ self .last_env_id : str = cfg .get ("last_env_id" , "" )
303+ if not self .api_key :
304+ self .api_key = cfg .get ("api_key" , "" )
305+
306+ # ── 持久化 ────────────────────────────────────────────────────────────────
307+
308+ def _save_env (self , env_id : str ) -> None :
309+ """记住最后一次使用的环境 ID。"""
310+ self .last_env_id = env_id
311+ cfg = _load_config ()
312+ cfg ["last_env_id" ] = env_id
313+ _save_config (cfg )
314+
315+ def _save_api_key (self , key : str ) -> None :
316+ """记住 API Key。"""
317+ cfg = _load_config ()
318+ cfg ["api_key" ] = key
319+ _save_config (cfg )
320+
98321 # ── 事件处理 ──────────────────────────────────────────────────────────────
99322
100323 def _on_sdk_event (self , event ) -> None :
@@ -134,6 +357,9 @@ def step_init_sdk(self) -> bool:
134357 log_err ("API Key 不能为空" )
135358 return False
136359
360+ # 持久化 API Key
361+ self ._save_api_key (self .api_key )
362+
137363 # 创建 API 客户端
138364 self .api_client = BrosdkApiClient (api_key = self .api_key )
139365
@@ -344,7 +570,7 @@ def step_start_env(self, env_id: str) -> None:
344570 config = json .dumps ({
345571 "envs" : [{
346572 "envId" : env_id ,
347- "args" : ["--no-first-run" , "--no-default-browser-check" , "--client-appid=test" ],
573+ "args" : ["--no-first-run" , "--no-default-browser-check" ],
348574 }]
349575 })
350576 log_info (f"正在启动环境: { env_id } " )
@@ -410,9 +636,11 @@ def run_interactive(self) -> None:
410636 print (bold (cyan ("╔══════════════════════════════════════╗" )))
411637 print (bold (cyan ("║ Brosdk SDK Python Demo ║" )))
412638 print (bold (cyan ("╚══════════════════════════════════════╝" )))
639+ if self .last_env_id :
640+ print (f" { dim ('上次使用的环境:' )} { cyan (self .last_env_id )} " )
413641 print ()
414642
415- current_env_id = ""
643+ current_env_id = self . last_env_id
416644
417645 while True :
418646 self ._flush_events ()
@@ -427,11 +655,12 @@ def run_interactive(self) -> None:
427655 print (f" { bold ('4.' )} 启动浏览器环境" )
428656 print (f" { bold ('5.' )} 关闭浏览器环境" )
429657 print (f" { bold ('6.' )} 查看 SDK 信息" )
658+ print (f" { bold ('7.' )} 更新动态库 { dim ('(从 GitHub Releases 下载)' )} " )
430659 print (f" { bold ('q.' )} 退出" )
431660 print ()
432661
433662 try :
434- choice = input (f" { bold ('请选择操作' )} [1-6 /q]: " ).strip ().lower ()
663+ choice = input (f" { bold ('请选择操作' )} [1-7 /q]: " ).strip ().lower ()
435664 except (EOFError , KeyboardInterrupt ):
436665 print ()
437666 break
@@ -446,16 +675,41 @@ def run_interactive(self) -> None:
446675 env_id = self .step_create_env ()
447676 if env_id :
448677 current_env_id = env_id
678+ self ._save_env (env_id )
449679
450680 elif choice == "4" :
451681 if not current_env_id :
452682 current_env_id = self ._get_env_id ()
683+ else :
684+ # 有记住的环境 ID,询问是否使用
685+ print ()
686+ try :
687+ ans = input (f" 使用上次环境 { cyan (current_env_id )} ?{ dim ('[Y/n]' )} : " ).strip ().lower ()
688+ except (EOFError , KeyboardInterrupt ):
689+ print ()
690+ continue
691+ if ans in ("n" , "no" ):
692+ picked = self ._get_env_id ()
693+ if picked :
694+ current_env_id = picked
453695 if current_env_id :
696+ self ._save_env (current_env_id )
454697 self .step_start_env (current_env_id )
455698
456699 elif choice == "5" :
457700 if not current_env_id :
458701 current_env_id = self ._get_env_id ()
702+ else :
703+ print ()
704+ try :
705+ ans = input (f" 使用上次环境 { cyan (current_env_id )} ?{ dim ('[Y/n]' )} : " ).strip ().lower ()
706+ except (EOFError , KeyboardInterrupt ):
707+ print ()
708+ continue
709+ if ans in ("n" , "no" ):
710+ picked = self ._get_env_id ()
711+ if picked :
712+ current_env_id = picked
459713 if current_env_id :
460714 self .step_stop_env (current_env_id )
461715
@@ -472,11 +726,14 @@ def run_interactive(self) -> None:
472726 except Exception as e :
473727 log_err (f"获取失败: { e } " )
474728
729+ elif choice == "7" :
730+ step_update_lib (self .lib_path )
731+
475732 elif choice in ("q" , "quit" , "exit" ):
476733 break
477734
478735 else :
479- log_warn ("无效选择,请输入 1-6 或 q" )
736+ log_warn ("无效选择,请输入 1-7 或 q" )
480737
481738 # 退出时关闭 SDK
482739 print ()
0 commit comments