跳到正文

PawWork

它是一个选择优先的 Chrome 网页 agent:先在活着的页面上圈出你要的那一块,再说清想要的产出。模型 key 是你自己的,没有托管服务,模型写出来的 JavaScript 被关在浏览器内的 QuickJS 沙箱里,最后交回的是一份可以继续编辑的办公文件。

Screenshot of PawWork
编辑截图, 1 Oct 2026PawWork ↗

这是什么

PawWork 是一个 Chrome MV3 扩展,以 unpacked 方式加载,而不是从应用商店安装;它把你已经登录着的浏览器当成 agent 干活的那台机器。顺序本身就是重点:你不是先打字,而是先在活页面上圈出一块,这块选区随即成为「属于用户的环境上下文」——侧栏里与剪贴板钉、页面条目并列的一组组选区,只有界面能改,模型的工具被明令禁止触碰。然后你描述想要的产出,一个跑在 offscreen 文档里的工具循环会先按这一轮真正握着的东西瞄准工具清单——inspect、acquire、run、clarify、action、task、sheet、doc、web——再去调你自己那家 OpenAI 兼容模型。交付物是可以继续编辑的文件:表格、Univer 文档与站点 HTML 在预览标签里打开;会话仓库、模型循环,以及执行模型所写 JavaScript 的沙箱,全都在扩展内部。没有 package.json,没有构建步骤,没有托管模型,也没有服务端。

谁做的仓库 20 次提交里有 17 次挂在这个账号下,全部发生在 2026 年 9 月;其中两次用的名字是 0xYN,邮箱相同。另有一次来自一个 Cursor agent,一次来自一个 ByteDance 邮箱,最后一次来自工作流机器人。20 次提交里有 15 次带共同作者尾注,其中 14 条署名 Cursor。这份 recon 报告里没有仓库主的自我介绍、关注者数或其它履历信息。

它是怎么搭起来的

组成 · 6

一个 unpacked 的 Chrome 扩展,把已登录的浏览器当成那台电脑,并严格按「每个 Chrome 进程被允许做什么」分层。agent 循环绝不放在会被杀掉的地方:service worker 只拿权限面——标签、下载、跨 frame 扇出、打开预览标签与 workspace_sys 路由——而会话仓库、工具循环与沙箱都在 offscreen 文档里,通过一层 RPC 门面与侧栏说话。选区以数据而不是文本的形式进来:它在页面的 content script 里被抓到,随后被建模成「用户拥有的环境上下文」,模型能读、工具不能碰——这正是让「选择优先」成为可执行约束而不是一句约定的原因。交付物是画布——表格、Univer 文档、站点页面——在预览标签里由入库的 vendor 运行时渲染,于是一个任务的产物是用户能继续编辑的东西,而不是一条消息。这一切都跑在用户自己提供的模型 key 上,访客沙箱拿不到扩展权限,只能通过一套很小的 ABI 去碰浏览器。

