docs: document persistent browser workflows

This commit is contained in:
xiaoxixi 2026-07-28 23:50:57 +08:00
parent 03bfa0ba2b
commit efb0e7f0b0
9 changed files with 88 additions and 39 deletions

View File

@ -103,8 +103,8 @@ Scheduler → SessionManager scheduled execution → AgentLoop → Scheduler del
- **WebUI authentication** protects every management API and `/ws`; only static pairing assets, public health/status, and pairing submission may bypass device auth. Pair-code issuance requires both a real loopback peer and the filesystem-held admin token. The same conjunction may authenticate only `/ws` for local one-shot `run`; it must never authorize management APIs. Never put bearer or admin tokens in URLs or logs - **WebUI authentication** protects every management API and `/ws`; only static pairing assets, public health/status, and pairing submission may bypass device auth. Pair-code issuance requires both a real loopback peer and the filesystem-held admin token. The same conjunction may authenticate only `/ws` for local one-shot `run`; it must never authorize management APIs. Never put bearer or admin tokens in URLs or logs
- **Providers** are pure HTTP clients; no bus/session/channel awareness - **Providers** are pure HTTP clients; no bus/session/channel awareness
- **Provider reasoning state** is private replay data: persist it, replay it only to the matching provider, and never expose it to clients, channels, or logs - **Provider reasoning state** is private replay data: persist it, replay it only to the matching provider, and never expose it to clients, channels, or logs
- **Tools** are executed by `AgentLoop`; they receive raw arguments and normally return text. Tools that produce model-consumable media use the structured `execute_with_media` side channel; model capability checks and provider content-block serialization stay outside tools - **Tools** are executed by `AgentLoop`; every invocation is normalized to `ToolOutput` and passes through `ToolOutputProcessor`. Plain `ToolResult` implementations use the default conversion, while artifact-producing tools declare model/user audience explicitly; model capability checks, final-reply attachment, channel delivery, and Provider serialization stay outside tools
- **Stateful tools** receive `ToolExecutionContext`; browser automation maps each PicoBot dialog to an opaque agent-browser session, uses per-session serialization, and returns screenshots through structured media. Do not reintroduce Fantoccini, ChromeDriver, WebDriver, or model-controlled raw browser session IDs - **Stateful tools** receive `ToolExecutionContext`; browser calls without `persistent_id` map each PicoBot dialog to an opaque transient agent-browser session. For long-running work an Agent may autonomously create a persistent identity and must pass its validated ID on every related action; the same ID shares one agent-browser session and serialization gate across dialogs, while different IDs have independent sessions/gates. `browser_profiles` may create IDs, persist bounded semantic labels, list, or delete only validated IDs beneath the configured profile root. Do not introduce a global persistence mode switch, a default persistent ID, automatic per-dialog persistent Profiles, Fantoccini, ChromeDriver, WebDriver, model-controlled raw agent-browser session IDs, or arbitrary Profile paths
- **Health diagnostics** are read-only and share `HealthService` across `picobot health`, the `health` tool, and `/health`; checks must not install/fix dependencies, call Provider APIs, or expose secrets - **Health diagnostics** are read-only and share `HealthService` across `picobot health`, the `health` tool, and `/health`; checks must not install/fix dependencies, call Provider APIs, or expose secrets
### Concurrency and Lifecycle Invariants ### Concurrency and Lifecycle Invariants

View File

@ -347,7 +347,8 @@ PicoBot 有两类记忆:
| `cron_add/list/remove/enable/disable/update` | 管理定时任务 | | `cron_add/list/remove/enable/disable/update` | 管理定时任务 |
| `routine_maintenance` | 安全清理超过保留期的 Timeline不删除 Knowledge | | `routine_maintenance` | 安全清理超过保留期的 Timeline不删除 Knowledge |
| `health` | 检查核心、配置相关和可选运行依赖 | | `health` | 检查核心、配置相关和可选运行依赖 |
| `browser` | 可选 agent-browser 浏览器自动化;每个 dialog 独立会话 | | `browser` | 可选 agent-browser 浏览器自动化;默认按 dialog 临时使用,长期任务可用 `persistent_id` 复用个人 Profile |
| `browser_profiles` | 创建、设置语义标签、列出或删除浏览器持久 ID 及其 Profile 目录 |
| MCP tools | 从配置的 MCP Server 动态发现并注册 | | MCP tools | 从配置的 MCP Server 动态发现并注册 |
### Skills ### Skills
@ -438,12 +439,19 @@ agent-browser install
"content_boundaries": true, "content_boundaries": true,
"allowed_domains": [], "allowed_domains": [],
"allow_private_hosts": false, "allow_private_hosts": false,
"artifact_dir": "~/.picobot/media/browser" "artifact_dir": "~/.picobot/media/browser",
"persistence": {
"profile_dir": "~/.picobot/browser/profiles"
}
} }
} }
``` ```
浏览器工具默认启用;缺少 agent-browser 或 Chrome 不阻止 Gateway 启动,但实际调用会失败并给出安装提示,`picobot health` 也会提前报告。修改后建议先运行 health再启动或重载 Gateway。旧的 `webdriver_url``chrome_path` 配置已删除,出现这两个字段时配置校验会明确失败。实际使用仍由 Agent 调用 `browser``open``snapshot` 获取 `@e1` 等引用 → `click` / `fill` / `type` → 页面变化后重新 `snapshot`。截图保存到受控产物目录并作为结构化图片返回,不再生成 Base64 工具文本。 浏览器没有全局“持久模式”开关,而是按每次调用分流:不传 `persistent_id` 时使用当前 dialog 的普通临时浏览器涉及长期工作、需要保留登录或站点状态时Agent 可以自主调用 `browser_profiles(create,label=...)` 生成 `picobot-profile-<uuid>`,并在该工作的后续每个 `browser` action 中持续传入同一个 ID。也可以用 `set_label` 随时修改语义化标签。PicoBot 不设置默认 ID也不会按 dialog 自动选择持久 Profile。同一 ID 跨 dialog 共享 agent-browser session 和串行锁,不同 ID 使用各自的 session、锁与 Chrome Profile因而可以并发操作。Cookie、localStorage、IndexedDB、Service Worker、缓存和标签随各自目录持久化。
`browser_profiles` 支持 `create``set_label``list``delete``list` 返回 ID、标签、目录和 active 状态,浏览器仍始终用不可变 ID 选择重命名标签不会破坏现有调用。Profile 根目录位于 `~/.picobot` 内,现有 Docker `picobot_data` 卷会一并持久化。agent-browser 无法同时保证 Profile 复用与 `allowed_domains` 域名隔离配置非空白名单后普通临时浏览器仍可用持久身份的创建和使用会被拒绝health 会给出可选能力警告。
缺少 agent-browser 或 Chrome 不阻止 Gateway 启动,但实际调用会失败并给出安装提示,`picobot health` 也会提前报告。修改后建议先运行 health再启动或重载 Gateway。旧的 `webdriver_url``chrome_path` 配置已删除,出现这两个字段时配置校验会明确失败。实际使用仍由 Agent 调用 `browser``open``snapshot` 获取 `@e1` 等引用 → `click` / `fill` / `type` → 页面变化后重新 `snapshot`。截图保存到受控产物目录,通过统一工具输出管线交给多模态模型,并默认附到本轮最终回复给用户查看,不再生成 Base64 工具文本;仅需模型内部检查时可显式设置 `present_to_user=false`
详细开发分层、进程协议、并发/安全边界和故障语义见 [agent-browser 集成设计](docs/AGENT_BROWSER_INTEGRATION.md)。 详细开发分层、进程协议、并发/安全边界和故障语义见 [agent-browser 集成设计](docs/AGENT_BROWSER_INTEGRATION.md)。

