Skip to content

Commit 71e22f5

Browse files
committed
feat(demo): 记住最后环境ID/API Key + 更新动态库功能 + 启动时询问环境
- 持久化存储最后使用的 env_id 和 API Key 到 ~/.brosdk-demo.json - 启动时自动恢复上次环境 ID,并在菜单显示 - 选项 7: 从 GitHub Releases 自动下载并安装最新动态库(带进度条) - 启动/关闭环境时询问是否使用上次环境 ID
1 parent 8fc054a commit 71e22f5

1 file changed

Lines changed: 261 additions & 4 deletions

File tree

demo.py

Lines changed: 261 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
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
----
@@ -22,10 +28,14 @@
2228
import logging
2329
import os
2430
import platform
31+
import shutil
2532
import sys
33+
import tarfile
2634
import tempfile
2735
import threading
2836
import time
37+
import urllib.request
38+
import zipfile
2939
from 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

69104
def _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

82284
class 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

Comments
 (0)