Chrome host layer
src/background.js 76,439 字节,是那个 service worker,路由 workspace RPC、页面 action、sys、sheet 与 canvas 宿主、截图、下载、预览与标签组;加上注册在全部 URL 所有 frame 上的 183,745 字节 src/content_script.js(负责选区与每 frame 的 action)、一个 2,822 字节的打包脚本,以及一份 2,207 字节的 manifest:它声明了侧栏、沙箱、content script,以及架构文档列出的十二项权限。
src/agent/ — the session workspace
109 个文件:RPC 门面(sessionWorkspaceService.js 64,105 字节)、工具循环(sendMessage.js 24,681、sessionAgent.js 23,217)与一份 73,485 字节的工具实现文件,另有 durable store、交付物与 office 面、prompt、skills 和一个渲染引擎;宿主原语(acquire、run、webFetch、webSearch、netGuard)、带标签租约与任务调度器的浏览器宿主适配器(browserSysHost.js 37,106),以及 QuickJS、esbuild 与 AI SDK 的 vendor loader。
src/sandbox/ and src/offscreen/
由 manifest.sandbox 声明的 4,076 字节 QuickJS 访客运行时,由一个 1,929 字节的 offscreen 运行时以 iframe 嵌入,后者负责创建会话工作区服务。访客没有 chrome.*,只能通过 workspace_sys 路由上提供的 sys ABI 去碰浏览器——这也是 userScripts 与 debugger 两个权限存在的唯一理由。
src/preview/ — the canvases
35 个文件,扛着仓库的大部分体积:把表格、文档、站点与交付物渲染成可编辑画布的那些标签页,包括 60,164 字节的表格运行时、24,347 字节的站点动效、32,440 字节的预览脚本与 8,078 字节的表格编解码;旁边是入库的 Univer 包(12,984,218 与 8,939,175 字节)和一份 61,309 字节的 fflate。
src/sidepanel/ 与侧栏外壳
48,710 字节的 sidepanel.html 后面是 494,430 字节的 sidepanel.js 与 182,057 字节的样式表,另有 21 个支撑模块——其中 i18n 22,805 字节,还有任务状态、会话隔离、标签租约 UI 与轨迹 UI——负责渲染对话、选区上下文与交付物,不跑 agent 循环。
Docs, tests and release
根目录 AGENTS.md(8,932 字符,13,419 字节),加上 src/ 下的嵌套 agent 文档(src/AGENTS.md 8,194 字节、src/agent/AGENTS.md 11,504、src/preview/AGENTS.md 1,990)与三份 README(5,600、997 与 690 字节),写的是分层、工具契约与边界。七个测试文件覆盖纯 Node 的逻辑回归(18,524 字节)、标签租约、task 模型/调度/接线/卡片,另有一个 9,267 字节的 Playwright 烟测而 CI 不跑;1,513 字节的 workflow 跑同一套测试 glob,外加打包形状断言,其中一条要求打包产物必须带上那个 TSV 修复。

取舍,以及它替代了什么

  • 模型 key 自带,中间不放托管模型 替代 由项目自己运营一个托管或代理的模型

    README 用一句话讲清了它——没有托管模型,带自己的 key——而 key 是粘进侧栏、存在 chrome.storage.local 里的。后果也写明了:offscreen 文档在没有 key 时照样启动,因为模型只在发消息那一刻才被解析。recon 报告在整棵树里没有记下任何账号、计费或后端界面。

  • 让模型写的 JavaScript 作为访客跑在 QuickJS 沙箱里 替代 让生成的代码直接调扩展 API

    分层表给了沙箱两条禁令——不许读扩展存储、不许改 SelectionGroup——边界一节也重申访客没有 chrome.*,只能通过 sys ABI 去碰浏览器。这份隔离的代价正是 9 月 16 日那条 pull request 的主题:访客没有定时器、单次求值约二十秒上限,而模型得手工绕开这两条。

  • agent 放在 offscreen 文档里,service worker 保持无状态 替代 把会话仓库与模型循环放在 service worker 里

    架构文档是从平台事实出发的:worker 会被杀掉,所以它不能持有会话仓库、也不能跑循环,只留权限面——标签、下载、跨 frame 扇出、预览标签与 sys 路由。同一个选择也出现在项目接受的边界里:崩溃后的 Execution 直接作废,活页面标签租约存在 worker 内存里、随之消失。

  • 用可选的 durable task,而不是 exactly-once 日志 替代 一份按 call 记录、崩溃后自动重放的日志

    文档写得很明确:task 是一个跟着会话 meta 走的进度对象,alarms 只按存下来的到期时间唤醒,store 才是权威。普通消息既不创建也不绑定 task,只有 task run、alarm 或从界面 resume 才会把这一轮挂到已有 task 上。项目自称这是进度加闹钟而不是日志,并说明跨崩溃的 exactly-once 行为并未提供。

  • 在页面里等,而不是从宿主反复轮询 替代 在宿主侧反复做短求值、手工分块 sleep

    加上这两个原语的那条 pull request 是从一条真实轨迹立论的:访客没有定时器,每次求值又有约二十秒上限,于是长轮询被掐断,一次流式读取要拆成大约十次 run。等待原语用页面自己的定时器轮询,因此落在那个上限之外,最长可等 120 秒,还能一直等到一个不断变化的值稳定下来。