View File

@ -1,6 +1,6 @@
# agent-browser 集成设计 # agent-browser 集成设计
本文描述 PicoBot 1.3.1 的浏览器工具实现。目标是在保持模型侧单一 `browser` 工具协议的同时,用 agent-browser 完全替代 Fantoccini、ChromeDriver 和 WebDriver并让浏览器状态、并发、产物和健康检查服从 PicoBot 的 Session 生命周期 本文描述 PicoBot 1.4.0 的浏览器工具实现。目标是在保持模型侧稳定 `browser` 协议的同时,用 agent-browser 完全替代 Fantoccini、ChromeDriver 和 WebDriver并让临时会话、单用户多持久 Profile、管理操作、产物和健康检查拥有明确边界
## 1. 选择与边界 ## 1. 选择与边界
@ -10,7 +10,7 @@ PicoBot 使用 agent-browser CLI 的 `--json` 协议,不直接链接其内部
- agent-browser 是原生 Rust CLI + daemondaemon 通过 Chrome CDP 驱动浏览器CLI 进程很短,浏览器状态跨命令保存在 daemon 中。 - agent-browser 是原生 Rust CLI + daemondaemon 通过 Chrome CDP 驱动浏览器CLI 进程很短,浏览器状态跨命令保存在 daemon 中。
- CLI 是项目的稳定公开边界PicoBot 不需要依赖 agent-browser 的内部 Rust 模块布局。 - CLI 是项目的稳定公开边界PicoBot 不需要依赖 agent-browser 的内部 Rust 模块布局。
- PicoBot 包装层可统一绑定 dialog、限制并发和输出、校验 URL、控制截图目录并把图片接入现有 `ToolResultWithMedia` - PicoBot 包装层可统一管理个人浏览器身份、限制并发和输出、校验 URL、控制 Profile/截图目录,并把图片接入统一 `ToolOutput` 后处理管线
- 直接暴露 MCP 会让 session ID、文件路径、输出规模和安全策略落到模型参数中也难以自动绑定当前 PicoBot dialog。 - 直接暴露 MCP 会让 session ID、文件路径、输出规模和安全策略落到模型参数中也难以自动绑定当前 PicoBot dialog。
这不是把浏览器逻辑重新实现一遍。元素定位、accessibility snapshot、页面交互、Chrome 启动、CDP 通信和 daemon 生命周期均由 agent-browser 负责PicoBot 只负责编排和边界控制。 这不是把浏览器逻辑重新实现一遍。元素定位、accessibility snapshot、页面交互、Chrome 启动、CDP 通信和 daemon 生命周期均由 agent-browser 负责PicoBot 只负责编排和边界控制。
@ -21,9 +21,9 @@ PicoBot 使用 agent-browser CLI 的 `--json` 协议,不直接链接其内部
AgentLoop AgentLoop
│ ToolExecutionContext(session_id, turn_id) │ ToolExecutionContext(session_id, turn_id)
BrowserTool 模型侧单一 browser schema BrowserTool / BrowserProfilesTool 浏览与持久 Profile 管理 schema
BrowserManager dialog → opaque session并发/空闲/产物 BrowserManager 临时 dialog session / 共享持久 Profile
├─ security URL、DNS、私网与 allowlist 前置校验 ├─ security URL、DNS、私网与 allowlist 前置校验
├─ action browser action → CLI argv ├─ action browser action → CLI argv
└─ AgentBrowserRunner timeout、env、--json、错误与输出解析 └─ AgentBrowserRunner timeout、env、--json、错误与输出解析
@ -33,26 +33,30 @@ agent-browser CLI → Rust daemon → Chrome/Chromium CDP
源文件: 源文件:
- `src/tools/browser/mod.rs`工具 schema 与入口。 - `src/tools/browser/mod.rs``browser``browser_profiles` schema 与入口。
- `src/tools/browser/action.rs`:严格参数解析和 argv 映射。 - `src/tools/browser/action.rs`:严格参数解析和 argv 映射。
- `src/tools/browser/manager.rs`会话表、per-session mutex、空闲回收、截图媒体。 - `src/tools/browser/manager.rs`临时会话表、按 ID 管理的持久 Profile、并发、回收和截图媒体。
- `src/tools/browser/runner.rs`:无 Shell 的子进程调用、硬超时、JSON/错误解析。 - `src/tools/browser/runner.rs`:无 Shell 的子进程调用、硬超时、JSON/错误解析。
- `src/tools/browser/security.rs`:导航策略。 - `src/tools/browser/security.rs`:导航策略。
- `src/tools/traits.rs`:向有状态工具提供 `ToolExecutionContext`;其他工具沿用默认实现。 - `src/tools/traits.rs`:向有状态工具提供 `ToolExecutionContext`;其他工具沿用默认实现。
## 3. 会话与并发 ## 3. 会话、持久 ID 与并发
`SessionManager` 为每次 AgentLoop 执行传入完整 PicoBot session ID。BrowserManager 第一次看到该 ID 时生成随机、不透明的 `picobot-<uuid>` agent-browser session模型不能选择或猜测底层 session。 BrowserManager 按每次调用是否携带 `persistent_id` 分流,两种浏览器可在同一个 Gateway 中同时使用:
- 同一个 dialog所有浏览器 action 由该 session 的 mutex 串行cookie、storage、历史和当前页面连续。 - 省略 `persistent_id` 时使用普通临时浏览器。`SessionManager` 传入完整 PicoBot session IDBrowserManager 第一次看到该 ID 时生成随机、不透明的 `picobot-<uuid>` agent-browser session。同一 dialog 串行、不同 dialog 可并发;空闲会话按 `idle_timeout_secs` 回收,数量受 `max_sessions` 限制。
- 不同 dialog使用不同 agent-browser session可并发执行。 - 需要长期保留登录或站点状态时Agent 可以自主调用 `browser_profiles(action=create,label=...)` 创建持久身份,并在后续相关的每个 `browser` action 中显式传入返回的 `persistent_id`
- 子 Agent若显式获准使用 `browser`,沿用发起任务的 PicoBot session因此与主 Agent 共享同一浏览器并受同一 mutex 保护。
- Scheduler使用 `cron:<job-id/name>` 隔离,不与交互 dialog 混用。
- `close`:先从 PicoBot 映射表移除,再调用 agent-browser close重复关闭幂等。
- 空闲回收:创建新会话前移除超过 `idle_timeout_secs` 的映射并尽力关闭底层 session。
- 容量:达到 `max_sessions` 且没有可回收会话时明确失败,不静默复用别人的浏览器。
Gateway 配置重载会构造新的 ToolRegistry/BrowserManager旧运行代按现有 drain 规则退出。agent-browser daemon 的空闲退出时间通过 `AGENT_BROWSER_IDLE_TIMEOUT_MS` 同步设置,避免遗留浏览器无限驻留。 - 用户可以拥有多个 `picobot-profile-<32 hex>` ID。`browser_profiles(action=create,label=...)` 创建 `persistence.profile_dir/<id>` 专用目录和可选语义标签;标签可通过 `set_label` 重命名ID 保持不变。
- PicoBot 不保存默认 ID也不按 dialog 自动选择或绑定持久 Profile。没有 ID 的调用始终回到该 dialog 的临时浏览器,不会隐式选中任何持久身份。
- 每个 ID 分别映射 agent-browser `--session`、Chrome `--profile` 路径和 mutex。同一 ID 可从不同 dialog、子 Agent 或 Scheduler 使用并保持串行;不同 ID 的浏览器状态和锁相互独立,可以并发。
- ID 与 Profile 目录跨 Gateway 重启、配置重载和 agent-browser daemon 空闲退出保持不变Cookie、localStorage、IndexedDB、Service Worker 和缓存由各 Chrome Profile 自身保存。
- `close` 关闭显式选择的浏览器进程但保留 ID、标签和 Profile下一次使用该 ID 时从相同目录重新打开。
- `browser_profiles(action=list)` 返回每个合法 ID 的标签、完整目录和当前 Manager 是否 active。
- `browser_profiles(action=set_label,id=...,label=...)` 写入语义标签并同步当前活动实例;标签去除首尾空白,限制为 180 个非控制字符。
- `browser_profiles(action=delete,id=...)` 只接受 `create/list` 返回的完整格式 ID。删除时持有管理锁、等待该 ID 的活动 action、尝试关闭其浏览器再递归删除对应目录。
Profile 根目录和每个生成目录在 Unix 上收敛为 `0700`,目录内 `.picobot-label` 标签文件为 `0600`。列表忽略格式非法的目录和符号链接选择、改标签和删除拒绝路径穿越、符号链接及非目录目标。Gateway 配置重载会构造新的 ToolRegistry/BrowserManager旧运行代按现有 drain 规则退出。agent-browser daemon 的空闲退出时间通过 `AGENT_BROWSER_IDLE_TIMEOUT_MS` 同步设置。
## 4. Action 映射 ## 4. Action 映射
@ -67,9 +71,9 @@ Gateway 配置重载会构造新的 ToolRegistry/BrowserManager旧运行代
| `screenshot` | `screenshot <controlled-path> [--full] [--annotate]` | | `screenshot` | `screenshot <controlled-path> [--full] [--annotate]` |
| `close` | `close` | | `close` | `close` |
每次调用都使用 argv 数组直接启动进程,不经过 Shell。`fill` / `type` 的内容不会写入 PicoBot 日志;日志只记录 action 名和是否绑定 session 每次调用都使用 argv 数组直接启动进程,不经过 Shell。`fill` / `type` 的内容不会写入 PicoBot 日志;日志只记录 action 名、是否绑定 session 和所选持久 ID
Runner 固定传入 `--session``--json` 和明确的 headed 状态,并设置: Runner 固定传入 `--session``--json` 和明确的 headed 状态;携带持久 ID 的调用额外传入由 Manager 生成的 `--profile <controlled-path>`。Runner 还设置:
- `AGENT_BROWSER_EXECUTABLE_PATH`(配置后) - `AGENT_BROWSER_EXECUTABLE_PATH`(配置后)
- `AGENT_BROWSER_CONTENT_BOUNDARIES` - `AGENT_BROWSER_CONTENT_BOUNDARIES`
@ -86,13 +90,16 @@ Runner 固定传入 `--session`、`--json` 和明确的 headed 状态,并设
命令成功后 PicoBot 再验证文件存在、是普通文件且非空,然后返回: 命令成功后 PicoBot 再验证文件存在、是普通文件且非空,然后返回:
```text ```text
ToolResultWithMedia { ToolOutput {
result: ToolResult { output: "Screenshot saved: ..." }, result: ToolResult { output: "Screenshot saved: ..." },
media_refs: [MediaRef { media_type: "image", path: "..." }] artifacts: [ToolArtifact {
media_ref: MediaRef { media_type: "image", path: "..." },
audience: ModelAndUser
}]
} }
``` ```
因此多模态 Provider 可在下一轮直接看到图片,历史中仍只保存短路径清单,不产生 Base64 上下文膨胀。 统一处理器会把截图同时交给下一轮多模态 Provider并附到本 Turn 最终 assistant 回复供用户查看。`present_to_user` 默认为 `true`;显式设为 `false` 时 audience 改为 `Model`,适用于无需展示的内部视觉检查。历史中仍只保存短路径清单,不产生 Base64 上下文膨胀。
## 6. 安全模型 ## 6. 安全模型
@ -104,9 +111,12 @@ ToolResultWithMedia {
- 默认开启 content boundaries并把页面文本限制为 50,000 字符。 - 默认开启 content boundaries并把页面文本限制为 50,000 字符。
- 包装层没有 `eval`、上传、下载、cookie/storage 写入或任意 agent-browser 命令透传,模型只能使用 allowlisted action。 - 包装层没有 `eval`、上传、下载、cookie/storage 写入或任意 agent-browser 命令透传,模型只能使用 allowlisted action。
- 截图有单独的产物目录,不能用来覆盖任意文件。 - 截图有单独的产物目录,不能用来覆盖任意文件。
- `browser_profiles` 只允许创建受控 ID或按格式严格的 ID 设置受限标签、列出和删除 `persistence.profile_dir` 的直接子目录;模型不能提交任意 Profile 路径。
`allowed_domains=[]` 表示不启用 agent-browser 域名过滤,适合通用浏览;这不是 OS 网络沙箱。需要强隔离时,应同时设置明确域名表和容器/主机 egress 策略。允许私网浏览是显式配置,适合本地应用测试,但会扩大 SSRF 风险。 `allowed_domains=[]` 表示不启用 agent-browser 域名过滤,适合通用浏览;这不是 OS 网络沙箱。需要强隔离时,应同时设置明确域名表和容器/主机 egress 策略。允许私网浏览是显式配置,适合本地应用测试,但会扩大 SSRF 风险。
agent-browser 0.33.0 明确拒绝在 `allowed_domains` 启用时复用 Chrome Profile因为无法保证页面脚本执行前完整安装同等域名约束。PicoBot 因此允许受域名限制的普通临时浏览器继续工作,但会拒绝创建或使用持久 Profile并在 health 中给出可选能力警告列表、改标签和删除仍可用于管理已有目录。Profile 包含可直接代表用户身份的登录信息,目录必须视作敏感凭据,不得提交版本控制或跨用户共享。
## 7. 安装 ## 7. 安装
验证版本为 `0.33.0` 验证版本为 `0.33.0`
@ -146,7 +156,7 @@ agent-browser install
## 8. 使用 ## 8. 使用
1. 浏览器工具默认启用;从旧配置删除 `webdriver_url``chrome_path`加入新的 browser 字段。缺少依赖不会阻止 Gateway 启动,只会让 health 和实际浏览器调用失败。 1. 浏览器工具默认启用;从旧配置删除 `webdriver_url``chrome_path`仅在需要改变持久目录位置时配置 `persistence.profile_dir`。没有持久化模式开关。缺少依赖不会阻止 Gateway 启动,只会让 health 和实际浏览器调用失败。
2. 运行 `picobot health`;应看到 agent-browser CLI 版本和 offline quick doctor 通过。 2. 运行 `picobot health`;应看到 agent-browser CLI 版本和 offline quick doctor 通过。
3. 启动或重载 Gateway。 3. 启动或重载 Gateway。
4. 对 Agent 说“使用浏览器打开 …”。模型的推荐动作序列是: 4. 对 Agent 说“使用浏览器打开 …”。模型的推荐动作序列是:
@ -162,6 +172,19 @@ browser(close)
agent-browser 的 `@e` 引用属于当前页面快照。导航、弹窗或 DOM 大幅变化后必须重新 snapshot不能长期缓存旧引用。 agent-browser 的 `@e` 引用属于当前页面快照。导航、弹窗或 DOM 大幅变化后必须重新 snapshot不能长期缓存旧引用。
持久 Profile 管理:
```text
browser_profiles(create, label="工作账号") # 返回 persistent ID
browser_profiles(list)
browser_profiles(set_label, id=picobot-profile-..., label="个人账号")
browser(open, url, persistent_id=picobot-profile-...)
browser(snapshot, persistent_id=picobot-profile-...)
browser_profiles(delete, id=picobot-profile-...)
```
同一个持久操作链必须持续传入同一 `persistent_id`;一旦省略,调用会明确转到当前 dialog 的普通临时浏览器。不同对话可以同时操作不同 ID。标签用于识别不能代替 ID 选择浏览器;删除会清除该 ID 的全部浏览器数据、标签和登录态,调用前必须有用户要求并使用 `create/list` 返回的精确 ID。
## 9. Health 三入口 ## 9. Health 三入口
`src/health.rs``HealthService` 是唯一检查实现: `src/health.rs``HealthService` 是唯一检查实现:
@ -170,14 +193,16 @@ agent-browser 的 `@e` 引用属于当前页面快照。导航、弹窗或 DOM
- `/health`:当前 Gateway 配置的聊天入口。 - `/health`:当前 Gateway 配置的聊天入口。
- `health` ToolAgent 可调用的只读入口,支持 `json=true` - `health` ToolAgent 可调用的只读入口,支持 `json=true`
检查项包括 workspace、Bash、内容/文件搜索后端、可选 systemctl、配置中的 stdio MCP 命令,以及浏览器启用时的 agent-browser 版本、显式浏览器路径和 `doctor --offline --quick --json`。检查不安装软件、不执行 `doctor --fix`、不访问 Provider API也不输出配置密钥。 检查项包括 workspace、Bash、内容/文件搜索后端、可选 systemctl、配置中的 stdio MCP 命令,以及浏览器启用时的持久 Profile 可用性、agent-browser 版本、显式浏览器路径和 `doctor --offline --quick --json`。检查不创建或删除 Profile、不安装软件、不执行 `doctor --fix`、不访问 Provider API也不输出配置密钥。
## 10. 迁移和故障处理 ## 10. 迁移和故障处理
- 配置使用 `deny_unknown_fields`;遗留 WebDriver 字段会在加载时失败,而不是被静默忽略。 - 配置使用 `deny_unknown_fields`;遗留 WebDriver 字段会在加载时失败,而不是被静默忽略。
- `failed to start 'agent-browser'`CLI 不在 PATH`browser.command` 错误;运行 health。 - `failed to start 'agent-browser'`CLI 不在 PATH`browser.command` 错误;运行 health。
- doctor 失败:运行 `agent-browser doctor` 查看完整诊断,再安装浏览器/系统库。 - doctor 失败:运行 `agent-browser doctor` 查看完整诊断,再安装浏览器/系统库。
- session limit关闭不再使用的 dialog 浏览器,或调整 `max_sessions`;不要让多个 dialog 共享同一底层 ID。 - session limit仅临时模式适用关闭不再使用的 dialog 浏览器,或调整 `max_sessions`
- persistence policy非空 `allowed_domains` 下普通临时浏览器仍可用,但创建或使用持久 Profile 会失败;根据需求选择个人登录态复用或严格域名隔离。
- Profile 删除失败:先确认没有外部 Chrome 使用该目录,再用 `browser_profiles(list)` 核对精确 ID 后重试;不要手工扩大删除路径。
- domain blocked补充站点和必要 CDN 域名;不要用空白 allowlist 绕过生产隔离策略。 - domain blocked补充站点和必要 CDN 域名;不要用空白 allowlist 绕过生产隔离策略。
- command timeout确认页面/浏览器未卡死,再按部署风险调整 `command_timeout_secs` - command timeout确认页面/浏览器未卡死,再按部署风险调整 `command_timeout_secs`
- 截图不存在:视为工具失败,不构造失效 MediaRef。 - 截图不存在:视为工具失败,不构造失效 MediaRef。

View File

@ -195,7 +195,7 @@ Session ID 格式为:
当前 WebUI/TUI Turn 通过 `send_message(files=...)` 向自身 session 投递文件时,文件先进入 task-local Turn delivery 暂存区,成功结束后附加到最终 assistant 消息,与工具链一起原子提交;因此持久化和刷新后的顺序都是工具调用/结果在前、携带附件的最终回复在后,也不会生成带 `[message from ...]` 的自投递气泡。其他同 Turn 自投递仍是受控例外:只有 task-local Turn ID 仍匹配该 session 的 active Turn写入才允许不递增 `state_version`。跨 Turn、跨 session 以及无法证明所有权的写入仍必须递增版本。Provider 回放历史附件时,只有 user 输入和当前工具结果可生成模型原生媒体块assistant/system 附件只回放文本清单,避免把图片放到供应商不接受的角色。 当前 WebUI/TUI Turn 通过 `send_message(files=...)` 向自身 session 投递文件时,文件先进入 task-local Turn delivery 暂存区,成功结束后附加到最终 assistant 消息,与工具链一起原子提交;因此持久化和刷新后的顺序都是工具调用/结果在前、携带附件的最终回复在后,也不会生成带 `[message from ...]` 的自投递气泡。其他同 Turn 自投递仍是受控例外:只有 task-local Turn ID 仍匹配该 session 的 active Turn写入才允许不递增 `state_version`。跨 Turn、跨 session 以及无法证明所有权的写入仍必须递增版本。Provider 回放历史附件时,只有 user 输入和当前工具结果可生成模型原生媒体块assistant/system 附件只回放文本清单,避免把图片放到供应商不接受的角色。
SessionManager 负责组装会话上下文系统提示、Skills、召回的 Knowledge、压缩后的 Timeline、可选的 active plan 摘要和当前消息历史。`session::turn_input` 在 Session 锁外并行读取 Knowledge、active plan 并压缩历史,然后通过同一个 assembly 路径生成首次请求和 context-overflow 重试输入;重试不得复制系统提示或 runtime context 拼接逻辑。普通闲聊 session 没有 plan 摘要;计划状态由 `WorkManager` 从 SQLite 读取,因此不以自然语言摘要作为权威来源。`AgentLoop` 接收完整输入执行一次模型/工具循环,本身不拥有会话状态。执行工具时额外传递只包含 session/turn 身份的 `ToolExecutionContext`;无状态工具使用默认实现忽略它,有状态外部适配器必须用它隔离资源,不能自行反向查询 SessionManager。 SessionManager 负责组装会话上下文系统提示、Skills、召回的 Knowledge、压缩后的 Timeline、可选的 active plan 摘要和当前消息历史。`session::turn_input` 在 Session 锁外并行读取 Knowledge、active plan 并压缩历史,然后通过同一个 assembly 路径生成首次请求和 context-overflow 重试输入;重试不得复制系统提示或 runtime context 拼接逻辑。普通闲聊 session 没有 plan 摘要;计划状态由 `WorkManager` 从 SQLite 读取,因此不以自然语言摘要作为权威来源。`AgentLoop` 接收完整输入执行一次模型/工具循环,本身不拥有会话状态。执行工具时额外传递只包含 session/turn 身份的 `ToolExecutionContext`;无状态工具使用默认实现忽略它,有状态外部适配器用它路由资源,但可按明确的单用户配置跨 dialog 共享,且不能自行反向查询 SessionManager。
当前 Turn 的工具进度只从 `AgentLoop` 的结构化 `TurnEvent` 进入 `TurnController`,不能另建字符串 notification 通道重复投递。后台子 Agent 的 `TaskNotification` 表达跨 Turn 的任务完成仍由独立的受监督消费者投递。自动标题属于非关键派生工作Turn 持久化完成后由 `TaskSupervisor` 调度Session worker 不等待模型生成;同一 Session 同时最多有一个标题任务,提交时仍校验标题保持默认值,避免覆盖用户改名。 当前 Turn 的工具进度只从 `AgentLoop` 的结构化 `TurnEvent` 进入 `TurnController`,不能另建字符串 notification 通道重复投递。后台子 Agent 的 `TaskNotification` 表达跨 Turn 的任务完成仍由独立的受监督消费者投递。自动标题属于非关键派生工作Turn 持久化完成后由 `TaskSupervisor` 调度Session worker 不等待模型生成;同一 Session 同时最多有一个标题任务,提交时仍校验标题保持默认值,避免覆盖用户改名。
@ -251,9 +251,9 @@ Gateway 在 `/` 提供随二进制编译的 HTML/CSS/JavaScript不依赖外
WebUI/TUI 文件字节通过受鉴权的 HTTP 接口流式传输WebSocket 只携带短期 `upload_id` 和结构化附件描述。`UploadRegistry` 在内存中按 `cli_chat` chat scope 校验并消费待发送上传;消息继续以 `media_refs` 保存 Gateway 本地路径,不建立永久附件资产。下载接口必须通过 client、session、message 和附件序号反查路径,不能接受客户端路径。历史附件路径失效属于正常状态,不得影响历史文本读取。待发送但未进入消息的上传由 `TaskSupervisor` 所有的限时清理任务回收。Agent 上下文会为所有媒体注入内部路径清单,客户端响应不得暴露该路径。 WebUI/TUI 文件字节通过受鉴权的 HTTP 接口流式传输WebSocket 只携带短期 `upload_id` 和结构化附件描述。`UploadRegistry` 在内存中按 `cli_chat` chat scope 校验并消费待发送上传;消息继续以 `media_refs` 保存 Gateway 本地路径,不建立永久附件资产。下载接口必须通过 client、session、message 和附件序号反查路径,不能接受客户端路径。历史附件路径失效属于正常状态,不得影响历史文本读取。待发送但未进入消息的上传由 `TaskSupervisor` 所有的限时清理任务回收。Agent 上下文会为所有媒体注入内部路径清单,客户端响应不得暴露该路径。
工具默认通过 `ToolResult` 返回文本;需要把图片等产物交给模型时,通过 `Tool::execute_with_media` 返回文本和结构化 `MediaRef`。工具只负责经过自身路径策略校验后声明媒体,不感知当前模型或 Provider`AgentLoop` 仅将最新连续工具结果批次的媒体交给 `MediaHandlerRegistry`,旧工具媒体只回放文本和路径,避免历史 Base64 膨胀。OpenAI-compatible Provider 保持 `tool` 结果为文本,并在完整工具批次后构造仅存在于请求内的临时多模态 `user` 消息Anthropic Provider 将媒体放入对应 `tool_result.content`。媒体加载、格式或能力检查失败必须降级成文本,不得使历史记录不可读取。 所有工具调用统一归一化为 `ToolOutput`,并由 `AgentLoop` 中唯一的 `ToolOutputProcessor` 后处理。普通文本工具仍实现 `ToolResult`,默认转换会将其包装为无产物的 `ToolOutput`;产物工具返回带 `ToolArtifact` 的输出,并用 `Model``User``ModelAndUser` 声明受众。处理器只发布成功工具的产物去重后分别形成下一轮模型媒体和最终用户回复附件。工具只负责经过自身路径策略校验后声明产物与意图不感知当前模型、Provider、Session 或 Channel`AgentLoop` 仅将最新连续工具结果批次的模型媒体交给 `MediaHandlerRegistry`,旧工具媒体只回放文本和路径,避免历史 Base64 膨胀;用户媒体累积到本 Turn 最终 assistant 消息,随工具链原子持久化,并由 committed-history 或普通出站路径呈现。OpenAI-compatible Provider 保持 `tool` 结果为文本,并在完整工具批次后构造仅存在于请求内的临时多模态 `user` 消息Anthropic Provider 将媒体放入对应 `tool_result.content`。媒体加载、格式或能力检查失败必须降级成文本,不得使历史记录不可读取。
`browser` 是有状态工具适配器:`BrowserTool` 保持模型侧 action schema`BrowserManager` 把 PicoBot dialog 映射到随机 agent-browser session并用每 session mutex 保证同一页面串行、不同 dialog 并发;`AgentBrowserRunner` 以 argv 和 `--json` 调用外部原生 CLI设置硬超时、输出/content boundaries/domain allowlist底层 daemon 通过 Chrome CDP 工作。PicoBot 不链接 agent-browser 内部 crate、不直接暴露其 MCP、不使用 Fantoccini/ChromeDriver/WebDriver。截图只能写入配置的 artifact directory`ToolResultWithMedia` 返回。完整边界见 [AGENT_BROWSER_INTEGRATION.md](AGENT_BROWSER_INTEGRATION.md)。 `browser` 是有状态工具适配器:`BrowserTool` 保持模型侧 action schema`BrowserManager` 按每次调用是否带 `persistent_id` 分流。省略 ID 时把 PicoBot dialog 映射到随机临时 agent-browser session并用每 session mutex 保证同一页面串行、不同 dialog 并发;长期工作需要保留登录或站点状态时Agent 可自主创建持久身份并在后续相关 action 中持续传入同一个 ID。Manager 按持久 ID 保存 agent-browser session 和 mutex同一 ID 跨 dialog 共享且串行,不同 ID 相互独立并可并发Gateway 重启或 daemon 退出后继续使用原 Profile没有全局持久化开关、默认 ID 或按 dialog 隐式选择。`browser_profiles` 在受控根目录下创建、设置语义化标签、列出或删除格式合法的 ID标签只负责识别选择仍使用不可变 ID删除活动 ID 时先等待其 action 并关闭浏览器。`AgentBrowserRunner` 以 argv 和 `--json` 调用外部原生 CLI设置硬超时、输出/content boundaries/domain allowlist并在持久调用中传入受控 `--profile` 路径,底层 daemon 通过 Chrome CDP 工作。PicoBot 不链接 agent-browser 内部 crate、不直接暴露其 MCP、不使用 Fantoccini/ChromeDriver/WebDriver。持久 Profile 与 `allowed_domains` 因上游安全边界互斥;设置域名限制时临时浏览器仍可用,持久调用会被拒绝。截图只能写入配置的 artifact directory作为 `ModelAndUser` 产物返回,默认附到最终用户回复;仅当调用显式设置 `present_to_user=false` 时才作为模型内部观察。完整边界见 [AGENT_BROWSER_INTEGRATION.md](AGENT_BROWSER_INTEGRATION.md)。
`HealthService` 是依赖检查的唯一实现。CLI `picobot health`、只读 `health` 工具和 `/health` 斜杠命令必须复用它;检查可探测命令、版本、配置路径和 agent-browser offline quick doctor但不能安装/修复软件、连接模型 API 或泄漏配置秘密。 `HealthService` 是依赖检查的唯一实现。CLI `picobot health`、只读 `health` 工具和 `/health` 斜杠命令必须复用它;检查可探测命令、版本、配置路径和 agent-browser offline quick doctor但不能安装/修复软件、连接模型 API 或泄漏配置秘密。

View File

@ -96,7 +96,10 @@
"content_boundaries": true, "content_boundaries": true,
"allowed_domains": [], "allowed_domains": [],
"allow_private_hosts": false, "allow_private_hosts": false,
"artifact_dir": "~/.picobot/media/browser" "artifact_dir": "~/.picobot/media/browser",
"persistence": {
"profile_dir": "~/.picobot/browser/profiles"
}
}, },
"workspace_dir": "~/.picobot/workspace" "workspace_dir": "~/.picobot/workspace"
} }

