我没写一行代码,飞书里聊出了一个 Chrome 扩展

我平时使用 Chrome 经常开着三四十个标签,找一个东西要在标签栏上来回扫,眼都花了,而且非常影响效率,这是一个真实存在的痛点,并且困扰我很久了。 一天后后:一个跑在我浏览器里的扩展,按域名把所有标签摊成瀑布卡,一眼能看到哪个站开太多了。 关键的事:这一次我一行代码都没写。

TabHub 跑起来的样子

整个开发过程,我手里只有飞书。

没有 VSCode,没有 Chrome DevTools,没有 git 命令。所有需求都是我用语音、文字、截图发到飞书里跟 Claude 说的,Claude 通过 lark-channel-bridge 桥接到我的飞书账号上,自己写代码、自己装扩展、自己截图发回来给我看,我说「这里再小一点」,它就改。

最后开源到 GitHub、部署到我的个人 pages,全程也是在飞书里说一句它就办了。

我管这种交互叫 「躺平开发」——人躺着,活照干🐶。


灵感是从哪冒出来的

那天我又一次在 Chrome 标签栏上来回滑,找一个早上开过的飞书文档。明明开着,就是找不到。

我对着浏览器骂了一句:「我的标签到底在哪里?」

骂完愣了一下——这句话本身就是个产品名啊。

我打开飞书,给 Claude Code 发了一条语音:

我想做一个东西。每次我标签开太多就找不到。能不能搞一个页面,把我所有打开的标签按网站聚类摊出来?类似 Pinterest 那种瀑布流。名字就叫「我的标签在哪里」。

Claude 没废话,问了我一句:「老大,你想先做静态原型看效果,还是直接做能装的 Chrome 扩展?」

我说先看效果。它就开干了。


原型 v1.0 → v1.7:在飞书里改了七稿

第一版出来的时候,是个长得很普通的卡片网格。我看了眼说:

太规矩了,我想要那种编辑设计 / 杂志感的,米黄底、衬线大字、几何配色,有点 CAUSO 那个味儿。

它改了。第二版有了米黄底和衬线字,但配色还是太冷。

第三版加了橙黄色。第四版把 hero 区的大字换成了「当前打开 27 个标签,来自 9 个域名」这种直接说人话的句子。第五版加上「我的标签在哪里?」的衬线大字 + 橙色问号。

第六、第七版调的是节奏感——卡片之间的距离、卡头颜色梯度、tab 行的留白。

每一稿都不用打开任何工具。Claude 在本地起了个 python3 -m http.server,用 playwright-cli 跑无头浏览器截图(这里有个老大的硬规矩:Chrome 跑久了内存泄漏严重,所有浏览器调试一律走 playwright-cli 无头模式),然后把截图直接通过 lark-cli 发到飞书里。

手机一震,看一眼,回一句「右上角那个标签数字小一点」,循环继续。

这一段体感最深的是:视觉迭代的瓶颈从来不是工具,而是来回的成本。以前自己改 CSS,每次都要切窗口、刷浏览器、对比上一版,注意力散得很。现在我躺着看图,注意力全在「这个版本到底好不好看」上。


「不写代码」到底是怎么发生的

可能有人会问:你说一行代码没写,那到底是谁在写?

是 Claude。但 Claude 写完之后,不是甩给我一个 PR 让我去 review,而是自己跑通自己截图自己发结果回来。

整个链路是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
我(飞书)
│
└──> lark-channel-bridge(在我电脑后台跑)
│
└──> Claude Code(本地 CLI)
│
├── Edit / Write 改代码
├── Bash 执行命令
├── playwright-cli 截图
├── lark-cli 把卡片/截图发回飞书
│
└──> 我(飞书)继续看 + 回复

lark-channel-bridge 是关键。它把我和 Claude 这个 CLI 之间架了一根管子。我在飞书里发的每条消息它都自动转给 Claude 当作 prompt,Claude 的回复也通过 lark-cli 用飞书 CardKit 2.0 卡片的形式回来。

不是简单的文字消息——是带按钮、能引用、能贴图的交互卡。

这个体验差距挺大。同样是「让我确认一下」,干巴巴一句「你看一下这个截图」和一张带「确认 / 重做 / 微调」三按钮的卡,后者点完按钮 Claude 立刻就知道我选了什么继续干,前者我还得手动打字。

所以你看到的「一行代码没写」,更准确的表述是:我不需要在任何一个工具里手动操作。打开浏览器、起服务、跑测试、截图、推 git、装扩展——这些通常占据 80% 时间的「胶水活」,全都被 Claude 当成它流程的一部分自己做了。


MV3 扩展从零搭起来

原型敲定后,老大(也就是我自己)一句话:

做成 Chrome 扩展。我想直接装到浏览器里用真实的标签数据。

这一句话之后,Claude 在 /Users/lytton/feishu-cc-workspace/code/tabhub-extension/ 下从零起了一个 MV3 项目:

1
2
3
4
5
6
tabhub-extension/
├── manifest.json MV3 manifest
├── background.js service worker
├── tabhub.html 主页面
├── tabhub.js 全部逻辑
└── icons/ 三种尺寸图标

第一版装到我浏览器里——chrome://extensions → 开发者模式 → 加载已解压——能开起来。但出问题了。


踩过的坑(按时间排序)

坑 1:装上之后瀑布卡全是空的

我截了一张图发给 Claude:右上角统计数字是对的(「9 个域名,27 个标签」),但下面一张卡都没有。

1
"依旧没有任何的真实域名的瀑布卡。"

Claude 排查了一会儿,告诉我:是 render 函数在拿到真实数据时炸了。原型用的是 mock 数据,字段都是齐的;真实 chrome.tabs 返回的对象里有些字段是 undefined(比如某些 tab 的 lastAccessed),还有标题里带英文双引号没转义。

修复是三件小事:

  • 给所有可能 undefined 的字段加 fallback
  • HTML 属性里的 " 和 < 一律转义
  • favicon 从 Google s2 兜底切到 Chrome 原生 _favicon API(chrome.runtime.getURL('_favicon/?pageUrl=...&size=64')),这样离线也能拿到真图标

教训:原型跑通不等于真实数据跑通。原型阶段省下的字段校验,到真实数据这一步要还回来。

坑 2:CSP 把内联 script 全干掉了

修完上面那个,浏览器控制台又冒出来一条:

1
2
Refused to execute inline script because it violates the
following Content Security Policy directive: "script-src 'self'".

这是 MV3 的默认安全策略——不允许内联 <script> 标签,也不允许 onerror="..." 这种内联事件处理器。

我以前没踩过这个,因为 MV2 时代默认 CSP 是允许内联的。MV3 一刀切了。

修复方案有两步:

  1. 把原型那个塞在 tabhub.html 末尾的 <script>...大段代码...</script> 整块抽出来,存成 tabhub.js,然后 HTML 里写 <script src="tabhub.js"></script>。
  2. 那张 <img onerror="..."> 兜底逻辑也违规。改成在 JS 里 addEventListener('error', ...):
1
2
3
4
5
6
7
mount.querySelectorAll('img[data-fallback]').forEach(img => {
img.addEventListener('error', () => {
const span = document.createElement('span');
span.textContent = img.dataset.fallback;
img.replaceWith(span);
}, { once: true });
});

教训:MV3 写扩展,从第一行代码就把所有 inline 戒掉,不要等 CSP 报错才回来抽。

坑 3:macOS 的 sips 渲染 SVG 不靠谱

中间我说「这个橙色方块图标太丑了,换成几何拼接风格的,米黄底 + 暗方块 + 橙菱形 + 黄三角 + 黑圆点」。

Claude 写了一个 SVG 出来,然后要把 SVG 转成三种尺寸的 PNG(16/48/128,扩展 manifest 要求)。第一反应是用 macOS 自带的 sips:

1
sips -s format png icon.svg --out icon-128.png

结果出来的 PNG 全是黑的。sips 对 SVG 的支持很拉胯,遇到 clip-path 和 transform: rotate(45) 这种就渲染不出来。

绕路:

  1. 写一个 icon.html 把 SVG 塞进 viewport 占满
  2. 用 playwright-cli 跑无头浏览器开这个 html
  3. resize 到目标尺寸
  4. 截图保存
1
2
3
playwright-cli -s=ico open http://localhost:8769/icons/icon.html
playwright-cli -s=ico resize 128 128
playwright-cli -s=ico screenshot --filename icons/icon-128.png

三个尺寸截三遍。视觉效果对得上。

教训:SVG 转 PNG 这种事,浏览器永远是最准的渲染器,命令行小工具能用就行,不能用就上无头浏览器。

新图标 v0.1.1

坑 4:git push 大文件超时

代码写完,老大一句「推到 GitHub,公开仓库,写 README」。

gh repo create --public --push 一气呵成,仓库出来了。第二天我补了一张全屏截图(506KB)想加到 README 顶上展示效果,git push 死活推不上去:

1
2
3
error: RPC failed; HTTP 408 curl 22 The requested URL returned error: 408
send-pack: unexpected disconnect while reading sideband packet
fatal: the remote end hung up unexpectedly

切 http.version=HTTP/1.1、加 http.postBuffer=524288000、重试三次,都炸。

挖根因——curl --max-time 10 -v https://github.com 一看,DNS 解析出来是 198.18.0.44。这是 Clash 的 fakeip 段。我本地代理在转,但当时那条线路对 GitHub 的连接不稳,HTTP 协商超时。

绕路:

  • 文本改动(改 README,没图)单独切一个 commit,几 KB,能 push 上去
  • 那张 506KB 的图,不走 git push,直接用 gh api 把文件 PUT 到 contents API:
1
2
3
4
5
base64 -i docs/screenshot.png > /tmp/sshot.b64
gh api -X PUT repos/Azure12355/tabhub-extension/contents/docs/screenshot.png \
-f message="docs: add real screenshot image" \
-F "content=@/tmp/sshot.b64" \
-f branch=main

GitHub 服务端会自动生成一个 commit 把这个文件加上。本地 git pull --rebase 一下就同步了。

教训:gh api 走的是 HTTPS API 通道,和 git push 走的 git smart-http 协议不在一根线上。前者通了,后者断了,是常见状态。大文件推不动的时候,直接走 API 上传往往更稳。


开源 + 部署:又是一句话的事

代码 ok 了,仓库地址 github.com/Azure12355/tabhub-extension 也公开了,MIT 协议,README 写得清清楚楚——hero 截图、6 大特性、4 步安装、目录结构、路线图全在里面。

我说:

把这个项目整理到我的 weilanx-pages 里,然后写一个 demo 演示页,用于跳转和演示。

Claude 干了三件事:

  1. 主站 pages.weilanx.com 上加了一张项目卡(PROJECT 04 / BROWSER EXT),点击直达 demo
  2. 在 /tabhub/ 下写了一个完整的 landing 页:hero「我的标签在哪里?」+ 截图 + 三按钮(在线预览 / GitHub / 安装指南)+ 6 特性卡 + 4 步装扩展 + 可复制的 git clone 命令块
  3. 在 /tabhub/preview/ 下放了一个 web 版:扩展 UI 直接跑 mock 数据,访客不用装扩展就能体验完整交互。landing 页里把它嵌成 iframe

pages landing 页

最后一句 edgeone pages deploy 推到 EdgeOne,2 分钟后线上可访问:


复盘:这种工作方式到底好在哪、坏在哪

做得好的地方

注意力没被打断过。我一整天该工作工作、该吃饭吃饭,飞书一震我瞄一眼回一句,工程就推进一步。原来当老板的感觉这么爽!

视觉迭代真的快。从 v1.0 到 v1.7 改了七稿,全过程不超过两小时。我自己手动改 CSS 加切窗口看效果,七稿至少一整天。

坑全都暴露在外面。Claude 报错就发出来,能看到「哦原来 MV3 CSP 不让内联了」「哦原来 sips 渲染不动 SVG」。这些经验我真实学到了,而不是 Claude 默默处理掉我啥也没学到。

做得不太好的地方

我有点失去对代码细节的掌控。中间有一段我让它「实现真实的 tab 切换」,它给出的方案是先 chrome.windows.update 再 chrome.tabs.update。这个顺序对不对?我没去查文档。我相信它,它跑通了我就用了。这种信任的代价早晚会以某种形式来收账,我目前还没遇到,但我知道它在。

对话成本随项目变大。前期改原型一两句话能搞定,到后期上线、写文档、改 README,每次都要打几十个字才能说清楚要什么。临界点大概在「这个项目超过 5 个文件 + 3 种工具链」之后开始变重。

沟通失败的情况也有。中间让它「让 TabHub 钉在我标签栏左上角」,它跟我解释 Chrome 没这个 API,扩展只能由用户手动 pin 到工具栏右侧。这种「想法不可行」的反馈,文字 + 截图来回了两轮才说清。

我下次会怎么做

  1. 关键决策点开 plan mode。比如选 MV3 还是 MV2、要不要 override newtab,这种决定影响后面一周的事,应该让它先出 plan 我确认,不要直接动手。
  2. 更早做端到端联调。原型阶段 mock 数据塞满了所有字段,到真实数据这一步炸了。下次原型阶段就用「半残的 mock」——故意留几个 undefined,提前把鲁棒性写进去。
  3. 重要里程碑要它发卡总结。「v0.1.1 上线」「开源完成」「部署成功」这种节点,用 CardKit 卡片把变更点列出来,比扒聊天记录回顾要清晰得多。

写到这里我想说一个有点反直觉的事。

以前总觉得「不写代码就能开发」是个销售话术,是给非技术人画的饼。但这次跑下来我发现,正因为我自己懂代码,我才能把这种方式用到极致。

什么时候让它自己跑,什么时候我要介入;它说「修好了」我信几分,它说「这个搞不定」我能不能反推回去;CSP 报错我看一眼就知道该走哪个方向——这些判断没法外包。

所以这不是「门槛消失了」,而是门槛挪了个位置。从「会不会写代码」挪到了「会不会判断代码该往哪写」。

如果你也想试试这套交互,三件套:

  1. 装 Claude Code — Anthropic 官方 CLI
  2. 装 lark-channel-bridge — 飞书桥
  3. 装 lark-cli — Claude 发卡用的

配好之后,你的飞书里多一个机器人,跟它说话就行。

最后丢一句话给那些和我一样标签管理失控的人:

你的注意力比你的标签金贵。 标签丢了能重新搜出来,注意力丢了找不回来。

TabHub 开源在这里,欢迎拿走改:github.com/Azure12355/tabhub-extension ✨