64 KiB
基于 CDP 的远程浏览器实时流控与调试方案(Lime 落地版)
融合 NextBrowser 特征调查 + Codex 架构设计的务实实施方案
目标与原则
核心目标
在现有 chrome relay + cdp_direct 基础上,补齐双通道能力:
- 事件/控制通道: 基于 DevTools WebSocket 建立稳定 CDP 会话,实时收发命令、console、network、runtime、page lifecycle 事件
- 画面流通道: 基于
Page.startScreencast或受控截图轮询输出低延迟页面画面,用于任务执行中的实时观察与回放
设计原则
- ✅ 保持现有架构: 不改动后端优先级模型(aster_compat、lime_extension_bridge、cdp_direct 并存)
- ✅ 渐进式增强: 只升级 cdp_direct 从"HTTP 探测初版"到"有状态 CDP 会话后端"
- ✅ KISS/YAGNI: 不引入第四套浏览器控制通道,避免过度设计
- ✅ 跨平台兼容: macOS 与 Windows 都可运行,平台差异仅体现在 Chrome 启动和端口探测
- ✅ 统一抽象: 后端产出 session + event stream + frame stream + command result 抽象,前端只消费统一事件
一、NextBrowser 核心特征总结
1. Live View (实时视图) ⭐⭐⭐
功能描述:
- 在任务执行期间实时流式传输远程浏览器画面
- 支持远程控制和调试
- 可以手动干预解决验证码、多因素认证等问题
技术要点:
- 实时视频流传输
- 双向交互(观看 + 控制)
- WebSocket 持久连接
Lime 实现优先级: 🔥 高优先级
2. Debugger (调试器) ⭐⭐⭐
功能描述:
- 通过 Chrome DevTools 检查浏览器自动化
- 获取 browser_id 后可以获取所有标签页的 debug_url
- 支持 WebSocket 连接到远程 DevTools
API 流程:
1. 创建任务 → 获取 browser_id
2. GET /api/v1/browser/browser_id/{browser_id}/tabs → 获取 debug_url
3. 在 Chrome 中打开 debug_url?is-playground=true
技术要点:
- CDP (Chrome DevTools Protocol) 直连
- WebSocket debugger URL:
wss://... - 支持多标签页调试
Lime 实现优先级: 🔥 高优先级
3. Browser & Proxy Settings (浏览器与代理设置) ⭐⭐
功能描述:
浏览器会话类型:
persistent: 持久会话,保存 cookies 和登录状态one-time: 一次性会话,每次从头开始
代理设置:
- 代理类型: Residential(住宅) / Mobile(移动)
- 地理定位: 国家/地区/城市/ISP
- 用于绕过地理限制和反爬虫检测
Lime 实现优先级: 🔥 高优先级
4. Location Customization (位置定制) ⭐⭐
功能描述:
- 配置代理、地理位置、设备配置文件
- 模拟真实用户行为避免检测
- 支持 190+ 国家和主要城市
- 设备配置: Windows / macOS / Android / iOS
技术要点:
- 代理 IP 轮换
- 浏览器指纹伪装
- Geolocation API 覆盖
- User-Agent 和设备特征模拟
Lime 实现优先级: 🟡 中优先级
5. Profiles (保存的登录) ⭐⭐⭐
功能描述:
- 保存并重用已认证的浏览器会话
- 自动化任务启动时已登录
- 支持从 Multilogin 导入配置文件
工作流程:
1. 创建 Profile → 在 Profile Mode 中登录 → 保存
2. 使用 Profile → 选择已保存的 Profile → 浏览器自动加载 cookies
3. 管理 Profile → 重命名/删除/刷新
安全性:
- Cookies 加密存储
- 用户级别隔离
- 支持会话刷新
Lime 实现优先级: 🔥 高优先级
6. Credentials (凭证管理) ⭐
功能描述:
- 安全存储凭证
- 会话过期时自动重新认证
- 支持双因素认证
Lime 实现优先级: 🟡 中优先级
7. AutoCAPTCHA Solver (自动验证码解决) ⭐⭐
功能描述:
- 默认启用,无需配置
- 支持多种验证码类型:
- reCAPTCHA v2/v3
- TextCAPTCHA
- AWS WAF CAPTCHA
- Cloudflare CAPTCHA
- FunCAPTCHA
- 等等
Lime 实现优先级: 🟢 低优先级 (可集成第三方服务)
8. Scheduled Task (定时任务) ⭐
功能描述:
- 一次性任务: 指定日期和时间运行
- 循环任务: 每小时/每天/每周/自定义 cron 表达式
Lime 实现优先级: 🟡 中优先级
9. Connections (连接器) ⭐
功能描述:
- 集成外部服务: Google Sheets / Google Drive / Gmail
- OAuth 认证
- 自动续期连接
Lime 实现优先级: 🟢 低优先级
10. Input & Output (输入输出) ⭐⭐
功能描述:
- 支持多种输出格式:
- 纯文本
- 列表
- 表格
- CSV
- JSON
- 超链接
Lime 实现优先级: 🟡 中优先级
二、关键实现变更
2.1 后端会话层 - CdpSessionManager
在 src-tauri/src/commands/webview_cmd.rs 对应能力后新增 CdpSessionManager,按 profile_key + target_id 管理长连接会话。
会话建立流程
1. 发现可用 target
2. 选择 page target
3. 连接 webSocketDebuggerUrl
4. Target.setAutoAttach(flatten=true)
5. 启用 Page/Runtime/Network/Log/Console 域
会话状态维护
// src-tauri/src/cdp/session_manager.rs
pub struct CdpSession {
pub session_id: String,
pub target_id: String,
pub profile_key: String,
pub remote_debugging_port: u16,
// 最近页面快照摘要
pub page_snapshot: PageSnapshot,
// 最近事件 ring buffer(用于调试与回放)
pub event_buffer: RingBuffer<BrowserEvent>,
// 命令请求-响应映射
pub pending_commands: HashMap<u64, CommandRequest>,
// WebSocket 连接
ws: Arc<Mutex<WebSocket>>,
}
pub struct PageSnapshot {
pub title: String,
pub url: String,
pub html_summary: String, // 或 Markdown 摘要
pub last_updated: SystemTime,
}
2.2 统一事件模型
新增统一浏览器事件类型,前端与调试页只订阅统一事件流,不直接理解原始 CDP 包。
核心事件类型
// src-tauri/src/cdp/events.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum BrowserEvent {
// 会话生命周期
SessionOpened { session_id: String, target_id: String },
SessionClosed { session_id: String, reason: String },
SessionError { session_id: String, error: String },
// 页面状态
PageInfoChanged {
session_id: String,
title: String,
url: String,
},
// Console 输出
ConsoleMessage {
session_id: String,
level: String, // log/warn/error
text: String,
timestamp: u64,
},
// Network 事件
NetworkRequest {
session_id: String,
request_id: String,
url: String,
method: String,
},
NetworkResponse {
session_id: String,
request_id: String,
status: u16,
mime_type: String,
},
NetworkFailed {
session_id: String,
request_id: String,
error_text: String,
},
// DOM 事件(仅摘要)
DomEvent {
session_id: String,
event_type: String,
summary: String,
},
// 画面流
FrameChunk {
session_id: String,
data: String, // base64 JPEG
metadata: FrameMetadata,
},
FrameDropped {
session_id: String,
reason: String,
},
// 命令执行
CommandStarted {
session_id: String,
command_id: u64,
action: String,
},
CommandCompleted {
session_id: String,
command_id: u64,
result: serde_json::Value,
},
CommandFailed {
session_id: String,
command_id: u64,
error: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrameMetadata {
pub width: u32,
pub height: u32,
pub timestamp: u64,
pub sequence: u64,
}
2.3 命令执行模型 - 高层 Action API
将当前 browser_execute_action 的 cdp_direct 分支升级为真正的 WebSocket CDP 调用。
支持的高层 Action
// src-tauri/src/cdp/actions.rs
pub enum BrowserAction {
Click { selector: String },
Type { selector: String, text: String },
Scroll { direction: ScrollDirection, amount: i32 },
ScrollPage { direction: PageScrollDirection },
RefreshPage,
GoBack,
GoForward,
Navigate { url: String },
GetPageInfo,
ReadConsoleMessages { since: Option<u64> },
ReadNetworkRequests { since: Option<u64> },
}
Action → CDP 命令编排
impl CdpSession {
pub async fn execute_action(&mut self, action: BrowserAction) -> Result<ActionResult> {
match action {
BrowserAction::Navigate { url } => {
// Page.navigate + lifecycle 等待
self.ws.send_command("Page.navigate", json!({ "url": url })).await?;
self.wait_for_lifecycle("load").await?;
Ok(ActionResult::Success)
}
BrowserAction::Click { selector } => {
// Runtime.evaluate 或 DOM + Input.dispatchMouseEvent
let element = self.query_selector(&selector).await?;
self.dispatch_mouse_click(element.x, element.y).await?;
Ok(ActionResult::Success)
}
BrowserAction::GetPageInfo => {
// Runtime.evaluate 抽取标题、URL、可见文本摘要、关键元素
let info = self.evaluate_page_info().await?;
Ok(ActionResult::PageInfo(info))
}
BrowserAction::ReadConsoleMessages { since } => {
// 从长期订阅事件缓冲区读取
let messages = self.event_buffer
.iter()
.filter_map(|e| match e {
BrowserEvent::ConsoleMessage { timestamp, .. }
if since.map_or(true, |s| *timestamp > s) => Some(e.clone()),
_ => None,
})
.collect();
Ok(ActionResult::ConsoleMessages(messages))
}
// ... 其他 action
}
}
}
2.4 画面流通道 - Screencast + Screenshot Fallback
默认策略: Page.startScreencast
// src-tauri/src/cdp/screencast.rs
impl CdpSession {
pub async fn start_screencast(&mut self) -> Result<()> {
self.ws.send_command("Page.startScreencast", json!({
"format": "jpeg",
"quality": 60, // 降低质量减少带宽
"maxWidth": 1280,
"maxHeight": 720,
"everyNthFrame": 1
})).await?;
// 监听 Page.screencastFrame 事件
// 转发为 BrowserEvent::FrameChunk
Ok(())
}
async fn handle_screencast_frame(&mut self, params: serde_json::Value) -> Result<()> {
let data = params["data"].as_str().unwrap();
let session_id = params["sessionId"].as_u64().unwrap();
// 发送到前端
self.emit_event(BrowserEvent::FrameChunk {
session_id: self.session_id.clone(),
data: data.to_string(),
metadata: FrameMetadata {
width: params["metadata"]["deviceWidth"].as_u64().unwrap() as u32,
height: params["metadata"]["deviceHeight"].as_u64().unwrap() as u32,
timestamp: chrono::Utc::now().timestamp_millis() as u64,
sequence: self.frame_sequence,
},
}).await?;
self.frame_sequence += 1;
// 确认接收
self.ws.send_command("Page.screencastFrameAck", json!({
"sessionId": session_id
})).await?;
Ok(())
}
}
Fallback 策略: Page.captureScreenshot 轮询
impl CdpSession {
pub async fn start_screenshot_polling(&mut self, interval_ms: u64) -> Result<()> {
let mut interval = tokio::time::interval(Duration::from_millis(interval_ms));
loop {
interval.tick().await;
match self.capture_screenshot().await {
Ok(data) => {
self.emit_event(BrowserEvent::FrameChunk {
session_id: self.session_id.clone(),
data,
metadata: FrameMetadata {
width: 1280,
height: 720,
timestamp: chrono::Utc::now().timestamp_millis() as u64,
sequence: self.frame_sequence,
},
}).await?;
self.frame_sequence += 1;
}
Err(e) => {
self.emit_event(BrowserEvent::FrameDropped {
session_id: self.session_id.clone(),
reason: e.to_string(),
}).await?;
}
}
}
}
async fn capture_screenshot(&self) -> Result<String> {
let result = self.ws.send_command("Page.captureScreenshot", json!({
"format": "jpeg",
"quality": 60
})).await?;
Ok(result["data"].as_str().unwrap().to_string())
}
}
帧流缓存策略
// 只保留最近 N 帧缓存,避免内存膨胀
const MAX_FRAME_BUFFER_SIZE: usize = 30; // 约 1 秒的缓存(30fps)
impl CdpSession {
fn add_frame_to_buffer(&mut self, frame: FrameChunk) {
if self.frame_buffer.len() >= MAX_FRAME_BUFFER_SIZE {
self.frame_buffer.pop_front();
}
self.frame_buffer.push_back(frame);
}
}
2.5 Tauri/前端接口
在 src/lib/webview-api.ts 扩展公共接口。
新增 API
// src/lib/webview-api.ts
/** 打开 CDP 会话 */
export async function openCdpSession(params: {
profile_key: string;
target_id?: string;
}): Promise<{ session_id: string; target_id: string }> {
return invoke('open_cdp_session', params);
}
/** 关闭 CDP 会话 */
export async function closeCdpSession(params: {
session_id: string;
}): Promise<void> {
return invoke('close_cdp_session', params);
}
/** 启动浏览器流 */
export async function startBrowserStream(params: {
session_id: string;
mode: 'events' | 'frames' | 'both';
}): Promise<void> {
return invoke('start_browser_stream', params);
}
/** 停止浏览器流 */
export async function stopBrowserStream(params: {
session_id: string;
mode?: 'events' | 'frames' | 'both';
}): Promise<void> {
return invoke('stop_browser_stream', params);
}
/** 执行浏览器 Action(复用现有 API,补充 session_id/target_id) */
export async function browserExecuteAction(params: {
session_id?: string;
action: BrowserAction;
// ... 现有参数
}): Promise<ActionResult & { session_id: string; target_id: string }> {
return invoke('browser_execute_action', params);
}
/** 获取浏览器会话状态 */
export async function getBrowserSessionState(params: {
session_id: string;
}): Promise<SessionState> {
return invoke('get_browser_session_state', params);
}
/** 获取浏览器事件缓冲区 */
export async function getBrowserEventBuffer(params: {
session_id: string;
cursor?: number;
}): Promise<{ events: BrowserEvent[]; next_cursor: number }> {
return invoke('get_browser_event_buffer', params);
}
事件订阅
// 订阅统一浏览器事件
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<BrowserEvent>('browser-event', (event) => {
switch (event.payload.type) {
case 'SessionOpened':
console.log('会话已打开:', event.payload.session_id);
break;
case 'FrameChunk':
updateCanvas(event.payload.data);
break;
case 'ConsoleMessage':
addConsoleLog(event.payload);
break;
// ... 处理其他事件
}
});
2.6 Profiles - 会话持久化
数据库设计
-- src-tauri/migrations/add_profiles.sql
CREATE TABLE profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
domain TEXT NOT NULL,
cookies_encrypted BLOB NOT NULL,
local_storage TEXT,
session_storage TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_used_at INTEGER
);
CREATE INDEX idx_profiles_domain ON profiles(domain);
Profile Service
// src-tauri/src/services/profile_service.rs
pub struct ProfileService {
db: Arc<Database>,
encryption_key: Vec<u8>,
}
impl ProfileService {
pub async fn save_profile(
&self,
name: String,
domain: String,
cookies: Vec<Cookie>,
) -> Result<String> {
let profile_id = Uuid::new_v4().to_string();
// 加密 cookies (AES-256-GCM)
let cookies_json = serde_json::to_vec(&cookies)?;
let encrypted = self.encrypt(&cookies_json)?;
sqlx::query!(
"INSERT INTO profiles (id, name, domain, cookies_encrypted, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)",
profile_id,
name,
domain,
encrypted,
chrono::Utc::now().timestamp(),
chrono::Utc::now().timestamp()
)
.execute(&self.db.pool)
.await?;
Ok(profile_id)
}
pub async fn load_profile(&self, profile_id: &str) -> Result<Vec<Cookie>> {
let row = sqlx::query!(
"SELECT cookies_encrypted FROM profiles WHERE id = ?",
profile_id
)
.fetch_one(&self.db.pool)
.await?;
let decrypted = self.decrypt(&row.cookies_encrypted)?;
let cookies: Vec<Cookie> = serde_json::from_slice(&decrypted)?;
Ok(cookies)
}
fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
// AES-256-GCM 加密实现
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::Aead;
let cipher = Aes256Gcm::new_from_slice(&self.encryption_key)?;
let nonce = Nonce::from_slice(b"unique nonce"); // 实际应使用随机 nonce
let ciphertext = cipher.encrypt(nonce, data)?;
Ok(ciphertext)
}
fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
// AES-256-GCM 解密实现
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::Aead;
let cipher = Aes256Gcm::new_from_slice(&self.encryption_key)?;
let nonce = Nonce::from_slice(b"unique nonce");
let plaintext = cipher.decrypt(nonce, data)?;
Ok(plaintext)
}
}
2.7 调试与可观测性
在现有 ChromeRelaySettings 旁补一个最小调试面板。
调试面板组件
// src/components/BrowserDebugPanel.tsx
export function BrowserDebugPanel({ sessionId }: { sessionId: string }) {
const [sessionState, setSessionState] = useState<SessionState | null>(null);
const [consoleMessages, setConsoleMessages] = useState<ConsoleMessage[]>([]);
const [networkRequests, setNetworkRequests] = useState<NetworkRequest[]>([]);
useEffect(() => {
// 订阅浏览器事件
const unlisten = listen<BrowserEvent>('browser-event', (event) => {
if (event.payload.session_id !== sessionId) return;
switch (event.payload.type) {
case 'ConsoleMessage':
setConsoleMessages(prev => [...prev, event.payload]);
break;
case 'NetworkRequest':
case 'NetworkResponse':
updateNetworkRequests(event.payload);
break;
}
});
return () => { unlisten.then(fn => fn()); };
}, [sessionId]);
return (
<div className="debug-panel">
<div className="session-info">
<h3>会话状态</h3>
<div>Profile: {sessionState?.profile_key}</div>
<div>Target: {sessionState?.target_id}</div>
<div>最后心跳: {sessionState?.last_heartbeat}</div>
<div>最近命令: {sessionState?.last_command}</div>
</div>
<div className="console-view">
<h3>Console</h3>
{consoleMessages.map((msg, i) => (
<div key={i} className={`console-${msg.level}`}>
[{msg.level}] {msg.text}
</div>
))}
</div>
<div className="network-view">
<h3>Network</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>URL</th>
<th>Status</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{networkRequests.map((req, i) => (
<tr key={i}>
<td>{req.method}</td>
<td>{req.url}</td>
<td>{req.status}</td>
<td>{req.mime_type}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="live-preview">
<h3>实时画面</h3>
<LiveViewCanvas sessionId={sessionId} />
</div>
</div>
);
}
错误与日志集成
所有原始 CDP 错误、命令超时、target detach 都进入现有 audit/log 体系:
// src-tauri/src/cdp/session_manager.rs
impl CdpSession {
async fn handle_error(&self, error: CdpError) {
// 记录到现有日志系统
tracing::error!(
session_id = %self.session_id,
target_id = %self.target_id,
error = ?error,
"CDP session error"
);
// 发送错误事件
self.emit_event(BrowserEvent::SessionError {
session_id: self.session_id.clone(),
error: error.to_string(),
}).await.ok();
}
}
三、实现优先级路线图
Phase 1: 核心 CDP 会话 (2-3 周) 🔥
目标: 建立稳定的 CDP WebSocket 会话,实现基础事件流和命令执行
任务清单
-
CdpSessionManager - 会话生命周期管理
- 发现 target 并建立 WebSocket 连接
- Target.setAutoAttach + 启用 Page/Runtime/Network/Log/Console 域
- 会话状态维护(page_snapshot, event_buffer, pending_commands)
-
统一事件模型 - BrowserEvent 枚举定义
- 会话生命周期事件(SessionOpened/Closed/Error)
- 页面状态事件(PageInfoChanged)
- Console/Network 事件
- 命令执行事件(CommandStarted/Completed/Failed)
-
高层 Action API - browser_execute_action 升级
- Navigate + lifecycle 等待
- Click/Type (Runtime.evaluate 或 DOM + Input)
- GetPageInfo (Runtime.evaluate 抽取摘要)
- ReadConsoleMessages/ReadNetworkRequests (从 event_buffer 读取)
-
Tauri 命令接口
open_cdp_sessionclose_cdp_sessionbrowser_execute_action(补充 session_id/target_id)get_browser_session_stateget_browser_event_buffer
-
事件推送 - Tauri event 通道
- 后端通过
app.emit("browser-event", event)推送 - 前端通过
listen<BrowserEvent>('browser-event')订阅
- 后端通过
验收标准
- ✅ 启动一个 profile,会话建立后能实时看到 PageInfoChanged 事件
- ✅ 执行 navigate/click/type 时,前端能收到 CommandStarted/Completed 事件
- ✅ Console 输出和 Network 请求能实时推送到前端
- ✅ 断开浏览器时,会话能正确关闭并发送 SessionClosed 事件
Phase 2: 画面流通道 (1-2 周) 🔥
目标: 实现实时画面流式传输,支持 screencast + screenshot fallback
任务清单
-
Screencast 实现
Page.startScreencast+Page.screencastFrameAck- 监听
Page.screencastFrame事件 - 转发为
BrowserEvent::FrameChunk - 帧流缓存(最近 30 帧)
-
Screenshot Fallback
Page.captureScreenshot轮询(默认 200ms 间隔)- 自动降级策略(screencast 失败时切换)
BrowserEvent::FrameDropped错误上报
-
前端 LiveView 组件
- Canvas 渲染 base64 JPEG 帧
- 远程控制(鼠标/键盘事件转发)
- 工具栏(启用/禁用控制、截图、停止会话)
-
Tauri 命令接口
start_browser_stream(mode: events/frames/both)stop_browser_stream
验收标准
- ✅ 启动 screencast 后,前端能实时看到浏览器画面(30fps)
- ✅ 点击 Canvas 能触发远程浏览器的点击事件
- ✅ Screencast 失败时自动降级到 screenshot 轮询
- ✅ 帧流缓存不超过 30 帧,避免内存膨胀
Phase 3: Profiles 持久化 (1-2 周) 🔥
目标: 实现浏览器会话持久化,支持 Cookies 加密存储
任务清单
-
数据库设计
profiles表(id, name, domain, cookies_encrypted, created_at, updated_at)- 索引(idx_profiles_domain)
-
ProfileService
save_profile- AES-256-GCM 加密 cookiesload_profile- 解密并返回 cookieslist_profiles- 列出所有 profilesdelete_profile- 删除 profile
-
Tauri 命令接口
create_profile- 创建新 profileload_profile_cookies- 加载 profile 的 cookieslist_profiles- 列出所有 profilesdelete_profile- 删除 profile
-
前端 Profiles 管理页面
- ProfileCard 组件
- CreateProfileModal 组件
- 编辑/删除 profile
验收标准
- ✅ 创建 profile 后,cookies 被加密存储到数据库
- ✅ 加载 profile 时,cookies 被正确解密并应用到浏览器
- ✅ 前端能列出、创建、编辑、删除 profiles
- ✅ 加密密钥安全存储,用户级别隔离
Phase 4: 调试面板 (1 周) 🟡
目标: 提供最小调试面板,方便开发和排障
任务清单
-
BrowserDebugPanel 组件
- 会话状态显示(profile_key, target_id, last_heartbeat)
- Console 视图(实时显示 console 输出)
- Network 视图(请求列表、状态、类型)
- 实时画面预览(复用 LiveViewCanvas)
-
错误与日志集成
- CDP 错误记录到 tracing
- 命令超时上报
- Target detach 处理
验收标准
- ✅ 调试面板能显示当前会话状态
- ✅ Console 输出实时更新
- ✅ Network 请求列表实时更新
- ✅ CDP 错误能在日志中查看
Phase 5: API 服务器扩展 (可选) 🟢
目标: 提供 NextBrowser 兼容的 HTTP API
任务清单
-
任务管理 API
POST /api/v1/chat/tasks- 创建任务GET /api/v1/chat/sessions/{session_id}/state- 查询任务状态GET /api/v1/browser/browser_id/{browser_id}/tabs- 获取浏览器标签页
-
Profile 管理 API
POST /api/v1/profiles- 创建 profileGET /api/v1/profiles- 列出 profilesDELETE /api/v1/profiles/{profile_id}- 删除 profile
验收标准
- ✅ API 端点符合 NextBrowser 规范
- ✅ 支持 JSON 请求/响应
- ✅ 错误处理和状态码正确
四、测试与验收
4.1 Rust 单元测试
// src-tauri/src/cdp/session_manager_test.rs
#[tokio::test]
async fn test_session_lifecycle() {
let manager = CdpSessionManager::new();
// 创建会话
let session_id = manager.open_session("test_profile", None).await.unwrap();
assert!(!session_id.is_empty());
// 获取会话状态
let state = manager.get_session_state(&session_id).await.unwrap();
assert_eq!(state.profile_key, "test_profile");
// 关闭会话
manager.close_session(&session_id).await.unwrap();
// 验证会话已关闭
assert!(manager.get_session_state(&session_id).await.is_err());
}
#[tokio::test]
async fn test_event_mapping() {
// 测试 CDP 事件到统一事件的映射
let cdp_event = json!({
"method": "Console.messageAdded",
"params": {
"message": {
"level": "error",
"text": "Test error",
"timestamp": 1234567890
}
}
});
let browser_event = map_cdp_event(cdp_event).unwrap();
match browser_event {
BrowserEvent::ConsoleMessage { level, text, .. } => {
assert_eq!(level, "error");
assert_eq!(text, "Test error");
}
_ => panic!("Expected ConsoleMessage event"),
}
}
#[tokio::test]
async fn test_command_timeout() {
let session = CdpSession::new("test_session", "test_target", "ws://localhost:9222");
// 发送命令并设置超时
let result = session.execute_action(BrowserAction::Navigate {
url: "https://example.com".to_string()
}).await;
// 验证超时处理
// ...
}
#[tokio::test]
async fn test_screencast_fallback() {
let mut session = CdpSession::new("test_session", "test_target", "ws://localhost:9222");
// 模拟 screencast 失败
session.screencast_enabled = false;
// 启动画面流
session.start_screencast().await.unwrap();
// 验证自动降级到 screenshot
assert!(session.screenshot_fallback_active);
}
4.2 前端测试
// src/lib/webview-api.test.ts
import { describe, it, expect, vi } from 'vitest';
import { openCdpSession, closeCdpSession } from './webview-api';
describe('webview-api', () => {
it('should open CDP session', async () => {
const mockInvoke = vi.fn().mockResolvedValue({
session_id: 'test-session',
target_id: 'test-target'
});
global.invoke = mockInvoke;
const result = await openCdpSession({ profile_key: 'test' });
expect(result.session_id).toBe('test-session');
expect(mockInvoke).toHaveBeenCalledWith('open_cdp_session', {
profile_key: 'test'
});
});
it('should close CDP session', async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
global.invoke = mockInvoke;
await closeCdpSession({ session_id: 'test-session' });
expect(mockInvoke).toHaveBeenCalledWith('close_cdp_session', {
session_id: 'test-session'
});
});
});
// src/components/BrowserDebugPanel.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { BrowserDebugPanel } from './BrowserDebugPanel';
describe('BrowserDebugPanel', () => {
it('should render session info', () => {
render(<BrowserDebugPanel sessionId="test-session" />);
expect(screen.getByText(/会话状态/i)).toBeInTheDocument();
});
it('should handle no stream state', () => {
render(<BrowserDebugPanel sessionId="test-session" />);
// 验证无流状态下的 UI
expect(screen.queryByText(/实时画面/i)).toBeInTheDocument();
});
it('should handle disconnection', async () => {
const { rerender } = render(<BrowserDebugPanel sessionId="test-session" />);
// 模拟断线
// ...
rerender(<BrowserDebugPanel sessionId="test-session" />);
// 验证断线状态
// ...
});
});
4.3 集成验证
场景 1: 基础会话建立
# 1. 启动 Lime
npm run tauri:dev
# 2. 打开调试面板
# 3. 创建新会话
# 4. 验证:
# - 能看到 SessionOpened 事件
# - 能看到 PageInfoChanged 事件(标题/URL)
# - Console 视图显示初始日志
# - Network 视图显示初始请求
场景 2: 实时画面流
# 1. 在已建立的会话中启动画面流
# 2. 验证:
# - 画面连续刷新(30fps)
# - 点击 Canvas 能触发远程点击
# - 输入键盘能触发远程输入
# - 帧流缓存不超过 30 帧
场景 3: 命令执行
# 1. 执行 navigate 命令
# 2. 验证:
# - 收到 CommandStarted 事件
# - 页面导航成功
# - 收到 CommandCompleted 事件
# - 出错时收到 CommandFailed 事件(包含 CDP 错误)
场景 4: 断线重连
# 1. 关闭浏览器进程
# 2. 验证:
# - 收到 SessionClosed 事件
# - 前端状态正确更新
# - 不残留脏 session
场景 5: Profile 持久化
# 1. 创建 Profile 并登录网站
# 2. 保存 Profile
# 3. 关闭会话
# 4. 重新加载 Profile
# 5. 验证:
# - Cookies 被正确加载
# - 网站保持登录状态
# - 加密存储安全
4.4 验收标准总结
核心功能
- ✅ cdp_direct 不再报"需要建立 WebSocket DevTools 会话后补齐"
- ✅ 用户在任务执行期间可同时看到:
- 实时画面(30fps)
- Console/Network 事件
- 命令结果
- ✅ 默认链路在 macOS 与 Windows 都可运行
性能指标
- ✅ 画面流延迟 < 200ms
- ✅ 事件推送延迟 < 100ms
- ✅ 命令执行响应 < 500ms
- ✅ 内存占用 < 500MB (单会话)
稳定性
- ✅ 会话断线自动重连(最多 3 次)
- ✅ Screencast 失败自动降级到 screenshot
- ✅ 命令超时自动取消(默认 30s)
- ✅ 帧流缓存不超过 30 帧
五、默认决策与假设
架构决策
- ✅ 默认以当前仓库落地为目标,不设计独立外部服务
- ✅ 默认先实现单 page target 单活会话,多 tab 并发观察放到第二阶段,但数据模型提前兼容
- ✅ 默认优先走 Tauri event 推送事件与帧,只有验证吞吐不足时再切专用本地 WebSocket
- ✅ 默认画面流使用 JPEG screencast,并提供 screenshot fallback,暂不引入视频编码、录屏文件落盘、音频流
- ✅ 默认只暴露高层 action API 给业务层,原始 CDP 命令透传仅保留给调试入口,避免公共接口过早失控
- ✅ 默认不改动现有后端优先级策略,只增强 cdp_direct 能力,保持 KISS / YAGNI,避免再造第四套浏览器控制通道
技术选型
- ✅ WebSocket: 使用
tokio-tungstenite实现 CDP WebSocket 连接 - ✅ 事件推送: 优先使用 Tauri event,吞吐不足时切换到本地 WebSocket
- ✅ 加密: 使用
aes-gcmcrate 实现 AES-256-GCM 加密 - ✅ 数据库: 使用 SQLite + sqlx
- ✅ 日志: 使用
tracingcrate
平台兼容性
- ✅ macOS: 默认支持,Chrome 路径
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome - ✅ Windows: 默认支持,Chrome 路径
C:\Program Files\Google\Chrome\Application\chrome.exe - ✅ Linux: 暂不支持,但数据模型兼容
六、技术难点与解决方案
6.1 实时视频流性能优化
问题: Page.startScreencast 产生大量 JPEG 帧,可能导致带宽和 CPU 压力
解决方案:
-
动态调整帧率和质量
// 根据网络状况动态调整 if network_latency > 200ms { quality = 40; // 降低质量 everyNthFrame = 2; // 降低帧率 } -
客户端缓存和差分编码
// 只渲染变化的区域 const diff = computeFrameDiff(prevFrame, currentFrame); if (diff.percentage < 5%) { skipFrame(); } -
WebRTC 替代方案(第二阶段)
- 使用 WebRTC Data Channel 传输帧
- 更低延迟(< 100ms)
- 自动拥塞控制
6.2 多浏览器实例管理
问题: 同时运行多个浏览器实例,需要隔离和资源管理
解决方案:
-
独立用户数据目录
let user_data_dir = format!("/tmp/lime/profile_{}", profile_key); let chrome_args = vec![ format!("--user-data-dir={}", user_data_dir), "--no-first-run", "--no-default-browser-check", ]; -
端口池管理
pub struct PortPool { available_ports: Vec<u16>, used_ports: HashMap<String, u16>, } impl PortPool { pub fn allocate(&mut self, session_id: &str) -> Option<u16> { let port = self.available_ports.pop()?; self.used_ports.insert(session_id.to_string(), port); Some(port) } pub fn release(&mut self, session_id: &str) { if let Some(port) = self.used_ports.remove(session_id) { self.available_ports.push(port); } } } -
资源限制
// 使用 tokio 限制并发会话数 const MAX_CONCURRENT_SESSIONS: usize = 10; let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SESSIONS)); async fn create_session() -> Result<Session> { let _permit = semaphore.acquire().await?; // 创建会话... }
6.3 Cookies 加密安全
问题: Cookies 包含敏感信息,需要安全存储
解决方案:
-
AES-256-GCM 加密
use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; use aes_gcm::aead::{Aead, OsRng}; pub fn encrypt_cookies(cookies: &[Cookie], key: &[u8]) -> Result<Vec<u8>> { let cipher = Aes256Gcm::new_from_slice(key)?; // 生成随机 nonce let mut nonce_bytes = [0u8; 12]; OsRng.fill_bytes(&mut nonce_bytes); let nonce = Nonce::from_slice(&nonce_bytes); let plaintext = serde_json::to_vec(cookies)?; let ciphertext = cipher.encrypt(nonce, plaintext.as_ref())?; // 拼接 nonce + ciphertext let mut result = nonce_bytes.to_vec(); result.extend_from_slice(&ciphertext); Ok(result) } pub fn decrypt_cookies(data: &[u8], key: &[u8]) -> Result<Vec<Cookie>> { let cipher = Aes256Gcm::new_from_slice(key)?; // 分离 nonce 和 ciphertext let (nonce_bytes, ciphertext) = data.split_at(12); let nonce = Nonce::from_slice(nonce_bytes); let plaintext = cipher.decrypt(nonce, ciphertext)?; let cookies: Vec<Cookie> = serde_json::from_slice(&plaintext)?; Ok(cookies) } -
密钥派生(PBKDF2)
use pbkdf2::{pbkdf2_hmac}; use sha2::Sha256; pub fn derive_encryption_key(password: &str, salt: &[u8]) -> [u8; 32] { let mut key = [0u8; 32]; pbkdf2_hmac::<Sha256>( password.as_bytes(), salt, 100_000, // 迭代次数 &mut key ); key } -
用户级别隔离
// 每个用户使用独立的加密密钥 let user_salt = format!("lime_user_{}", user_id); let encryption_key = derive_encryption_key(&user_password, user_salt.as_bytes());
6.4 WebSocket 连接稳定性
问题: CDP WebSocket 连接可能断开,需要自动重连
解决方案:
-
心跳检测
impl CdpSession { async fn start_heartbeat(&self) { let mut interval = tokio::time::interval(Duration::from_secs(30)); loop { interval.tick().await; if let Err(e) = self.ws.send_ping().await { tracing::warn!("Heartbeat failed: {}", e); self.reconnect().await.ok(); } } } } -
自动重连
impl CdpSession { async fn reconnect(&mut self) -> Result<()> { const MAX_RETRIES: usize = 3; const RETRY_DELAY: Duration = Duration::from_secs(2); for attempt in 1..=MAX_RETRIES { tracing::info!("Reconnecting attempt {}/{}", attempt, MAX_RETRIES); match self.connect_websocket().await { Ok(ws) => { self.ws = ws; self.resubscribe_events().await?; return Ok(()); } Err(e) => { tracing::warn!("Reconnect failed: {}", e); tokio::time::sleep(RETRY_DELAY).await; } } } Err(anyhow!("Failed to reconnect after {} attempts", MAX_RETRIES)) } } -
事件重放
// 重连后重新订阅事件 async fn resubscribe_events(&self) -> Result<()> { self.ws.send_command("Page.enable", json!({})).await?; self.ws.send_command("Runtime.enable", json!({})).await?; self.ws.send_command("Network.enable", json!({})).await?; self.ws.send_command("Log.enable", json!({})).await?; self.ws.send_command("Console.enable", json!({})).await?; Ok(()) }
6.5 事件推送吞吐量
问题: Tauri event 可能无法满足高频事件推送(如 30fps 画面流)
解决方案:
-
批量推送
// 批量推送事件,减少 IPC 开销 const BATCH_SIZE: usize = 10; const BATCH_INTERVAL: Duration = Duration::from_millis(100); let mut event_batch = Vec::new(); let mut interval = tokio::time::interval(BATCH_INTERVAL); loop { tokio::select! { event = event_rx.recv() => { event_batch.push(event); if event_batch.len() >= BATCH_SIZE { app.emit("browser-events", &event_batch)?; event_batch.clear(); } } _ = interval.tick() => { if !event_batch.is_empty() { app.emit("browser-events", &event_batch)?; event_batch.clear(); } } } } -
本地 WebSocket Fallback
// 当 Tauri event 吞吐不足时,切换到本地 WebSocket if event_rate > 100 { // 每秒超过 100 个事件 switch_to_local_websocket().await?; } async fn switch_to_local_websocket() -> Result<()> { let listener = TcpListener::bind("127.0.0.1:0").await?; let port = listener.local_addr()?.port(); // 通知前端切换到 WebSocket app.emit("switch-to-websocket", json!({ "port": port }))?; // 接受 WebSocket 连接 let (stream, _) = listener.accept().await?; let ws = tokio_tungstenite::accept_async(stream).await?; // 通过 WebSocket 推送事件 // ... Ok(()) }
七、与现有 Lime 架构的集成
7.1 复用现有模块
现有基础设施:
- ✅
webview_cmd.rs- 已有 CDP HTTP 探测,升级为 WebSocket 会话 - ✅
server.rs- 扩展 API 端点(任务管理、Profile 管理) - ✅
database.rs- 添加 Profiles 表 - ✅ DevBridge - 前端与 Tauri 通信,复用事件推送机制
集成策略:
- 不改动现有优先级: aster_compat、lime_extension_bridge、cdp_direct 继续并存
- 只增强 cdp_direct: 从 HTTP 探测升级为有状态 WebSocket 会话
- 保持接口兼容:
browser_execute_action保持现有签名,补充session_id/target_id返回值
7.2 新增模块
src-tauri/src/
├── cdp/
│ ├── mod.rs # CDP 模块入口
│ ├── session_manager.rs # CdpSessionManager - 会话生命周期管理
│ ├── events.rs # BrowserEvent - 统一事件模型
│ ├── actions.rs # BrowserAction - 高层 Action API
│ ├── screencast.rs # Screencast + Screenshot Fallback
│ ├── input.rs # 远程控制(鼠标/键盘)
│ └── websocket.rs # CDP WebSocket 封装
├── services/
│ ├── profile_service.rs # Profile 加密存储与加载
│ ├── port_pool.rs # CDP 端口池管理
│ └── browser_pool.rs # 浏览器实例池(可选,第二阶段)
├── models/
│ ├── profile.rs # Profile 数据模型
│ ├── session.rs # Session 数据模型
│ └── browser_settings.rs # BrowserSettings 数据模型
└── commands/
└── cdp_commands.rs # 新增 Tauri 命令(open_cdp_session 等)
7.3 数据库迁移
-- src-tauri/migrations/YYYYMMDD_add_profiles.sql
CREATE TABLE IF NOT EXISTS profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
domain TEXT NOT NULL,
cookies_encrypted BLOB NOT NULL,
local_storage TEXT,
session_storage TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_used_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_profiles_domain ON profiles(domain);
CREATE INDEX IF NOT EXISTS idx_profiles_updated_at ON profiles(updated_at);
7.4 前端集成
新增页面:
src/pages/
├── LiveView.tsx # 实时画面 + 远程控制
├── Profiles.tsx # Profile 管理
└── BrowserDebug.tsx # 调试面板
新增组件:
src/components/
├── LiveViewCanvas.tsx # Canvas 渲染画面流
├── ConsoleOutput.tsx # Console 输出视图
├── NetworkTable.tsx # Network 请求表格
├── ProfileCard.tsx # Profile 卡片
└── CreateProfileModal.tsx # 创建 Profile 弹窗
新增 Hooks:
src/hooks/
├── useCdpSession.ts # CDP 会话管理
├── useBrowserEvents.ts # 浏览器事件订阅
└── useProfiles.ts # Profile CRUD
八、总结
核心价值
NextBrowser 的核心价值在于:
- 实时可视化 - Live View 让用户看到自动化过程
- 会话持久化 - Profiles 避免重复登录
- 远程调试 - CDP Debugger 提供专业调试能力
实施策略
Lime 已经具备了 CDP 基础设施,采用渐进式增强策略:
- Phase 1 (2-3 周): 核心 CDP 会话 - 建立稳定的 WebSocket 连接和统一事件模型
- Phase 2 (1-2 周): 画面流通道 - 实现 screencast + screenshot fallback
- Phase 3 (1-2 周): Profiles 持久化 - 实现 Cookies 加密存储
- Phase 4 (1 周): 调试面板 - 提供最小调试界面
- Phase 5 (可选): API 服务器扩展 - NextBrowser 兼容 API
预期成果
- ✅ 6-8 周完成核心功能实现
- ✅ 跨平台兼容(macOS + Windows)
- ✅ 保持现有架构,不引入第四套浏览器控制通道
- ✅ 统一抽象层,前端只消费统一事件,不直接理解 CDP
- ✅ 生产可用,满足实时流控、远程调试、会话持久化需求
关键决策
- ✅ 采用 Codex 的架构思路(CdpSessionManager + 统一事件模型)
- ✅ 采用 NextBrowser 的功能清单(Live View + Profiles + Debugger)
- ✅ 优先 Tauri event,吞吐不足时切 WebSocket
- ✅ Screencast 优先,Screenshot fallback
- ✅ 单 page target 先行,多 tab 后续
- ✅ 高层 action API,原始 CDP 仅调试用
九、基于当前代码库的差距复盘(截至 2026-03-15)
本节用于校正文档前文的“规划态”描述,按当前仓库真实实现判断 Lime 已做到什么、还缺什么,以及后续应如何按基础设施优先推进。
9.1 当前已经具备的能力底座
A. 实时画面与 CDP 会话底座:已具备,可继续加固
- 已有
BrowserRuntimeManager、CdpSessionState、事件缓冲区、人工接管状态机:src-tauri/crates/browser-runtime/src/manager.rssrc-tauri/crates/browser-runtime/src/types.rs
- 已支持
Page.startScreencast,失败时自动回退到Page.captureScreenshot轮询:src-tauri/crates/browser-runtime/src/manager.rs
- 已暴露 Tauri 命令与前端调试页:
src-tauri/src/commands/browser_runtime_cmd.rssrc-tauri/src/commands/webview_cmd.rssrc/features/browser-runtime/BrowserRuntimeWorkspace.tsxsrc/features/browser-runtime/BrowserRuntimeDebugPanel.tsx
B. 浏览器 Profile 隔离:已具备基础,但还不是产品级“个人资料”
- 已支持按
profile_key启动独立 Chrome 用户目录,天然保留 cookies / localStorage / 登录态:src-tauri/src/commands/webview_cmd.rs
- 已支持列出和关闭运行中的 Profile 会话:
get_chrome_profile_sessionsclose_chrome_profile_session
- 现状问题:
- 只有“运行中的 Chrome profile 目录”概念,没有“可管理的 Profile 实体”概念
- 没有名称、标签、站点、最后使用时间、描述、导入/导出、锁定策略、加密策略
- 没有“保存当前登录为资料”的明确工作流
C. 调度引擎:已具备通用能力,但不是浏览器任务编排
- 已有调度器、轮询执行器、Cron/At/Every 调度计算与健康治理:
src-tauri/crates/scheduler/src/*src-tauri/src/app/scheduler_service.rssrc-tauri/src/services/heartbeat_service/*src-tauri/src/commands/heartbeat_cmd.rs
- 现状问题:
- 当前主要服务于 Heartbeat/通用任务,不是浏览器自动化任务模板
- 缺少“任务绑定哪个 browser profile / 环境预设 / 输出 schema / 人工检查点”的模型
D. 浏览器动作与输出:已具备最小可用能力
- 已支持
navigate / click / type / scroll / read_page / read_console_messages / read_network_requests - 已有统一的
browser_execute_action多后端编排:src-tauri/src/commands/webview_cmd.rssrc-tauri/crates/browser-runtime/src/action.rs
- 现状问题:
- 输出仍偏底层:
markdown / page_info / console / network event - 没有任务级结构化输出合同,例如
json schema / table / csv / fields mapping
- 输出仍偏底层:
9.2 与截图功能的差距矩阵
| 功能 | 当前状态 | 结论 |
|---|---|---|
| 实时画面 | 已有 CDP 帧流、回退截图、调试页、人工接管 | 已做基础版,需稳定化和产品化 |
| 个人资料(已保存的登录信息) | 已有独立 Chrome profile 目录和会话复用 | 部分完成,缺产品级资料管理 |
| 计划任务 | 已有通用调度器、Heartbeat、Cron 校验 | 部分完成,缺浏览器任务模型与 UI |
| 输入和输出 | 已有页面信息、控制台、网络事件、动作执行结果 | 部分完成,缺结构化 I/O 层 |
| 位置定制 | 未见浏览器级代理、地理位置、时区、语言、UA、指纹预设 | 未实现 |
| 证书 | 未见浏览器级客户端证书/站点证书选择与存储模型 | 未实现 |
| 自动验证码求解器 | 仅支持人工接管,没有 solver 抽象与供应商接入 | 未实现 |
| 连接 | 现有 connection_cmd 是终端/SSH/WSL 连接,不是外部业务连接器 |
未实现截图语义下的连接器 |
9.3 架构判断:先不要直接堆功能页
如果现在直接开始补“位置定制 / 计划任务 / 连接 / 输入和输出”这些页面,仓库会出现新的平行概念:
- 一套运行中 session 概念
- 一套 Chrome profile 目录概念
- 一套 Heartbeat 任务概念
- 一套未来的浏览器任务概念
这会导致三类问题:
- 状态源分裂:Profile、Session、Task、Connector 各自一套 id 和生命周期
- 配置不可复用:位置定制、登录资料、任务调度之间无法组合
- 上层功能失去稳定锚点:定时任务、验证码、人机接管都需要先有稳定的会话装配模型
因此正确顺序不是“按截图逐个做页面”,而是先补一层浏览器控制面(control plane)。
9.4 建议新增的统一域模型
1. Browser Profile
表示一个“可复用的登录资料容器”,而不是当前仅存在的目录。
建议字段:
idkeynamedescriptionsite_scopestorage_mode:persistent | ephemeralprofile_dirlast_used_atcreated_atupdated_atarchived_at
2. Browser Environment Preset
承载“位置定制”能力,后续任务和 Profile 都引用它。
建议字段:
idnameproxy_typeproxy_serverproxy_auth_refcountryregioncitytimezone_idlocaleaccept_languagegeolocation_latgeolocation_lnggeolocation_accuracy_muser_agentviewport_widthviewport_heightdevice_scale_factorplatform
3. Browser Task Template
承载“计划任务”的可执行定义,复用现有 scheduler,而不是另起炉灶。
建议字段:
idnameentry_urlprofile_idenvironment_preset_idschedule_kindschedule_payloadstepsrequires_human_checkpointoutput_schemaoutput_destinationenabled
4. Browser Connector
承载“连接”能力,目标是把结果投递到外部系统,而不是终端连接。
建议字段:
idtype:google_sheets | gmail | webhook | drive | notion | ...nameauth_kindsecret_refconfig_jsonstatuslast_checked_at
5. Browser Certificate Asset
承载浏览器证书与站点绑定。
建议字段:
idnamecert_kind:client_tls | custom_cafile_refpassphrase_refhost_patternscreated_at
9.5 推荐实施优先级
P0. 收口现有浏览器控制面(最高优先级)
目标:把“运行时会话”变成后续一切能力的稳定底座。
本阶段做什么:
- 把当前
profile_key升级为数据库中的Browser Profile实体 - 给运行时 session 增加
profile_id / environment_preset_id / task_id关联位 - 把
open_chrome_profile_window的启动参数抽象成LaunchBrowserSessionRequest - 保持现有
cdp_direct / extension_bridge / aster_compat编排不变,只收口输入模型 - 给浏览器会话增加稳定审计日志:谁启动、带什么环境、来自哪个任务
本阶段不做什么:
- 不先做 CAPTCHA
- 不先做连接器 UI
- 不先做证书上传页
原因:
- 没有统一控制面,上层功能都会变成一次性参数拼装,后续很难维护
P1. 个人资料产品化(高优先级)
目标:让“保存的登录”从目录能力升级为可管理资产。
本阶段做什么:
- 新增 Profile 列表、创建、重命名、归档、删除、最近使用
- 支持“从当前运行会话保存为资料”
- 支持“打开资料并进入人工登录”
- 支持资料与站点作用域绑定
- 支持资料锁定策略和敏感信息隔离说明
验收标准:
- 用户可以明确看到哪些登录资料存在
- 用户可以复用而不是记
profile_key - Agent 可以按
profile_id复用资料
P2. 位置定制(高优先级)
目标:让 Profile 可以在不同地区/设备语境中稳定复用。
本阶段做什么:
- 浏览器启动参数支持
--proxy-server - CDP 注入
Emulation.setGeolocationOverride - CDP 注入
Emulation.setTimezoneOverride - CDP 注入
Emulation.setUserAgentOverride - 前端提供 Environment Preset 编辑页
- Profile 与 Preset 解耦,可自由组合
关键原则:
- 位置定制必须是独立 Preset,不能直接塞进 Profile
- 否则同一个登录资料无法复用到多个国家/城市场景
9.6 当前已落地的基础层(截至 2026-03-15)
已完成
P1 Browser Profile已完成第一版资产化:- 已有
browser_profiles表、DAO、Service、Tauri 命令、前端资料管理 UI - 运行时会话仍以
profile_key驱动,但新需求已经收口到Browser Profile实体
- 已有
P2 Browser Environment Preset已完成第一版基础落地:- 已有
browser_environment_presets表、DAO、Service、Tauri 命令、前端预设管理 UI - 浏览器工作台支持“资料 + 环境预设”组合启动
- 启动链已支持:
- Chrome 启动参数
--proxy-server - CDP 注入
Emulation.setGeolocationOverride - CDP 注入
Emulation.setTimezoneOverride - CDP 注入
Emulation.setUserAgentOverride - CDP 注入
Emulation.setLocaleOverride - CDP 注入
Emulation.setDeviceMetricsOverride
- Chrome 启动参数
- 运行时
session已增加environment_preset_id / environment_preset_name关联位
- 已有
- 浏览器运行时统一审计已接入基础层:
- 启动链与动作链统一写入同一浏览器运行时审计缓冲区
launch审计已覆盖profile/profile_id、环境预设、session_id/target_id、URL、复用状态、窗口打开方式、流模式、浏览器来源、CDP 端口- 调试面板高级区可以直接查看最近启动与动作审计
- 浏览器启动请求已完成第一轮收口:
- 新增统一
LaunchBrowserSessionRequest profile_id与profile_key启动都收口到同一 session 启动边界BrowserProfileManager、浏览器工作台恢复链、Chrome Relay、Agent Chat 浏览器协助都已切到统一启动请求
- 新增统一
当前限制
- 代理属于浏览器启动参数;若资料对应的 Chrome 进程已在运行,切换代理前必须先关闭该资料会话
- Locale override 依赖目标 Chrome 版本;若方法不存在,当前实现按 best-effort 处理并保留日志告警
- 当前 Environment Preset 只覆盖运行时真正可落地的字段:
proxy_servertimezone_idlocaleaccept_languagegeolocation_*user_agentplatformviewport_*device_scale_factor
- 尚未实现:
- 地区标签字段的产品化筛选与统计
- 证书资产
- CAPTCHA solver
- 任务模板与 connector 组合编排
当前事实源分类
currentbrowser_profilesbrowser_environment_presetslaunch_browser_session + LaunchBrowserSessionRequestautomation_job.payload.browser_session + Automation executorbrowser_profile_cmdbrowser_environment_cmdBrowserRuntimeAuditRecord统一浏览器运行时审计模型BrowserProfileManagerBrowserEnvironmentPresetManagerBrowserRuntimeDebugPanel中的最近启动/动作审计面板
compat- 旧的裸
profile_key/ Chrome 目录启动链仍保留,但只允许委托到新控制面,不再承载新功能 launch_browser_runtime_assistlaunch_browser_profile_runtime_assist_cmdget_browser_action_audit_logs命名暂保留,但返回值已升级为统一运行时审计记录
- 旧的裸
P3. 浏览器计划任务(中高优先级)
目标:复用现有 scheduler/heartbeat 底座,做真正的浏览器自动化任务。
当前进展(第一刀已落地):
- 不新增平行调度系统,先把浏览器任务收口为
automation_jobs.payload.browser_session - 调度执行时直接复用
launch_browser_session - profile / environment preset 在保存任务时就做存在性校验
- 执行历史继续写现有
ExecutionTracker - 自动化详情页已直接嵌入现有
BrowserRuntimeDebugPanel,复用waiting_for_human / human_controlling / live状态机处理人工接管 - 浏览器任务不再在启动成功后立即记为
success;现在会保持agent_runs=running,并通过session_id -> automation_jobs / agent_runs回写waiting_for_human / human_controlling / agent_resuming - 人工点击“恢复给 Agent”后,会在原链路内把自动化任务收口为成功并恢复下一次调度,不新增
browser_task_runs一类旁路表 - 自动化详情页、运行历史和风险任务面板开始直接消费
agent_runs.metadata.human_reason,等待人工/人工接管/恢复中的原因不再只藏在实时面板里 - 自动化主列表开始直显
当前阻塞 / 最近异常摘要,值守时无需进入详情页也能判断浏览器任务卡在什么环节 delivery_json已扩成最小输出投递配置,支持output_format=text|jsondelivery_json已继续扩展为最小输出契约,新增output_schema- 当前
output_schema第一版支持:textjsontablecsvlinks
- 第一批输出目标先落
webhook / local_file;webhook会携带结构化output_data,local_file用于最小闭环落盘,telegram继续只作为兼容通知通道 telegram现在明确固定为文本提醒,不承诺结构化 output schema;结构化下游集成只允许继续收敛到webhook / local_fileautomation_jobs已补最小last_delivery_json,最近一次投递结果继续收敛在任务主记录里,不新增投递历史旁路表- 自动化详情页开始直接展示:
- 输出契约
- 最近一次投递结果
best_effort=false的语义已收口为真实失败:- 输出投递失败会把本次 job 最终状态记为
error - 最近一次运行 metadata 会携带
delivery摘要,运行历史与详情页不再各写一套投递状态
- 输出投递失败会把本次 job 最终状态记为
本阶段做什么:
- 第一阶段:继续基于
automation_jobs承载浏览器任务模板 - 调度执行时自动装配:
automation job -> profile -> environment preset -> browser session - 支持一次性、周期性、cron
- 支持“需要人工介入”的挂起态,与当前
waiting_for_human / human_controlling状态机打通 - 执行历史统一写入现有执行追踪体系
原因:
- 没有 P1/P2,任务就不可复现
- 定时任务是对稳定会话装配能力的消费方,不应先于底座实现
P4. 输入和输出 + 连接器(中优先级)
目标:让浏览器任务结果可被下游系统稳定消费。
本阶段做什么:
- 定义
output_schema - 支持输出类型:
text / json / table / csv / links - 支持输出目标:
download / local_file / webhook / connector - 引入 Browser Connector 抽象
- 第一批只做
webhook和google_sheets
建议顺序:
- 先做结构化输出 schema
- 再做 connector 适配器
当前进展(第三刀已落地):
delivery_json已同时承载:output_schemaoutput_format
output_schema负责表达语义契约,output_format只负责投递编码webhook当前会稳定输出:output_schemaoutput_formatoutput_data
local_file当前支持:- text 模式按 schema 渲染
- json 模式落结构化 payload
automation_jobs.last_delivery_json已承载最近一次投递结果,历史开关关闭时仍可直接在任务详情中观察agent_runs.metadata.delivery已补投递摘要,运行历史和详情页共用同一份运行态事实
当前进展(第四刀已落地):
- 不新增
browser_connectors表,也不引入独立 connector runtime - 第一个真正 connector 已继续收敛到现有
delivery边界:channel=google_sheets- 继续使用
automation_jobs.delivery_json - 继续把最近一次投递结果写回
automation_jobs.last_delivery_json
google_sheets当前采用最小 service account 直连模式:- 目标串使用
spreadsheet_id=...;sheet=...;credentials_file=... - 可选
include_header=true - 可选
value_input_option=RAW|USER_ENTERED
- 目标串使用
- 输出语义继续复用现有
output_schema:table/csv直接按行追加links追加为链接记录text/json追加为单行摘要/JSON 记录
telegram仍维持compat文本通知;结构化下游集成只允许继续收敛到webhook / local_file / google_sheets
当前进展(第五刀已落地):
- delivery 幂等与重试语义继续收敛在同一条事实源:
automation_service::deliveryautomation_jobs.last_delivery_jsonagent_runs.metadata.delivery
- 新增稳定
delivery_attempt_id:- 有
run_id时直接复用dlv-{run_id} - 无 history/run_id 时按
job_id + started_at + execution_retry_count生成稳定哈希键
- 有
webhook当前会输出并透传:- payload 字段
delivery_attempt_id - 请求头
Idempotency-Key - 请求头
X-Lime-Delivery-Attempt-Id
- payload 字段
google_sheets当前会在每一行前置:delivery_attempt_idrun_idjob_idexecution_retry_count
- 网络型输出目标当前采用最小内建重试:
webhookgoogle_sheets- 默认最多 3 次,保留同一个
delivery_attempt_id
last_delivery_json与运行历史 metadata 当前会继续记录:delivery_attempt_idrun_idexecution_retry_countdelivery_attempts
下一刀不应继续堆通知通道,应该优先补:
- 输出目标的能力边界说明
- 连接器失败重试与幂等策略
原因:
- 没有统一输出 schema,连接器会各自解析页面结果,后续无法维护
P5. 证书(中低优先级)
目标:支持企业站点、银行类或需要 mTLS 的场景。
本阶段做什么:
- 先只支持
client_tls证书资产管理 - 支持证书与 host pattern 绑定
- 启动浏览器时注入证书选择策略或使用平台能力完成匹配
为什么不是更早:
- 这是企业纵深能力,不是大多数浏览器任务的基础阻塞项
P6. 自动验证码求解器(低优先级)
目标:减少人工介入,但不破坏当前可用的人机协同链路。
本阶段做什么:
- 先定义
CaptchaSolver抽象 - 再接第三方供应商
- 最后支持策略:自动求解失败后回退人工接管
为什么最后做:
- 当前已有人工接管 + 实时画面,可满足可用性底线
- CAPTCHA 成本高、供应商不稳定、风控强,不应先于 Profile/Preset/Task/I-O
9.6 建议的数据库与模块落点
建议新增表:
browser_profilesbrowser_environment_presetsbrowser_profile_bindingsbrowser_task_templatesbrowser_task_runsbrowser_connectorsbrowser_certificate_assets
建议新增模块:
src-tauri/src/browser_control/profile_service.rsenvironment_preset_service.rstask_template_service.rsconnector_service.rscertificate_service.rs
建议保持不动的模块:
src-tauri/crates/browser-runtime/- 继续只做运行时与 CDP 交互
src-tauri/src/commands/webview_cmd.rs- 继续做命令入口,但逐步改为调用新 service
src-tauri/src/app/scheduler_service.rs- 继续复用,不重新发明调度器
9.7 结论
从代码现状看,Lime 并不是“还没有浏览器底座”,而是已经跨过了最难的第一步:
- 已有实时画面
- 已有 CDP 会话
- 已有人工接管
- 已有独立 Chrome profile
- 已有通用调度器
真正缺的是中间那层“浏览器控制面产品模型”:
- Profile 还是目录,不是资产
- 调度器还是通用任务,不是浏览器任务
- 输出还是原始事件,不是结构化结果
- 连接还是终端连接,不是业务连接器
所以后续路线必须是:
- 先收口控制面
- 再做个人资料
- 再做位置定制
- 再做浏览器定时任务
- 再做输入和输出与连接器
- 最后补证书与自动验证码
这条路线最符合当前仓库状态,也最符合 KISS / YAGNI / DRY:先把已有底座变成稳定平台,再让上层功能自然长出来。