ArchiveBox v1 Personas API 详解:Persona 列表与浏览器身份同步接口(v1_personas 模块)
【免费下载链接】ArchiveBox🗃 Open source self-hosted web archiving. Takes URLs/browser history/bookmarks/Pocket/Pinboard/etc., saves HTML, JS, PDFs, media, and more...项目地址: https://gitcode.com/gh_mirrors/ar/ArchiveBox
本文基于 ArchiveBox 官方 API 文档 archivebox.api.v1_personas 及其对应源码 v1_personas.py 编写。读完你可以完整掌握 Personas 子 API 的两个端点(GET /api/v1/personas/personas与POST /api/v1/personas/sync)的请求/响应结构、四个 Schema 的全部字段与默认值、四个核心函数的底层行为(配置键映射、Persona 查找/创建规则、分页封装),并能直接复制可运行的调用示例。
1. 模块定位:v1 API 中的 Personas 路由
archivebox.api.v1_personas是 ArchiveBox REST API v1(ALPHA 阶段)中负责 Persona 管理的 Django Ninja 路由模块。Persona 是 ArchiveBox 中表示“浏览器身份/配置”的概念:每个 Persona 拥有独立的 Chrome 用户数据目录、cookies.txt与auth.json,从而让归档请求以特定登录态、特定浏览器特征(User-Agent、分辨率、时区等)执行。该模块的文档定位与接口面如下(与 API 文档 的 Module Contents 完全一致):
- Classes:
PersonaBrowserSettingsSchema、PersonaSyncSchema、PersonaSchema、PersonaSyncResponseSchema - Functions:
browser_settings_to_config、find_persona、get_personas、sync_persona - Data:
router(ninja.Router实例,标签为Personas)
路由挂载位置在 v1_api.py 的register_urls()中:
def register_urls(api: NinjaAPI) -> NinjaAPI: api.add_router("/auth/", "archivebox.api.v1_auth.router") api.add_router("/core/", "archivebox.api.v1_core.router") api.add_router("/crawls/", "archivebox.api.v1_crawls.router") api.add_router("/cli/", "archivebox.api.v1_cli.router") api.add_router("/machine/", "archivebox.api.v1_machine.router") api.add_router("/personas/", "archivebox.api.v1_personas.router") # 本模块挂载点 return api因此本模块的两个端点完整路径为GET /api/v1/personas/personas(列表)与POST /api/v1/personas/sync(同步/创建),与测试用例 test_api_v1_personas_personas.py 中的实际请求路径一致。
整个 API 的认证由 auth.py 的API_AUTH_METHODS统一提供,从源码看支持三种 Token 方式:
| 认证方式 | 实现类 | 传递形式 |
|---|---|---|
| 请求头 Token | HeaderTokenAuth | API 自定义请求头 |
| Bearer Token | BearerTokenAuth | Authorization: Bearer <token> |
| 查询参数 Token | QueryParamTokenAuth | ?api_key=<token> |
并且这三种方式在实现中均经过_require_superuser校验,即从源码结构看,Token 访问要求对应账户具备超级用户权限。
2. 端点一:GET /api/v1/personas/personas—— Persona 列表
源码定义(v1_personas.py):
@router.get("/personas", response=list[PersonaSchema], url_name="get_personas") @paginate(CustomPagination) def get_personas(request: HttpRequest): """List personas available on this ArchiveBox server.""" return Persona.objects.all().order_by("name")要点:
- 返回按
name升序排列的全部 Persona,序列化为PersonaSchema列表; - 通过
@paginate(CustomPagination)包装,CustomPagination定义在 v1_core.py,分页参数默认值为limit=200、offset=0、page=0,其中limit最大被截断为 500(limit = min(pagination.limit, 500)); - 响应体是统一的分页信封,字段固定为:
count、total_items、total_pages、page、limit、offset、num_items、items。
分页行为有测试佐证:test_api_v1_personas_personas.py 中先通过 sync 端点创建三个 Persona,再请求/api/v1/personas/personas?limit=1,断言响应是包含上述全部键的 dict,且count == total_items、limit == 1、num_items == 1、items长度为 1。
3. 数据模型(Schemas)逐字段说明
以下四张字段表完整覆盖 API 文档 列出的全部类与属性,类型与默认值取自源码 v1_personas.py。
3.1 PersonaBrowserSettingsSchema(浏览器特征覆盖)
描述扩展/客户端希望为 Persona 设置的浏览器外观与行为特征,全部可省略:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
user_agent | str | "" | 要使用的 User-Agent |
viewport_size | str | "" | 视口尺寸(如1920x1080) |
viewport_device_scale_factor | float \| None | None | 设备像素比 |
language | str | "" | 浏览器语言 |
timezone | str | "" | 时区 |
geolocation | dict[str, Any] \| None | None | 地理定位信息 |
3.2 PersonaSyncSchema(同步请求体)
POST /api/v1/personas/sync的入参结构:
| 字段 | 类型 | 必填/默认 | 说明 |
|---|---|---|---|
extension_persona_id | str | 必填 | 浏览器扩展侧的 Persona 标识,作为 upsert 的匹配键之一 |
name | str | 必填 | Persona 名称(须通过名称校验,见 §5.3) |
settings | PersonaBrowserSettingsSchema | Field(default_factory=...),默认空设置 | 浏览器特征覆盖,见 3.1 |
cookies_txt | str | "" | Netscape 格式 cookies 文本,非空时写入 Persona 目录 |
auth_json | dict[str, Any] | Field(default_factory=dict) | 完整认证状态(cookies/localStorage 等),非空时写入auth.json |
3.3 PersonaSchema(Persona 响应结构)
| 字段 | 类型 | 说明 |
|---|---|---|
TYPE | str | 固定值"personas.models.Persona",用于类型标识 |
id | uuid.UUID | Persona 主键(模型层使用 UUIDv7,见 models.py) |
name | str | Persona 名称,数据库层唯一约束 |
created_at | datetime | 创建时间 |
created_by_id | str | 创建者 ID,由静态方法resolve_created_by_id序列化为str(obj.created_by.pk) |
created_by_username | str | 由静态方法resolve_created_by_username解析为obj.created_by.username |
config | dict[str, Any] \| None | Persona 配置,返回前经过脱敏 |
resolve_config是安全关键点(v1_personas.py):
@staticmethod def resolve_config(obj): # Redact credential values so REST responses don't leak the raw # token/secret/api-key the operator stored in Persona.config. from archivebox.config.common import redact_sensitive_config return redact_sensitive_config(obj.config)即任何 GET 响应中,Persona.config里的 token/secret/api-key 类凭据值都会被redact_sensitive_config脱敏,防止 REST 响应泄露原始凭据。
3.4 PersonaSyncResponseSchema(同步响应结构)
| 字段 | 类型 | 说明 |
|---|---|---|
success | bool | 是否成功 |
created | bool | 本次调用是否新建了 Persona(False 表示命中已有记录并更新) |
persona | PersonaSchema | 落库后的 Persona 完整结构(config 已脱敏) |
cookies_file_written | bool | 是否写入了cookies.txt |
auth_file_written | bool | 是否写入了auth.json |
4. 核心函数逐一剖析
4.1browser_settings_to_config:设置项到配置键的映射
该函数(v1_personas.py)把扩展侧的浏览器设置翻译为 ArchiveBox 配置键,完整映射关系如下:
| 输入(settings 字段) | 生成的配置键 |
|---|---|
extension_persona_id(入参) | BROWSER_EXTENSION_PERSONA_ID(恒写入) |
| —(服务端时间) | BROWSER_EXTENSION_SYNCED_AT(恒写入,UTC ISO8601 +Z) |
user_agent(非空时) | USER_AGENT、CHROME_USER_AGENT、WGET_USER_AGENT、CURL_USER_AGENT四个键同值 |
viewport_size(非空时) | RESOLUTION、CHROME_RESOLUTION两键同值 |
viewport_device_scale_factor(非 None 时) | BROWSER_DEVICE_SCALE_FACTOR |
language(非空时) | BROWSER_LANGUAGE |
timezone(非空时) | BROWSER_TIMEZONE |
geolocation(非 None 时) | BROWSER_GEOLOCATION(原样保留 dict) |
注意 User-Agent 被同时写入四个键,说明这些覆盖对 Chrome、wget、curl 等不同抓取器都生效;而RESOLUTION的键名也表明视口设置同时面向通用配置与 Chrome 专用配置。
4.2find_persona:upsert 的匹配规则
def find_persona(extension_persona_id: str, name: str) -> Persona | None: return ( Persona.objects.filter( Q(config__BROWSER_EXTENSION_PERSONA_ID=extension_persona_id) | Q(name=name), ) .order_by("created_at") .first() )匹配规则为“或”关系:configJSON 中的BROWSER_EXTENSION_PERSONA_ID等于请求的扩展标识,或name等于请求名称,二者命中其一即视为同一 Persona;多个命中时取created_at最早的一条。这保证了同一浏览器扩展反复同步不会无限创建新记录,而是幂等更新既有 Persona。
4.3get_personas:见 §2,列表 +CustomPagination分页。
4.4sync_persona:完整的同步流程
sync_persona(v1_personas.py)是模块中最复杂的函数,其 docstring 明确了职责边界:扩展发送浏览器设置与可移植认证产物,服务端把浏览器覆盖设置保存在Persona.config,并把cookies.txt/auth.json写入 Persona 目录供抓取器消费。执行流程拆解如下:
- 名称校验:
name = payload.name.strip()后调用 validate_persona_name,不合法则直接raise ValueError(error_message)。该校验函数与 CLI/Admin 侧共用,防路径穿越规则为:空名、含/或\、含..、以.开头、含 NUL/换行/回车,均会被拒绝; - 查找或创建:调用
find_persona(payload.extension_persona_id, name);未命中则Persona(name=name)新建,且若request.user已认证则绑定persona.created_by = request.user; - 配置合并:
persona.config = {**(persona.config or {}), **browser_settings_to_config(...)},即保留既有 config 键、仅覆盖扩展同步写入的键,然后persona.save()并persona.ensure_dirs(); - 写认证产物:
cookies_txt去除首尾空白后非空才写入persona.path / "cookies.txt";auth_json非空才以json.dumps(..., indent=2, sort_keys=True) + "\n"写入persona.path / "auth.json"; - 返回:
{success, created, persona, cookies_file_written, auth_file_written}。
其中ensure_dirs()与 Persona 目录布局见 §6。
5. 调用示例(可复制)
5.1 创建/更新 Persona(sync)
最小请求体(与 test_api_v1_personas_sync.py 一致):
curl -X POST 'http://localhost:8000/api/v1/personas/sync' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <API_TOKEN>' \ -d '{ "extension_persona_id": "extension-api-persona-basic", "name": "api-persona-basic", "settings": {}, "cookies_txt": "", "auth_json": {} }'带完整浏览器设置与认证产物的请求体:
{ "extension_persona_id": "ext-0001", "name": "work-profile", "settings": { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "viewport_size": "1920x1080", "viewport_device_scale_factor": 1.0, "language": "en-US", "timezone": "America/New_York", "geolocation": {"latitude": 40.7128, "longitude": -74.006} }, "cookies_txt": "# Netscape HTTP Cookie File\n...", "auth_json": {"cookies": [], "localStorage": {}} }成功时(测试断言为200)返回PersonaSyncResponseSchema结构,首次创建时created为true;同一extension_persona_id或name再次调用时created为false,仅更新。
5.2 列出 Persona
curl 'http://localhost:8000/api/v1/personas/personas?limit=50&offset=0' \ -H 'Authorization: Bearer <API_TOKEN>'返回{"count": N, "total_items": N, "total_pages": P, "page": 1, "limit": 50, "offset": 0, "num_items": K, "items": [PersonaSchema, ...]},items按name排序,且config已脱敏。
5.3 错误行为
- 名称非法:
sync_persona抛出ValueError(如"Persona name cannot contain path separators (/ or \\)")。由于它不属于ObjectDoesNotExist/EmptyResultSet/PermissionDenied,会落入 v1_api.py 的通用异常处理器,返回503,响应体含succeeded: false、message(异常类名 + 信息)与errors(完整 traceback 文本); - 认证失败/权限不足:由 API 层认证逻辑处理;
- 所有 API 响应还带有
Cache-Control: no-store及X-ArchiveBox-Stdout/X-ArchiveBox-Stderr/X-ArchiveBox-Auth-*调试头(见 NinjaAPIWithIOCapture),便于排查后端输出。
6. 落盘结构与 Persona 模型的关系
sync 端点写入的目录由 Persona 模型决定。每个 Persona 对应PERSONAS_DIR/<name>/一个目录,ensure_dirs()(models.py)保证其中存在chrome_profile/与chrome_downloads/两个子目录;Persona.path、CHROME_USER_DATA_DIR、COOKIES_FILE、AUTH_STORAGE_FILE等属性均派生自该目录:
PERSONAS_DIR/ └── <persona_name>/ ├── chrome_profile/ # Chrome 用户数据目录(模板) ├── chrome_downloads/ # 下载目录 ├── cookies.txt # sync 端点写入,供 wget/curl 使用 └── auth.json # sync 端点写入,完整认证状态两个值得注意的实现细节:
- 默认权限注入:
Persona.save()(models.py)在config中PERMISSIONS缺失或不合法时,会从全局配置注入normalize_permissions(get_config(include_machine=True).PERMISSIONS),因此 sync 端点创建的 Persona 不会缺省权限策略; - 运行时隔离:Persona 目录是“模板”,实际爬取时
prepare_runtime_for_crawl(models.py)会把模板 profile 复制到该 Crawl 输出目录下的.persona/<name>/,并清理 Chrome 易变状态(Cache、Sessions、SingletonLock等,见VOLATILE_PROFILE_DIR_NAMES),从而避免多个 Crawl 共享同一 Chrome 用户数据目录。sync 端点写入的cookies.txt/auth.json同样会被复制到运行时根目录并映射为COOKIES_FILE/AUTH_STORAGE_FILE配置键。
7. 小结
archivebox.api.v1_personas用两个端点 + 四个 Schema 构成了 Persona 的完整 REST 面:GET /api/v1/personas/personas提供带分页信封、config 脱敏的 Persona 列表;POST /api/v1/personas/sync提供以extension_persona_id或name幂等匹配的 upsert 语义,把浏览器特征翻译成USER_AGENT/RESOLUTION/BROWSER_*等配置键并持久化,同时把cookies.txt/auth.json落盘到 Persona 目录供抓取器消费。围绕它的名称校验(importers.py)、认证(auth.py)、分页(v1_core.py)与 Persona 目录模型(models.py)共同保证了该接口在多用户、多 Crawl 场景下的安全性与隔离性。
【免费下载链接】ArchiveBox🗃 Open source self-hosted web archiving. Takes URLs/browser history/bookmarks/Pocket/Pinboard/etc., saves HTML, JS, PDFs, media, and more...项目地址: https://gitcode.com/gh_mirrors/ar/ArchiveBox
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考