View File

@ -62,7 +62,8 @@ Scheduler → SessionManager.handle_cron_message → AgentLoop → send_message
- ChannelManager 持有 MessageBus 和所有 channel - ChannelManager 持有 MessageBus 和所有 channel
- OutboundDispatcher 通过 ChannelManager 路由出站消息 - OutboundDispatcher 通过 ChannelManager 路由出站消息
- 配置目录 `.env` 与 workspace `.env` 仅在单线程启动阶段分层加载,并使用 `unsafe { env::set_var(...) }` 写入进程环境;优先级为既有进程环境 > workspace > 配置目录 - 配置目录 `.env` 与 workspace `.env` 仅在单线程启动阶段分层加载,并使用 `unsafe { env::set_var(...) }` 写入进程环境;优先级为既有进程环境 > workspace > 配置目录
- `browser` 工具默认启用,只有 `browser.enabled=false` 时不注册;缺少 CLI/Chrome 不阻止 Gateway 启动,但 health 和实际调用会给出安装错误。每个 PicoBot dialog 映射到独立 agent-browser session底层原生 daemon 使用 Chrome CDP不依赖 Fantoccini/ChromeDriver/WebDriver - `browser` 工具默认启用,只有 `browser.enabled=false` 时不注册;缺少 CLI/Chrome 不阻止 Gateway 启动,但 health 和实际调用会给出安装错误。每次调用按参数分流:不传 `persistent_id` 时按 dialog 使用普通临时浏览器;长期工作时 Agent 可自主创建持久身份,并在后续相关 action 中持续传入同一个 ID。同一 ID 跨 dialog 共享 session/锁,不同 ID 相互独立并可并发。`browser_profiles` 只在受控根目录中创建、设置语义标签、列出或删除合法 ID没有全局持久化开关、默认 ID也不自动按 dialog 建立或选择持久 Profile不依赖 Fantoccini/ChromeDriver/WebDriver
- 所有工具调用统一包装为 `ToolOutput` 并经过公共处理器;产物按模型/用户受众分流。浏览器截图默认同时供模型查看并附到最终回复,`file_read` 图片默认仅供模型理解
- 同一 session 的普通消息串行处理,不同 session 可并发session 队列容量为 32满时明确拒绝 - 同一 session 的普通消息串行处理,不同 session 可并发session 队列容量为 32满时明确拒绝
- 出站消息按 `(channel, chat_id)` 分 lane 保序lane 容量为 64慢目标不阻塞其他目标 - 出站消息按 `(channel, chat_id)` 分 lane 保序lane 容量为 64慢目标不阻塞其他目标
- 活动 Turn 与普通出站消息共享 `(channel, chat_id)` 写锁;禁止把 token delta 放入 MessageBus - 活动 Turn 与普通出站消息共享 `(channel, chat_id)` 写锁;禁止把 token delta 放入 MessageBus