依据AGENTS.md(8,932 字符,13,419 字节)里的分层、领域对象、一次用户消息的路径与「未实现的边界」清单;README.md(2,189 字符)里的加载步骤、Chrome 版本要求、BYOK 与测试命令;完整的 199 个文件树及其体积;以及三条 pull request 的正文。

制作过程

5 个阶段
  1. 01

    五十分钟里连打三次包,然后才出现版本号

    仓库建于 2026-08-28,而它里面的一切都发生在九月:2026-09-01 到 2026-09-17 之间 20 次提交,最早那次本身是一次发版构建,最新那次加的是 durable task 与活页面标签租约。这一趟里出了 6 个 release。其中四个以提交号命名,且全落在 2026-09-01——unpacked-9829868 在 01:59:49,unpacked-f2a02f0 在 02:07:37,unpacked-af56da4 在 02:49:04,unpacked-570482b 在 15:03:12——五十分钟内连打三次,再隔八小时才有第四次;往后是 2026-09-09 的 v1.0.0-unpacked 和 2026-09-16 的 v1.1.0。周围是 2,881 个星、10 个 fork、5 个 watcher、0 个开启的 issue,MIT 许可,JavaScript,体积 17,773 KB。20 次提交里 15 次带共同作者尾注,其中 14 条写的是 Cursor;贡献者列表只有三项:仓库主 17 次提交、一个 Cursor agent 一次、工作流机器人一次。材料里的名字对不上:报告描述的是 Player-YN/BrowserKitten,而 README 的标题是一个中文名加 PawWork,release 标题写的是「Paw Work unpacked」,AGENTS.md 记的远端是 PawWork_ZhuaZhua.git、manifest 的显示名则是一串更长的中文。仓库名 BrowserKitten 在这些文档里一次都没出现,而更早的名字 PageWand 还留在 chrome.storage.local 的键里,消息 target 也仍以 pawwork- 开头。

  2. 02

    选区就是界面,而模型不拥有它

    这个项目对自己的说法是「选择优先」,而承担这件事的代码分在扩展的两端。页面那一侧是 src/content_script.js——183,745 字节,注册在全部 URL 的所有 frame 上——负责 AGENTS.md 里说的「伸爪选区」,以及每个 frame 内的 action 快照与改写。会话那一侧,选区不是一段提示词文本,而是一个领域对象:Group + WebItem 被描述成「用户拥有的环境上下文」,其中包括伸爪选区、剪贴板钉与页面条目,架构文档也把规则写得很直白——只有界面 RPC 能改它,工具被禁止改写 SelectionGroup。于是模型读得到用户攒起来的上下文,却没法悄悄改掉它;工具循环同样是「瞄准」而不是「全给」:sendMessage 按这一轮真正握着的东西生成清单,文档里的说法是瞄准、不藏工具。清单是 inspect、acquire、run、clarify、action、task、sheet、doc、web,由 AI SDK 7 的 ToolLoopAgent 自动选择,而 sys 刻意不在这份清单里——它是在 run 执行的代码内部被调用的。被选中的 DOM 具体怎么到达模型,材料没有覆盖:文件树里有 16,856 字节的 pickContext.js、11,440 字节的 pageItems.js,旁边还有 pageContext.js、openClassify.js、pageBlueprint.js 与 selectionSuggest.js,但解释这条链路的 src/AGENTS.md 并不在这份 recon 报告里。

  3. 03

    交回来的文件是一块还能继续改的画布

    用户最后拿到的不是一段回答,而是一件交付物;架构文档把 Artifact 定义成会话交付物——表格、Univer 文档、站点 HTML 或文件——由 run、office 工具或界面创建。文件树能看出这份工作住在哪里:会话工作区里的 officeTools.js 33,341 字节、sheetApply.js 45,764、pptxExport.js 25,344;预览标签里还有 docExport.js、sheetCodec.js 以及 60,164 字节的 sheet.js;另有两份入库的 Univer 运行时包,表格那份 12,984,218 字节、文档那份 8,939,175 字节。画布只有三种:sheet、doc,以及带 data-paw-kind=site 的 web;文档明确写了没有 Design 与 Slides 画布,尽管仓库 topic 里提到了 tldraw,也确实存在一个 pptx 模块。至于究竟是哪段代码写出 Office 容器文件本身,材料没有覆盖。材料真正给出的格式层细节只有一个 bug:2026-09-15 的一条 pull request 说,保存单元格里含制表符的 TSV 时,那个制表符被当成分隔符写了进去,于是重新打开时一个格子裂成两列、后面的值整体错位;修法是在共用的 CSV/TSV 转义器里给含制表符的值加引号,而回归测试跑的是生产函数 aoaToCsv 到 parseDelimited 的整圈——在未改动的基础上新测试会失败,三个值变成四个;修完之后测试套件报 16 passed、0 failed。

  4. 04

    没有服务端、没有托管模型,访客也没有 chrome

    这个产品做的事全在浏览器里。扩展没有 package.json,也没有构建步骤;唯一在扩展之外跑的脚本是一个 2,822 字节的 Python 打包器,它把入库的 manifest.json、icons/ 与 src/ 变成一个发版目录和一份 zip——发版就是这样发出去的。没有托管模型,也没有代理:用户在侧栏里粘贴一个 OpenAI 兼容的 key,它存在 chrome.storage.local 里;而 offscreen 文档在没有 key 时照样能启动,因为模型只在发消息那一刻才被解析。模型写出来的 JavaScript 拿不到扩展的任何权限:它作为访客跑在由 manifest.sandbox 声明的 QuickJS 沙箱里(src/sandbox/runtime.js 4,076 字节),由 offscreen 文档以 iframe 嵌入,完全没有 chrome.*;要碰浏览器只能调一套 sys ABI,涵盖 tabs、eval、fetch、cdp、download 与 screenshot,由 service worker 提供,而 userScripts 与 debugger 这两个权限只为它存在。访客跑的是模型写下的一切,而打包发生在浏览器里:一个 45,789 字节的运行时从入库的 vendor 目录里拉起 13,978,850 字节的 esbuild wasm、902,483 字节的 QuickJS loader 和 1,162,837 字节的 AI SDK loader——agent 代码因此无需本机工具链就能跑。在这棵树里,recon 报告没有记下任何服务端、部署或容器文件。

  5. 05

    作者自己叫作「自伤」的摩擦,以及被写成边界的妥协

    仓库里最能说明问题的一份文档,是 2026-09-16 加上两个沙箱原语的那条 pull request;它也是三条 pull request 里唯一有评论的一条,作者自己在下面回了一句「gj Cursor」。它在背景里写道:作者分析了一条真实轨迹——与一个已登录的对话网页对话——发现 agent 大半力气都花在跟自己的沙箱约束搏斗,而不是做别的工具做不到的事。访客是 QuickJS,没有定时器,想「等一会儿再读」就只能把 sleep 逻辑塞进由页面执行的代码里;而每次 sys.eval 都有约二十秒的硬上限(SYS_TIMEOUT_MS),长轮询会被 SYS_TIMEOUT 掐断,于是一次流式读取被迫拆成多次短求值加手工分块 sleep。作者自己的判断是这类摩擦属于自伤、而不是物理限制,修法是两个 ABI 原语:一个由宿主提供、受本轮截止时间约束且可被中断的 guest 全局 sleep(ms),以及一个 sys.waitFor——按 code、selector 或 text 在页面内用页面自己的定时器轮询,返回那个 predicate 的 JSON 值,因此不受单次求值上限的约束,timeoutMs 最长可到 120 秒,stableMs 还能让它等到一个不断变化的值稳定下来。发一条消息再读完流式回答,从大约十次 run 压缩成一次发送加一次等待。同一种写法贯穿项目的「未实现的边界」一节:它把限制就叫作限制,而不是待办清单——没有 call 级的持久日志,标签租约只存在 service worker 内存里、它一死就丢,最后是出不了浏览器——没有原生系统控制、没有本地命令,用项目自己的话说,也没有验证过高保真的 Office 往返。

相关档案

全部档案 →