feat: v0.3.8–v0.3.14 工作区汇总(依赖升级、整理来源、手动入库、原地入库等)

生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。

- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
  PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
  (metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)

同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
  由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距

依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 15:47:17 +08:00
1 parent 8013196e3f
commit ceb66f8c77
265 files changed
+17751 -2483

No files matched your search

+13 -11
View File
@@ -1,13 +1,13 @@
# 浏览器桌面镜像(原站视图桌面模式,docs/design/webtop-remote-browser.md、docs/runbook/browser.md §8):
# linuxserver baseimage-selkies(Webtop 4.0 的底座:pixelflux + labwc Wayland 桌面 + Selkies Web 客户端)
# + camofox-browser 1.17.0 以有头模式跑在同一个 Wayland 会话里 → 桌面上看到的就是 Agent 驱动的那个 Firefox。
# + camofox-browser 1.18.0 以有头模式跑在同一个 Wayland 会话里 → 桌面上看到的就是 Agent 驱动的那个 Firefox。
#
# camofox 部分不重新下载:直接从旧镜像 camofox-browser:1.17.0(docker/camofox/Dockerfile 构建)复制
# node、/app 与 Camoufox 浏览器包,两个镜像的 camofox 完全一致,可随时用 CAMOFOX_IMAGE 切回旧镜像。
# camofox 部分不重新下载:直接从基础镜像 camofox-browser:1.18.0(docker/camofox/Dockerfile 构建)复制
# node、/app 与 Camoufox 浏览器包,两个镜像的 camofox 完全一致,可随时用 CAMOFOX_IMAGE 切回基础镜像。
# 手工构建:
# docker build -t camofox-browser:1.17.0 --build-context upstream=… docker/camofox # 先有旧镜像
# docker build -t umt-browser-desktop:1.17.0 docker/browser-desktop
ARG CAMOFOX_BASE=camofox-browser:1.17.0
# docker build -t camofox-browser:1.18.0 --build-context upstream=… docker/camofox # 先有基础镜像
# docker build -t umt-browser-desktop:1.18.0 docker/browser-desktop
ARG CAMOFOX_BASE=camofox-browser:1.18.0
# baseimage-selkies debiantrixie(Selkies 2.x、labwc 0.8.3,已含 fonts-noto-cjk);按 digest 锁定,升级需重新核验
ARG SELKIES_BASE=ghcr.io/linuxserver/baseimage-selkies:debiantrixie@sha256:f3ca0b10e923a603941a996ea1c3356b9527cc53163259f4e836f61fd4523e12
@@ -16,17 +16,19 @@ FROM ${CAMOFOX_BASE} AS camofox
FROM ${SELKIES_BASE}
ARG APT_MIRROR=
ARG APT_HTTPS_PROXY=
# 1 = 装容器内输入法 fcitx5(拼音 + mozc,缺省装;是否启动由运行时 UMT_DESKTOP_IME=fcitx5 决定)
ARG WITH_FCITX5=1
RUN if [ -n "$APT_MIRROR" ]; then for f in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do [ -f "$f" ] && sed -i "[email protected]@${APT_MIRROR}@g" "$f"; done; fi \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& apt-get -o "Acquire::https::Proxy=${APT_HTTPS_PROXY:-DIRECT}" update \
&& apt-get -o "Acquire::https::Proxy=${APT_HTTPS_PROXY:-DIRECT}" upgrade -y --no-install-recommends \
&& apt-get -o "Acquire::https::Proxy=${APT_HTTPS_PROXY:-DIRECT}" install -y --no-install-recommends \
libdbus-glib-1-2 libxt6t64 libasound2t64 libx11-xcb1 libxcomposite1 libxcursor1 libxdamage1 libxfixes3 \
libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libegl1 libgl1-mesa-dri libgbm1 \
fonts-noto-cjk fonts-noto-cjk-extra fonts-noto-color-emoji fontconfig \
&& if [ "$WITH_FCITX5" = "1" ]; then \
apt-get install -y --no-install-recommends \
apt-get -o "Acquire::https::Proxy=${APT_HTTPS_PROXY:-DIRECT}" install -y --no-install-recommends \
fcitx5 fcitx5-chinese-addons fcitx5-mozc fcitx5-frontend-gtk3 fcitx5-config-qt- ; \
fi \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*
@@ -40,8 +42,8 @@ COPY root/ /
RUN node -e 'const f="/opt/camofox/camofox.config.json";const c=JSON.parse(require("fs").readFileSync(f,"utf8"));c.plugins={...(c.plugins||{}),"umt-cookies":{enabled:true}};require("fs").writeFileSync(f,JSON.stringify(c,null,2))'
LABEL org.opencontainers.image.title="umt-browser-desktop" \
org.opencontainers.image.description="camofox-browser 1.17.0 (headed, Wayland) on linuxserver baseimage-selkies" \
org.opencontainers.image.version="1.17.0"
org.opencontainers.image.description="camofox-browser 1.18.0 (headed, Wayland) on linuxserver baseimage-selkies" \
org.opencontainers.image.version="1.18.0"
ENV CAMOUFOX_INSTALL_DIR=/opt/camoufox \
NODE_ENV=production \
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<labwc_config>
<core>
<decoration>server</decoration>
<gap>0</gap>
</core>
<keyboard>
<default />
<keybind key="A-F4"><action name="None" /></keybind>
<keybind key="A-Escape"><action name="None" /></keybind>
<keybind key="A-space"><action name="None" /></keybind>
<keybind key="W-e"><action name="None" /></keybind>
</keyboard>
<mouse>
<default />
<context name="Root">
<mousebind button="Right" action="Press"><action name="None" /></mousebind>
<mousebind button="Middle" action="Press"><action name="None" /></mousebind>
</context>
</mouse>
<windowRules>
<windowRule identifier="*" serverDecoration="no">
<action name="Maximize" />
</windowRule>
</windowRules>
</labwc_config>
@@ -2,7 +2,8 @@
# UMT 浏览器桌面:在 linuxserver 的 init-selkies-config 之后、Selkies/桌面启动之前执行(root)。
# 1) Selkies 双口令:由 CAMOFOX_ACCESS_KEY 经 HMAC 派生(app 用同一算法),不另设密钥;
# 2) labwc autostart 每次覆盖(只起可选 fcitx5,不起终端等其它应用);
# 3) 登录状态目录(与旧 camofox 镜像同一个卷 /root/.camofox)交给运行用户 abc。
# 3) 写入完整 labwc 配置,保证浏览器最大化;底座按行注释快捷键会破坏 XML。
# 4) 登录状态目录(与旧 camofox 镜像同一个卷 /root/.camofox)交给运行用户 abc。
set -e
ENVDIR=/run/s6/container_environment
derive() { printf '%s' "$1" | openssl dgst -sha256 -hmac "$CAMOFOX_ACCESS_KEY" -r | cut -c1-48; }
@@ -20,6 +21,9 @@ rm -f "$ENVDIR/PASSWORD" "$ENVDIR/PASSWD"
LABWC_DIR="$HOME/.config/labwc"
mkdir -p "$LABWC_DIR"
cp /defaults/umt-labwc.xml "$LABWC_DIR/rc.xml"
chown root:abc "$LABWC_DIR/rc.xml"
chmod 444 "$LABWC_DIR/rc.xml"
cp /defaults/autostart_wayland "$LABWC_DIR/autostart"
chown root:abc "$LABWC_DIR/autostart"
chmod 550 "$LABWC_DIR/autostart"
+39 -19
View File
@@ -1,21 +1,18 @@
# camofox-browser 镜像(docker-compose.yml 的 camofox 服务)。
# 上游 jo-inc/camofox-browser 不发布镜像;源码由命名上下文 upstream 提供(compose 里锁定 git tag v1.17.0,
# commit 389c996ae3c7d42e539295a336ee6f975847f066)。本文件照抄上游 Dockerfile 的 camofox-browser 阶段,只改三处:
# 1. 上游 v1.17.0 的 Dockerfile 在 `npm ci` 之前没有复制 postinstall.js,构建必然失败(MODULE_NOT_FOUND);
# 这里先复制 postinstall.js 与 lib/(Camoufox 已预置,postinstall 见到 version.json 直接跳过下载);
# 2. 可选 APT_MIRROR(国内构建换源,如 mirrors.ustc.edu.cn);
# 3. CMD 用 exec 启动 node(见文件末尾)。
# 手工构建:docker build -t camofox-browser:1.17.0 \
# --build-context upstream=https://github.com/jo-inc/camofox-browser.git#v1.17.0 docker/camofox
FROM node:22-trixie-slim AS camofox-browser
# camofox-browser v1.18.0; upstream commit f94c81babf4bfd44cdd4bd46e1f2e1fb1d91c753.
# Build with: docker build --build-context upstream=https://github.com/jo-inc/camofox-browser.git#f94c81babf4bfd44cdd4bd46e1f2e1fb1d91c753 -t camofox-browser:1.18.0 docker/camofox
# Runtime manifest/lock below are maintained locally; never run audit fix while building.
ARG CAMOUFOX_ASSET_STAGE=downloaded-browser
FROM node:24.21.0-trixie-slim@sha256:8ec5d7557396cfe32d21c3f9c13072355ceab22b584578ca4bb28af31120cffe AS browser-base
ARG CAMOUFOX_VERSION=152.0.4
ARG CAMOUFOX_RELEASE=beta.28
ARG ARCH=x86_64
ARG CAMOUFOX_RELEASE=beta.30
ARG TARGETARCH
ARG APT_MIRROR=
RUN if [ -n "$APT_MIRROR" ]; then sed -i "[email protected]@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources; fi \
&& apt-get update && apt-get install -y \
&& apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y \
libgtk-3-0 libdbus-glib-1-2 libxt6 libasound2 libx11-xcb1 libxcomposite1 libxcursor1 libxdamage1 libxfixes3 \
libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
libegl1 libgl1-mesa-dri libgbm1 \
@@ -23,8 +20,27 @@ RUN if [ -n "$APT_MIRROR" ]; then sed -i "[email protected]@${APT_MIRROR}@g" /etc
ca-certificates curl unzip python3-minimal \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /root/.cache/camoufox \
&& curl -fL -o /tmp/camoufox.zip "https://github.com/daijro/camoufox/releases/download/v${CAMOUFOX_VERSION}-${CAMOUFOX_RELEASE}/camoufox-${CAMOUFOX_VERSION}-${CAMOUFOX_RELEASE}-lin.${ARCH}.zip" \
# Optional local asset context accelerates builds without changing integrity checks.
FROM browser-base AS downloaded-browser
RUN case "${TARGETARCH}" in \
amd64) ARCH=x86_64; CAMOUFOX_SHA256=5720d45b894ce1770543de024c6f10d514b38be560fa2dc3226b3d8586caf672 ;; \
arm64) ARCH=arm64; CAMOUFOX_SHA256=60447260af8bebdb0ec3f2aa72f687b879e5598367303de2e3fdbc7a5be8c124 ;; \
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& curl -fL -o /tmp/camoufox.zip "https://github.com/daijro/camoufox/releases/download/v${CAMOUFOX_VERSION}-${CAMOUFOX_RELEASE}/camoufox-${CAMOUFOX_VERSION}-${CAMOUFOX_RELEASE}-lin.${ARCH}.zip"
FROM browser-base AS provided-browser
COPY --from=browser-asset /camoufox.zip /tmp/camoufox.zip
FROM ${CAMOUFOX_ASSET_STAGE} AS camofox-browser
ARG TARGETARCH
RUN case "${TARGETARCH}" in \
amd64) ARCH=x86_64; CAMOUFOX_SHA256=5720d45b894ce1770543de024c6f10d514b38be560fa2dc3226b3d8586caf672 ;; \
arm64) ARCH=arm64; CAMOUFOX_SHA256=60447260af8bebdb0ec3f2aa72f687b879e5598367303de2e3fdbc7a5be8c124 ;; \
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& mkdir -p /root/.cache/camoufox \
&& echo "${CAMOUFOX_SHA256} /tmp/camoufox.zip" | sha256sum -c - \
&& (unzip -q /tmp/camoufox.zip -d /root/.cache/camoufox || true) \
&& rm /tmp/camoufox.zip \
&& chmod -R 755 /root/.cache/camoufox \
@@ -33,7 +49,8 @@ RUN mkdir -p /root/.cache/camoufox \
WORKDIR /app
COPY --from=upstream package.json package-lock.json postinstall.js ./
COPY package.json package-lock.json ./
COPY --from=upstream postinstall.js ./
COPY --from=upstream scripts/ ./scripts/
COPY --from=upstream lib/ ./lib/
RUN apt-get update \
@@ -43,14 +60,17 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
COPY --from=upstream server.js camofox.config.json ./
COPY --from=upstream mcp/ ./mcp/
COPY --from=upstream mcp/server.mjs ./mcp/server.mjs
COPY --from=upstream mcp/lib/ ./mcp/lib/
# MCP uses the same installed parent dependency tree; do not ship the unused upstream lock.
COPY mcp-package.json ./mcp/package.json
COPY --from=upstream plugins/ ./plugins/
RUN sh scripts/install-plugin-deps.sh
LABEL org.opencontainers.image.source="https://github.com/jo-inc/camofox-browser" \
org.opencontainers.image.version="v1.17.0" \
org.opencontainers.image.revision="389c996ae3c7d42e539295a336ee6f975847f066"
org.opencontainers.image.version="v1.18.0" \
org.opencontainers.image.revision="f94c81babf4bfd44cdd4bd46e1f2e1fb1d91c753"
ENV NODE_ENV=production
ENV CAMOFOX_PORT=9377
+34
View File
@@ -0,0 +1,34 @@
# 浏览器独立构建
服务源码固定为 camofox-browser v1.18.0 的提交 `f94c81babf4bfd44cdd4bd46e1f2e1fb1d91c753`。标签对象 `60481dbd…` 不是源码提交。`build.sh` 核对 checkout 的提交与工作区后构建;Dockerfile 使用该源码命名上下文,并使用本目录的生产 manifest 和 lock。
```sh
APT_MIRROR=mirrors.ustc.edu.cn sh docker/camofox/build.sh
docker build --network=host --build-arg APT_MIRROR=mirrors.ustc.edu.cn \
-t umt-browser-desktop:1.18.0 docker/browser-desktop
```
桌面底座含 Docker HTTPS apt 源;需要时可另传 `--build-arg APT_HTTPS_PROXY=http://127.0.0.1:7890` 使用现有代理。该参数只作用于构建中的 apt HTTPS 请求,默认直连,不写入容器 apt 配置;Debian HTTP 镜像仍直连。
固定组合:Node 24.21.0(基础镜像固定摘要)、Camoufox 152.0.4-beta.30(amd64/arm64 官方资产 SHA-256)、camoufox-js 0.12.0、playwright-core 1.60.0、better-sqlite3 13.0.3、MCP SDK 1.31.0。桌面底座继续使用已核验的 Selkies 摘要;Cookie 插件、身份、代理、运行用户、umask 和 exec 信号转发保留。
下载较慢时,可先把对应架构的官方资产存成某目录中的 `camoufox.zip`,设置 `CAMOUFOX_ASSET_DIR=/path/to/asset` 再运行 `build.sh`。可选命名上下文直接提供 ZIP,但仍通过完全相同的 SHA-256 校验、解压与二进制存在检查;默认仍在构建时从官方 URL 下载。
Playwright 1.60.0 是满足 camoufox-js peer 的最新稳定版。官方 [修复 #312](https://github.com/apify/camoufox-js/commit/3fe80d8448) 说明:1.61 起 `Browser.setDefaultViewport` 的 `viewport.isMobile` 不被 Camoufox 的 Juggler scheme 接受,导致 newContext/newPage 失败。因此没有 override peer,也没有升级到 1.63 后隐藏冲突。
本地 manifest 从上游生产入口派生,去掉开发测试、OpenClaw 可选 peer 和发布脚本;保留服务、postinstall 和生产可选 MCP。安全 overrides 固定 `adm-zip` 0.6.1、`brace-expansion` 5.0.12、`fast-uri` 3.1.8、`hono` 4.13.12、`ip-address` 10.7.2;fast-uri 保留 AJV 所需的 3.x API。其它生产依赖由独立 lock 固定。这里是显式依赖补丁,没有在构建时运行 `npm audit fix`。
MCP 和内置插件从服务根目录的同一依赖树解析模块。镜像中的 MCP manifest 同步为 SDK 1.31.0,不复制上游未安装的独立旧 lock。`audit-2026-10-01.json` 记录此生产 lock 的 npm 审计:174 个依赖、0 条漏洞;该结果不代表操作系统包审计。两个镜像均在安装依赖前更新容器系统包,宿主机软件不变。
需要更新依赖时,先编辑明确版本,再用固定 Node 镜像生成 lock 并审计,审查 lock 差异后构建:
```sh
docker run --rm --network=host -v "$PWD/docker/camofox:/work" -w /work \
node:24.21.0-trixie-slim@sha256:8ec5d7557396cfe32d21c3f9c13072355ceab22b584578ca4bb28af31120cffe \
npm install --package-lock-only --ignore-scripts --omit=dev
docker run --rm --network=host -v "$PWD/docker/camofox:/work:ro" -w /work \
node:24.21.0-trixie-slim@sha256:8ec5d7557396cfe32d21c3f9c13072355ceab22b584578ca4bb28af31120cffe \
npm audit --package-lock-only --omit=dev --json
```
隔离验收使用独立容器、端口和 profile 卷,不挂生产 `umt-camofox`。`verify-upgrade.mjs` 接受测试地址和访问 key,支持重启前后两次运行以核验 Cookie 落盘和固定身份。此验收覆盖本地行为;真实 PT 挑战、远程桌面客户端兼容和 ARM64 执行仍需各自验收。
+22
View File
@@ -0,0 +1,22 @@
{
"auditReportVersion": 2,
"vulnerabilities": {},
"metadata": {
"vulnerabilities": {
"info": 0,
"low": 0,
"moderate": 0,
"high": 0,
"critical": 0,
"total": 0
},
"dependencies": {
"prod": 145,
"dev": 0,
"optional": 29,
"peer": 1,
"peerOptional": 0,
"total": 174
}
}
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
# Rebuild from a verified immutable upstream checkout and the local runtime lock.
set -eu
UPSTREAM_REVISION=f94c81babf4bfd44cdd4bd46e1f2e1fb1d91c753
BUILD_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
UPSTREAM_DIR=${1:-/tmp/umt-camofox-1.18.0-source}
if [ ! -d "$UPSTREAM_DIR/.git" ]; then
git clone --depth 1 --branch v1.18.0 https://github.com/jo-inc/camofox-browser.git "$UPSTREAM_DIR"
fi
test "$(git -C "$UPSTREAM_DIR" rev-parse HEAD)" = "$UPSTREAM_REVISION"
test -z "$(git -C "$UPSTREAM_DIR" status --porcelain)"
set -- docker build --network=host \
--build-arg "APT_MIRROR=${APT_MIRROR:-}" \
--build-context "upstream=$UPSTREAM_DIR" \
-t "${CAMOFOX_BUILD_TAG:-camofox-browser:1.18.0}"
if [ -n "${CAMOUFOX_ASSET_DIR:-}" ]; then
test -f "$CAMOUFOX_ASSET_DIR/camoufox.zip"
set -- "$@" --build-arg CAMOUFOX_ASSET_STAGE=provided-browser \
--build-context "browser-asset=$CAMOUFOX_ASSET_DIR"
fi
exec "$@" "$BUILD_DIR"
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@askjo/camofox-browser-mcp",
"version": "1.18.0",
"description": "Standalone stdio MCP server exposing the camofox-browser REST API as MCP tools (Claude Code, Codex, agy, Cursor, opencode). Proxies to an already-running camofox-browser REST server \u2014 does not itself require the browser binary, Playwright, or the core server's dependencies. Initial MCP implementation contributed by @epicsagas.",
"type": "module",
"main": "server.mjs",
"license": "MIT",
"author": "Jo Inc <[email protected]>",
"homepage": "https://github.com/jo-inc/camofox-browser/tree/master/mcp#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/jo-inc/camofox-browser.git",
"directory": "mcp"
},
"bugs": {
"url": "https://github.com/jo-inc/camofox-browser/issues"
},
"keywords": [
"mcp",
"model-context-protocol",
"camofox",
"camoufox",
"browser-automation",
"claude-code",
"codex",
"cursor",
"opencode"
],
"engines": {
"node": ">=22"
},
"bin": {
"camofox-browser-mcp": "server.mjs"
},
"files": ["server.mjs", "lib/", "README.md"],
"dependencies": {
"@modelcontextprotocol/sdk": "1.31.0"
},
"overrides": {
"fast-uri": "3.1.8",
"hono": "4.13.12"
}
}
+2275
View File
File diff suppressed because it is too large. Load diff
+148
View File
@@ -0,0 +1,148 @@
{
"name": "@askjo/camofox-browser",
"version": "1.18.0",
"description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
"type": "module",
"main": "server.js",
"license": "MIT",
"author": "Jo Inc <[email protected]>",
"homepage": "https://github.com/jo-inc/camofox-browser#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/jo-inc/camofox-browser.git"
},
"bugs": {
"url": "https://github.com/jo-inc/camofox-browser/issues"
},
"keywords": [
"browser",
"automation",
"headless",
"scraping",
"camofox",
"camoufox",
"anti-detection",
"ai-agent",
"openclaw",
"clawdbot",
"moltbot",
"playwright",
"firefox",
"youtube",
"transcript"
],
"engines": {
"node": ">=22"
},
"files": [
"server.js",
"plugin.js",
"openclaw.plugin.json",
"camofox.config.json",
"bin/camofox-browser.js",
"lib/",
"mcp/server.mjs",
"mcp/lib/config.mjs",
"mcp/lib/cookies.mjs",
"mcp/lib/tool-contracts.mjs",
"plugins/persistence/index.js",
"plugins/vnc/index.js",
"plugins/vnc/plugin.json",
"plugins/vnc/spawn.js",
"plugins/vnc/vnc-launcher.js",
"plugins/vnc/vnc-watcher.sh",
"plugins/vnc/vnc-watcher-lib.sh",
"plugins/youtube/index.js",
"plugins/youtube/youtube.js",
"postinstall.js"
],
"openclaw": {
"extensions": [
"plugin.js"
],
"runtimeExtensions": [
"plugin.js"
],
"compat": {
"pluginApi": ">=2026.9.4"
},
"build": {
"openclawVersion": "2026.9.4"
},
"tools": [
{
"name": "camofox_create_tab",
"description": "Open a new browser tab at a URL"
},
{
"name": "camofox_snapshot",
"description": "Get accessibility snapshot with element refs"
},
{
"name": "camofox_click",
"description": "Click an element by ref or CSS selector"
},
{
"name": "camofox_type",
"description": "Type text into an element"
},
{
"name": "camofox_navigate",
"description": "Navigate to URL or search macro"
},
{
"name": "camofox_scroll",
"description": "Scroll page up/down/left/right"
},
{
"name": "camofox_screenshot",
"description": "Capture page screenshot"
},
{
"name": "camofox_close_tab",
"description": "Close a browser tab"
},
{
"name": "camofox_evaluate",
"description": "Execute JavaScript in a Camoufox tab page context"
},
{
"name": "camofox_list_tabs",
"description": "List open tabs for a user"
},
{
"name": "camofox_import_cookies",
"description": "Import Netscape cookie file (requires CAMOFOX_API_KEY)"
}
]
},
"scripts": {
"start": "node server.js",
"postinstall": "node postinstall.js"
},
"dependencies": {
"better-sqlite3": "13.0.3",
"camoufox-js": "0.12.0",
"express": "5.2.1",
"playwright-core": "1.60.0",
"prom-client": "15.1.3",
"swagger-jsdoc": "6.3.0"
},
"optionalDependencies": {
"@modelcontextprotocol/sdk": "1.31.0"
},
"overrides": {
"swagger-jsdoc": {
"glob": "13.0.6"
},
"adm-zip": "0.6.1",
"brace-expansion": "5.0.12",
"fast-uri": "3.1.8",
"hono": "4.13.12",
"ip-address": "10.7.2"
},
"bin": {
"camofox-browser": "./bin/camofox-browser.js",
"camofox-browser-mcp": "./mcp/server.mjs"
}
}
+131
View File
@@ -0,0 +1,131 @@
import assert from "node:assert/strict";
import { readFile, writeFile } from "node:fs/promises";
// Use only an isolated candidate instance: this creates a synthetic browser profile.
const base = process.env.BROWSER_VERIFY_URL ?? "http://127.0.0.1:19377";
const key = process.env.BROWSER_VERIFY_KEY ?? "umt-isolated-upgrade-key";
const statePath = process.env.BROWSER_VERIFY_STATE ?? "/tmp/umt-browser-upgrade-state.json";
const mode = process.argv[2] ?? "before";
const userId = "umt-upgrade-isolated";
async function call(path, body, auth = true, method = body === undefined ? "GET" : "POST") {
const response = await fetch(base + path, {
method,
headers: {
...(auth ? { authorization: `Bearer ${key}` } : {}),
...(body === undefined ? {} : { "content-type": "application/json" }),
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
signal: AbortSignal.timeout(60_000),
});
const data = await response.json();
return { status: response.status, data };
}
assert.equal((await call("/health", undefined, false)).status, 200);
assert.equal((await call("/umt/instance", undefined, false)).status, 401);
assert.equal((await call("/tabs", { userId, sessionKey: "upgrade" }, false)).status, 401);
const instance = await call("/umt/instance");
assert.equal(instance.status, 200);
const identity = await call("/umt/identity");
assert.equal(identity.status, 200);
const cookiesPath = `/umt/sessions/${userId}/cookies`;
if (mode === "after") {
const before = JSON.parse(await readFile(statePath, "utf8"));
assert.notEqual(instance.data.instanceId, before.instanceId);
assert.deepEqual(identity.data, before.identity);
const persisted = await call(`${cookiesPath}?urls=https%3A%2F%2Fexample.com%2F`);
assert.equal(persisted.status, 200);
assert.equal(persisted.data.source, "persisted");
assert.ok(persisted.data.cookies.some((c) => c.name === "umt_upgrade" && c.value === "candidate-only"));
} else {
const imported = await call(cookiesPath, {
cookies: [
{
name: "umt_upgrade",
value: "candidate-only",
domain: "example.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "Lax",
expires: Math.floor(Date.now() / 1000) + 3600,
},
],
});
assert.equal(imported.status, 200);
assert.equal(imported.data.count, 1);
await writeFile(statePath, JSON.stringify({ instanceId: instance.data.instanceId, identity: identity.data }), {
mode: 0o600,
});
}
const tab = await call("/tabs", { userId, sessionKey: "upgrade" });
assert.equal(tab.status, 200, JSON.stringify(tab.data));
assert.ok(tab.data.tabId);
const tabPath = `/tabs/${tab.data.tabId}`;
const evaluated = await call(`${tabPath}/evaluate`, {
userId,
expression: `(() => { document.body.innerHTML = '<button id="probe">升级验证</button>'; window.probeClicked = false; document.querySelector('#probe').addEventListener('click', () => { window.probeClicked = true; }); return { title: document.title, userAgent: navigator.userAgent }; })()`,
});
assert.equal(evaluated.status, 200, JSON.stringify(evaluated.data));
assert.match(evaluated.data.result.userAgent, /Firefox\/152/);
const snapshot = await call(`${tabPath}/snapshot?userId=${userId}`);
assert.equal(snapshot.status, 200, JSON.stringify(snapshot.data));
assert.match(JSON.stringify(snapshot.data), /升级验证/);
const screenshot = await fetch(`${base}${tabPath}/screenshot?userId=${userId}`, {
headers: { authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(60_000),
});
assert.equal(screenshot.status, 200);
assert.match(screenshot.headers.get("content-type"), /image\/png/);
assert.deepEqual(
new Uint8Array(await screenshot.arrayBuffer()).slice(0, 8),
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
);
const clicked = await call(`${tabPath}/click`, { userId, selector: "#probe" });
assert.equal(clicked.status, 200, JSON.stringify(clicked.data));
const outcome = await call(`${tabPath}/evaluate`, { userId, expression: "window.probeClicked" });
assert.equal(outcome.data.result, true);
const mouseProbe = await call(`${tabPath}/evaluate`, {
userId,
expression:
"(() => { window.probeTrusted = false; const b = document.querySelector('#probe'); b.addEventListener('click', e => { window.probeTrusted = e.isTrusted; }); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; })()",
});
assert.equal(mouseProbe.status, 200);
const mouse = await call(`/umt/tabs/${tab.data.tabId}/mouse-click`, { userId, ...mouseProbe.data.result });
assert.equal(mouse.status, 200, JSON.stringify(mouse.data));
const trusted = await call(`${tabPath}/evaluate`, { userId, expression: "window.probeTrusted" });
assert.equal(trusted.data.result, true);
const wrongProfile = await call(`${tabPath}/evaluate`, {
userId: "umt-upgrade-other-isolated",
expression: "document.title",
});
assert.notEqual(wrongProfile.status, 200);
const cookies = await call(`${cookiesPath}?urls=https%3A%2F%2Fexample.com%2F`);
assert.ok(cookies.data.cookies.some((c) => c.name === "umt_upgrade" && c.value === "candidate-only"));
await call(`${tabPath}?userId=${userId}`, undefined, true, "DELETE");
console.log(
JSON.stringify(
{
mode,
ok: true,
instanceId: instance.data.instanceId,
identity: identity.data,
checks: [
"health",
"authorization",
"fixed-identity",
"cookies",
"new-context",
"new-page",
"evaluate",
"snapshot",
"screenshot",
"click",
"trusted-mouse-click",
"profile-isolation",
...(mode === "after" ? ["restart-persistence"] : []),
],
},
null,
2,
),
);
+4 -3
View File
@@ -26,8 +26,9 @@ BACKUP_KEEP=14
# PG_MAINTENANCE_WORK_MEM=256MB
# PG_MAX_WAL_SIZE=2GB
LOG_LEVEL=info
# APP_VERSION=v0.1.0
# APP_VERSION=v0.3.8
# 国内网络构建换源(可选):APT_MIRROR=mirrors.ustc.edu.cn
# 桌面底座 HTTPS 软件源代理(仅构建时生效,地址须能从构建网络访问):APT_HTTPS_PROXY=http://代理地址:端口
# worker 并发(可选):io 组分文件 / 网络 / 维护三条道,agent 组(Agent 会话)单独一池。
# 三条道缺省用 设置 → 自动化 → 高级 里的值(可热改);设了下面的变量就以变量为准。
@@ -41,7 +42,7 @@ LOG_LEVEL=info
# ── 浏览器服务(camofox-browser,原站视图 / Agent 浏览器能力;docs/runbook/browser.md)──
# 缺省随 compose 起一个 camofox 容器(只在内网,不映射端口),app 自动登记为 camofox-env。
# 缺省镜像是浏览器桌面(原站视图桌面模式,docker/browser-desktop);要先有 camofox-browser:1.17.0(runbook browser.md §1.1)。
# 缺省镜像是浏览器桌面(原站视图桌面模式,docker/browser-desktop);要先有 camofox-browser:1.18.0(runbook browser.md §1.1)。
# 访问凭据:camofox 与 app 共用;建议设置(openssl rand -hex 24)。桌面模式的控制 / 只读口令也由它派生,不设则桌面模式不可用
CAMOFOX_ACCESS_KEY=
# 接自有 camofox 实例:改成它的地址与凭据,并把 CAMOFOX_REPLICAS 设 0
@@ -62,7 +63,7 @@ CAMOFOX_ACCESS_KEY=
# ── 原站视图桌面模式(docs/runbook/browser.md §8)──
# 只要截图模式:改回纯截图镜像(同一个卷,登录状态通用)
# CAMOFOX_IMAGE=camofox-browser:1.17.0
# CAMOFOX_IMAGE=camofox-browser:1.18.0
# 桌面分辨率与帧率(纯 CPU 编码,持续滚动约占 1 核)
# BROWSER_DESKTOP_WIDTH=1280
# BROWSER_DESKTOP_HEIGHT=900