View File

@ -129,7 +129,7 @@ MCP 服务器单条配置:
## browser 字段 ## browser 字段
浏览器工具默认开启并注册 `browser` 工具。缺少外部依赖不会阻止 Gateway 启动,但实际调用会返回安装错误,`picobot health` 会提前判定。上层由 PicoBot 管理 dialog 会话与媒体,底层调用 agent-browser JSON CLI不再依赖 Fantoccini、ChromeDriver 或 WebDriver。 浏览器工具默认开启并注册 `browser` `browser_profiles` 工具。缺少外部依赖不会阻止 Gateway 启动,但实际调用会返回安装错误,`picobot health` 会提前判定。上层由 PicoBot 管理浏览器生命周期与媒体,底层调用 agent-browser JSON CLI不再依赖 Fantoccini、ChromeDriver 或 WebDriver。
| 字段 | 类型 | 默认 | 说明 | | 字段 | 类型 | 默认 | 说明 |
|------|------|------|------| |------|------|------|------|
@ -137,7 +137,7 @@ MCP 服务器单条配置:
| `command` | string | agent-browser | CLI 名称或绝对路径 | | `command` | string | agent-browser | CLI 名称或绝对路径 |
| `headless` | bool | true | 是否无头运行 | | `headless` | bool | true | 是否无头运行 |
| `browser_executable_path` | string | - | 自定义 Chrome/Chromium 可执行文件路径 | | `browser_executable_path` | string | - | 自定义 Chrome/Chromium 可执行文件路径 |
| `max_sessions` | int | 4 | 同时保留的 dialog 浏览器会话上限 | | `max_sessions` | int | 4 | 同时保留的普通 dialog 临时浏览器会话上限;持久身份不计入 |
| `idle_timeout_secs` | int | 900 | PicoBot 会话清理及 agent-browser daemon 空闲退出时间 | | `idle_timeout_secs` | int | 900 | PicoBot 会话清理及 agent-browser daemon 空闲退出时间 |
| `command_timeout_secs` | int | 120 | 单次 CLI 调用硬超时 | | `command_timeout_secs` | int | 120 | 单次 CLI 调用硬超时 |
| `max_output_chars` | int | 50000 | 页面来源文本输出上限 | | `max_output_chars` | int | 50000 | 页面来源文本输出上限 |
@ -145,6 +145,11 @@ MCP 服务器单条配置:
| `allowed_domains` | []string | [] | 可选域名白名单;空数组表示不启用域名限制 | | `allowed_domains` | []string | [] | 可选域名白名单;空数组表示不启用域名限制 |
| `allow_private_hosts` | bool | false | 是否允许回环、私网和本地域名 | | `allow_private_hosts` | bool | false | 是否允许回环、私网和本地域名 |
| `artifact_dir` | string | ~/.picobot/media/browser | 截图产物目录 | | `artifact_dir` | string | ~/.picobot/media/browser | 截图产物目录 |
| `persistence.profile_dir` | string | ~/.picobot/browser/profiles | 持久 ID、语义标签和 Profile 目录的受控根目录 |
持久化不是配置模式。`browser` 调用省略 `persistent_id` 时使用当前 dialog 的普通临时浏览器长期工作需要保留登录或站点状态时Agent 可自主调用 `browser_profiles(create,label=...)` 生成 `picobot-profile-<uuid>`,对应数据位于 `profile_dir/<id>`,然后在该工作的后续每个 action 中持续传入同一个 ID。没有默认 ID也不会按 dialog 自动选择持久身份。同一 ID 跨 dialog 共享底层 session 与串行锁,不同 ID 各自独立并可并发。`browser_profiles` 还支持 `set_label``list` 和使用精确 ID 的 `delete`;标签可重命名,但选择浏览器始终使用不可变 ID。
agent-browser 0.33.0 不允许持久 Profile 与 `allowed_domains` 同时使用。配置域名限制后普通临时浏览器仍可用创建或使用持久身份会被拒绝health 会提示该可选能力受限。Profile 包含登录凭据,应把目录视为敏感数据,不得提交到版本控制、跨用户共享或放在不受信任的网络文件系统中。
旧字段 `webdriver_url``chrome_path` 不再接受。推荐安装 `agent-browser@0.33.0` 后运行 `agent-browser install`Linux 可运行 `agent-browser install --with-deps`。使用前用 `picobot health` 检查 CLI 与 Chrome 环境。 旧字段 `webdriver_url``chrome_path` 不再接受。推荐安装 `agent-browser@0.33.0` 后运行 `agent-browser install`Linux 可运行 `agent-browser install --with-deps`。使用前用 `picobot health` 检查 CLI 与 Chrome 环境。

View File

@ -157,7 +157,7 @@ Cron 不是一个带 `action` 的统一工具,而是六个独立工具;仅
## browser — 浏览器自动化 ## browser — 浏览器自动化
默认注册;设置 `browser.enabled=false` 后不注册。PicoBot 上层包装统一 action 和结构化媒体,底层逐次调用 agent-browser `--json`;每个 PicoBot dialog 映射到一个不透明的 agent-browser session同 dialog 串行、不同 dialog 可并发。CLI daemon 自动常驻并通过 Chrome CDP 工作,不使用 Fantoccini、ChromeDriver 或 WebDriver。 默认注册;设置 `browser.enabled=false` 后不注册。PicoBot 上层包装统一 action 和结构化媒体,底层逐次调用 agent-browser `--json`。调用时不传 `persistent_id`,每个 dialog 使用各自的普通临时 session长期工作需要保留登录或站点状态时Agent 可自主用 `browser_profiles(create,label=...)` 创建身份,并在后续相关 action 中持续传入同一个 ID。PicoBot 没有全局持久化开关或默认持久 ID也不按 dialog 自动选择持久身份;同一 ID 跨 dialog 共享 session 和串行锁,不同 ID 使用独立 session/锁并可并发。CLI daemon 通过 Chrome CDP 工作,不使用 Fantoccini、ChromeDriver 或 WebDriver。
| action | 说明 | | action | 说明 |
|--------|------| |--------|------|
@ -166,12 +166,16 @@ Cron 不是一个带 `action` 的统一工具,而是六个独立工具;仅
| `click`, `click_at` | 点击元素或坐标 | | `click`, `click_at` | 点击元素或坐标 |
| `fill`, `type`, `press` | 输入文本或按键 | | `fill`, `type`, `press` | 输入文本或按键 |
| `get_text`, `get_title`, `get_url` | 读取页面信息 | | `get_text`, `get_title`, `get_url` | 读取页面信息 |
| `screenshot` | 保存到 `browser.artifact_dir` 并返回结构化图片媒体;支持 `full_page``annotate` | | `screenshot` | 保存到 `browser.artifact_dir`,交给模型并默认附到最终用户回复;支持 `full_page``annotate`,可用 `present_to_user=false` 仅供模型检查 |
| `focus`, `hover`, `scroll`, `wait` | 常见交互和等待 | | `focus`, `hover`, `scroll`, `wait` | 常见交互和等待 |
| `close` | 关闭浏览器会话 | | `close` | 关闭浏览器会话 |
典型流程:`open``snapshot` 获取 `@e` 引用 → 交互 → 页面变化后重新 `snapshot``path` 只接受 `.png` 文件名,不能逃逸产物目录。`open` 默认拒绝非 HTTP(S)、userinfo、回环、私网、本地域名及 DNS 解析到私网的地址;配置 `allowed_domains`agent-browser 同时限制导航、子资源、WebSocket、EventSource 与 WebRTC。页面输出是不可信内容默认开启 content boundary 元数据和 50,000 字符上限。 典型流程:`open``snapshot` 获取 `@e` 引用 → 交互 → 页面变化后重新 `snapshot``path` 只接受 `.png` 文件名,不能逃逸产物目录。`open` 默认拒绝非 HTTP(S)、userinfo、回环、私网、本地域名及 DNS 解析到私网的地址;配置 `allowed_domains`agent-browser 同时限制导航、子资源、WebSocket、EventSource 与 WebRTC。页面输出是不可信内容默认开启 content boundary 元数据和 50,000 字符上限。
## browser_profiles — 持久浏览器身份管理
`browser` 一同注册。`create` 生成新的持久 ID 和 Profile 目录,并可接受 180 字符的语义标签;`set_label` 按精确 ID 重命名标签;`list` 返回每个合法 Profile 的 ID、标签、目录和 active 状态;`delete` 必须传入 `create/list` 返回的精确 ID并删除该 ID 的完整 Chrome Profile、标签与登录态。删除活动 Profile 时会等待其操作完成并尝试关闭浏览器。标签只用于识别,不能代替 ID 选择;该工具不能接收任意目录。非空 `browser.allowed_domains` 下普通临时浏览器仍可用,但持久身份不能创建或使用。
依赖缺失时必须把错误和处置命令返回给用户不能声称已浏览也不能在工具内部静默安装CLI 不存在时安装 `agent-browser@0.33.0`Chrome 不存在时运行 `agent-browser install`Linux 共享库不完整时运行 `agent-browser install --with-deps`。用 `picobot health` 复查,再用 `agent-browser doctor` 获取详细上游诊断。用户明确不需要浏览器时才建议 `browser.enabled=false` 依赖缺失时必须把错误和处置命令返回给用户不能声称已浏览也不能在工具内部静默安装CLI 不存在时安装 `agent-browser@0.33.0`Chrome 不存在时运行 `agent-browser install`Linux 共享库不完整时运行 `agent-browser install --with-deps`。用 `picobot health` 复查,再用 `agent-browser doctor` 获取详细上游诊断。用户明确不需要浏览器时才建议 `browser.enabled=false`
## health — 依赖检查 ## health — 依赖检查

View File

@ -104,7 +104,10 @@
"content_boundaries": true, "content_boundaries": true,
"allowed_domains": [], "allowed_domains": [],
"allow_private_hosts": false, "allow_private_hosts": false,
"artifact_dir": "~/.picobot/media/browser" "artifact_dir": "~/.picobot/media/browser",
"persistence": {
"profile_dir": "~/.picobot/browser/profiles"
}
}, },
"workspace_dir": "~/.picobot/workspace" "workspace_dir": "~/.picobot/workspace"
} }