ENX Mark Known: 从等待接口改为乐观更新

背景 enX 的 Chrome 扩展会给查过的生词画下划线。主窗口点一个词会弹出浮层,浮层里有 Mark Known(标为已认识)按钮,点了之后这个词的下划线消失。 后来侧边栏的整句翻译也加上了同样的下划线(ADR-053),于是有了一个新问题:在主窗口标为已认识之后,侧边栏里这个词的下划线也应该一起消失。 原来的做法 content script → background → POST /api/mark → enx-api 写库 ↓ 成功 content script 收到回复 → 去掉主窗口下划线 主窗口要等接口成功才去掉下划线,侧边栏完全不知道这件事。 改成什么样 content script:先去掉主窗口下划线 └─ runtime.sendMessage({ type: 'markAcquainted', word }) ├─ 侧边栏:收到后立即去掉下划线,不回复 └─ background:调 enx-api,完成后回复 content script 两点变化: 主窗口先改界面,再让 background 在后台调接口,也就是常说的乐观更新(optimistic update),和它相对的是原来那种等服务端确认再改界面的做法。 侧边栏不需要新的通道。chrome.runtime.sendMessage 从 content script 发出时,会投递给扩展里所有监听 runtime.onMessage 的页面,background 和侧边栏都能收到。侧边栏只需要多注册一个监听。 为什么可以先改界面 标为已认识失败的代价很低:服务端没记上,用户下次读别的文章时这个词还会带下划线,再标一次就行。为了这个小概率的失败让用户每次都等一个网络往返,不划算。 如果改成「background 调接口成功后再通知侧边栏」,状态会更严格一致,但侧边栏要多等一个接口的时间。对这个操作来说,一致性没有那么重要。 多个接收方只能有一个回复 runtime.sendMessage 允许多个页面监听,但发送方只会收到第一个调用 sendResponse 的回复,其余的被忽略。侧边栏如果也回复,会抢在 background 前面,content script 就看不到接口的真实结果了。 ...

Created 12126-10-12 · Updated 12126-10-12 · 1 min · 95 words · -

Chrome Side Panel 侧边栏

Side Panel 是什么 Chrome 窗口右侧可以展开的一栏,和当前网页并排显示。Google 的说法是「a new companion surface that allows users to use tools alongside the content they are browsing」:一个伴随页面的界面,让用户一边看网页一边用工具。 它分两层: 浏览器自带的侧边栏:书签、阅读清单等 Chrome 自己的功能。 扩展的侧边栏:扩展通过 Side Panel API(chrome.sidePanel)在这一栏里显示自己的页面。 时间线 时间 版本 变化 2022-03 Chrome 99 稳定版出现侧边栏,内容是书签和阅读清单(9to5Google 报道;Google 自己的博客写的是 2022 年 5 月加入) 2023-05 Chrome 114 扩展可用 Side Panel API,只支持 Manifest V3,需要 sidePanel 权限 2023-08 Chrome 116 新增 sidePanel.open(),可以由扩展主动打开,但必须在用户操作(点击、快捷键、右键菜单等)里调用 为什么要有 Side Panel API API 出现之前,扩展想做侧边栏,只能用 content script 往每个网页里注入一个 <div>。Google 在发布 API 时给的理由: ...

Created 12126-10-12 · Updated 12126-10-12 · 1 min · 131 words · -

2026 W38-W41

临近国庆节,节奏有点乱。有四周没写周记了,W38 到 W41 合成一篇。这四周的主线还是 enx:给它取了个名字叫 Catglish,部署生产环境。最近在准备提交 Chrome Web Store,提交之前得再调整一轮 UI。 Catglish W37 里纠结的名字,最后用了 Catglish。网站和 Chrome 扩展上的名字都改成了 Catglish,仓库名还是 enx,等上线之前再找时间改。 W38 主要在调细节。跟 AI 一起重新做了一遍配色,统一了各个组件的颜色 token。在颜色这件事上,AI 比我专业多了。周五 Claude Code 赠送的 100 美元额度第二天就要过期,那两天开着 Opus 改了很多东西,也第一次往生产环境部署了一个版本。 W39 在补生产环境的链路。部署流水线从 GHCR 加 SSH 换成了 ECR 加 SSM,机器调整成 t3a.small。扩展的内容脚本改成按需注入,不再在每个页面上都注入。周五集中做了一次依赖版本升级:Next 16、Vite 8、TypeScript 6。E2E 测试改成用真实的登录流程跑。 W40 是国庆: 网站上加了 Beta 标记和 GitHub 链接 加了定价页;现在的配额都是内测阶段的数字,跑一段时间再定上线时各个等级的额度 除了按月订阅,也可以一次性付款购买 AI 积分 本地词典查不到的词,可以调用 AI 生成释义 收藏页面,以及按站点设置「在这个网站上自动开启」 另外加了 Prometheus 监控指标,homelab 和生产环境的监控面板都接上了。 W41 打好了 0.1.0 Beta 的扩展包,画了个 logo:一个带猫耳朵的 C。 ...

Created 9096-10-09 · Updated 9096-10-09 · 1 min · 173 words · -

Chrome Extension 开发笔记

chrome extension content script, 只能访问 dom, 不能访问页面 js 变量和函数 Injected Script, 被插入到页面里的 js 代码 chrome://extensions puzzle button: chrome extension 图标后面的拼图按钮 develop chrome extension doc https://developer.chrome.com/docs/extensions/mv3/ javascript web api: https://developer.mozilla.org/en-US/docs/Web/API chrome api: https://developer.mozilla.org/en-US/docs/Web/API html: https://web.dev/learn/html/ css: https://web.dev/learn/css/ javascript: https://developer.mozilla.org/en-US/docs/Learn/JavaScript chrome extension development overview: https://developer.chrome.com/docs/extensions/mv3/devguide/ chrome extension 的构成 The manifest, manifest.json, 唯一一个必须要存在于 extension 根目录的文件 The service worker, service worker 监听 chrome 的各种事件, 可以调用 Chrome api, 但是不能直接操作页面内容 Content scripts: content script 可以直接在页面中执行 js 代码, 可以直接操作 DOM, 还可以跟 service worker 通信 The popup and other pages content script 与 service worker 的分工和通信 各自负责什么 对比项 content script service worker(background) 运行位置 注入到网页里,每个匹配的 tab(以及 all_frames 打开时的每个 frame)各一份 整个扩展只有一个,运行在扩展自己的后台 DOM 能读写所在页面的 DOM 没有 DOM,也没有 window、document、localStorage 页面的 JS 变量 访问不到,运行在 isolated world 访问不到 可用的 chrome API 只有一小部分:chrome.runtime(发消息、getURL 等)、chrome.storage、chrome.i18n 按 manifest 里申请的 permissions 全部可用:tabs、scripting、cookies、alarms、contextMenus 等 网络请求 fetch() 按所在页面的 origin 处理,受页面 CORS 限制 对 host_permissions 里的域名 fetch() 不受 CORS 限制 生命周期 跟着页面走,页面刷新或关闭就没了 事件驱动,空闲约 30 秒被回收,有事件再拉起(见下一节) 一句话概括:content script 负责“页面里看得见的部分”,比如读取用户选中的文字、在页面上插入按钮或浮层;service worker 负责“页面之外的部分”,比如调用后端 API、保存登录 token、管理 tab、响应扩展图标点击和右键菜单。两边都做不了对方的事,所以绝大多数扩展功能都是两者配合完成的。 ...

Created 21216-04-21 · Updated 4046-10-04 · 4 min · 801 words · -

2026 W36

enx:认证从 Cognito 换成了 Clerk,单词高亮换成了浏览器原生的 Highlight API。 enx:认证从 Cognito 换到 Clerk 当初选 Cognito 的原因是,那会儿在聊一个职位,对方公司用 Java 技术栈,也用 AWS。我想让自己更熟悉 AWS 和 Java 一些,所以 enx 选型的时候选了 Cognito,还给 enx 加了一个 Java 后端。 那个职位后来没有 Offer,见 W33。 这周把认证换成了 Clerk。换的原因是 Clerk 和 Google 登录集成得更好一些,另外它同时支持 Google 和 GitHub 两种登录。网页端和扩展的会话也能同步。两端手写的 OAuth 客户端一起下线,Cognito 那套基础设施也删掉了。 enx:单词高亮换成浏览器原生的 Highlight API 有天晚上碰到思科的前同事 Roc,聊了一会儿,发现自己的前端知识实在太匮乏了。页面上点一下要查的词,其实可以用鼠标的坐标反查到点中的是哪个词,不需要像之前那样预先改 DOM、把所有词都拆开包起来,也不用挂那么多 onClick。 以前的做法就是把匹配到的单词用元素包起来,再给这个元素加样式。这样会改动页面的 DOM,在结构复杂或者会自己重绘的页面上容易出问题。 这周换成了 CSS Custom Highlight API:高亮范围用 Range 描述,交给浏览器渲染,不再往页面里插元素。点击查词改成在容器上做事件委托,用 caretPositionFromPoint 定位点到的是哪个词。 顺带把两件事拆开了:「阅读时高亮生词」和「点击查词」现在是两个独立的开关,可以只启用其中一个。X 上切换推文时正文是原地替换的,加了自动重建。Playwright 的用例按新的实现重写了一遍。 周六:大黑石 周六老婆公司团建,在大黑石的海边烧烤,她带着女儿去了。女儿下午有英语课,我中午开车去大黑石的沙滩接她们。 据说,小朋友到了沙滩就想去海边,饭都不吃了。一开始还在岸边挖沙子,后来看妈妈下水游泳,她也要下水。下水之后就不想走了,用老婆的话说是 “血脉觉醒”。 上次带她去海边是在广鹿岛,那时候她还只是在浅水里捡海星和贝壳。 路边为啥要栽这么多树… 开车去大黑石的路上,偶尔过桥的时候能看到远处的山,景色还不错。其余大部分时间视线都被路两边的树挡着,开多久,窗外都是一样的树。 ...

Created 7076-09-07 · Updated 7076-09-07 · 1 min · 82 words · -

2026 W33

面试结果 上周那场第二轮面试,周二有了结果:职位不匹配。 从 4 月离职到现在同事/朋友帮推过三家公司。一家给了 offer,薪资太低,拒了;一家没有下文;这一家前后聊了四个月,两个岗位都进了二面,都没有 offer. 这一家前后接触了一位 HR 和八位面试官。感觉得出来,有的是希望我加入的,也有的对我有疑虑。anyway,最终的结论是职位不匹配。 接下来先测试 enx 独立商业化的可行性:把功能做完、部署到 AWS、接上 Stripe 订阅,先在 YouTube 和 X 上推(因为只做了 google 账号登录),看能不能有几个付费订阅用户;跑不通的话就再找工作。 enx: 划词翻译 之前选中一个短语会走词典查询,而后端判定"包含空格就是句子",直接返回一句"句子翻译功能暂未开放",短语永远拿不到释义。现在改成:选中的如果是句子中间的几个词,侧边栏显示这个短语在整句里的含义。像 “hunt down emails”,三个词单看都认识,放进上下文里是什么意思才是要查的。 另外给三个翻译 provider 各补了单测。 enx:上线还差什么 周三花了一天,把 enx 距离上线还差哪些事情列了一遍,写成两份文档,一份是商业化设计,一份是任务清单。 先定义什么叫"上线":在生产环境上,真实用户可以注册、登录、订阅、使用 AI 翻译、被限流、退订,这条链路全部跑通,才谈得上开始推广。 顺序定成三步。先在现在这套 homelab 环境里把订阅功能做完、测通,不用等基础设施迁移——然后把跑通的功能迁到 AWS。最后在生产环境完整跑一遍端到端冒烟测试,作为发布门禁。 几条已经定下来的: 计算平台继续用 EC2,不上 ECS 或 EKS。EKS 控制面每月的固定成本,在验证付费意愿这个阶段不划算 上线时数据库继续用 SQLite,先不迁 PostgreSQL。现有链路已经跑过,上线门槛越低越好,数据库迁移不是订阅功能的前置依赖 域名不换。Cognito 的 callback、CORS、证书、扩展权限都已经围绕现在这个域名配好了 全部开源,包括计费规则,继续用 AGPL-3.0 最后一条想得比较久。最初担心闭源可以防止别人用 AI 抄产品,后来推翻了:闭源只能防直接复制代码,防不了照着功能用 AI 重写一遍,而后者才是真正的风险,源码可不可见对它基本无效。定价和免费额度这些数字,用户用一遍产品就能观察到,藏代码也藏不住。真正让我决定开源的是另一件事——找工作这条退路还在,一个有真实用户、有测试和 ADR 的开源项目本身就是求职作品集,这个价值闭源换不来,而闭源换来的那点"防护"本来就是假的。 局域网域名的一秒延迟 周五在准备 enx 的测试环境,发现局域网里的域名访问都带着一段很规律的延迟。查下来是 IPv6 的 DNS 问题。 ...

Created 16166-08-16 · Updated 16166-08-16 · 1 min · 176 words · -

2026 W32

广鹿岛:彩虹滩赶海 上周末坐船去了广鹿岛。 周一换了个海滩,去彩虹滩赶海。这次出来玩时间赶得不错,退潮都在傍晚 6 点左右,太阳快落山,正好是一天里凉快下来的时候。海滩很浅,可以往前走很远,靠岸这一段脚下都是细沙,沙滩上有很多海葵,寄居蟹更是数不过来。再往深处走,细沙变成礁石,海草也多起来,水到膝盖左右。 四岁的女儿一路捡,寄居蟹、螃蟹、各种贝壳,把整个桶底铺满了。后来我们在海草附近捡到一只海星,她立刻把桶里的螃蟹和寄居蟹全倒回海里,整个桶只装这一只海星。 海星带回了民宿。到晚上它大概是缺氧,开始往桶外爬。我们商量了一下,还是放回去吧,不然会死掉。于是又把海星送到离住处最近的海里。 赶海的时候一边拍照一边让 AI 识别,认识了海葵和鱿鱼的卵鞘。海葵挺有意思,拿铲子碰一下,它整个身体缩进沙子里,同时喷出一股海水。鱿鱼的卵鞘在滩上到处都是,半透明、软软的、圆圆的一团。 周三从广鹿岛回大连。在杏树港下船之后是我开车,女儿自己坐安全座椅,全程走国道没上高速——小朋友随时可能需要我,国道方便停车。过几周我要单独带她去爷爷奶奶家,这次算是一次 dry run。 enx:遮阳伞下的 side panel 周二在岛上找了家有泳池的酒店,女儿和妈妈在泳池里泡了一下午。我没带泳衣,就在遮阳伞下写码。 这周 enx 主要在开发句子翻译的 side panel。 后端给三个翻译 provider 都加上了「按句子上下文翻译单词」的能力,配了对应的 handler 和路由,每个 provider 补了单测。前端这边,侧边栏里点击单词后的展示从单行文字改成了卡片。 卡片这块反复改了几版。一开始词、音标、上下文含义、查询次数分四行堆着,太占纵向空间,改成一行内排列、放不下才折行;有道词典的外链也删掉了,侧边栏就这么宽;「Query Count」几个字重复出现太啰嗦,换成一个放大镜图标,原文字挪到 title 属性里做悬浮提示。 加载方式也调了。以前是词典查询和 AI 上下文翻译两个请求都回来才一起渲染,现在点击的时候就先插入一张骨架卡片,两个请求各自返回后只更新自己那部分。词典那边通常更快,音标和释义会先显示出来。列表按点击时间倒序排,重复点同一个词不再发请求,只把已有的卡片挪到最前面。 第二轮面试 周三从广鹿岛回来,在杏树港排队驶出停车场的时候接到了 HR 的电话。是第二轮面试的邀请,安排在周五下午,30 分钟,听起来会是全英文。 当天下午要带娃上课,距离面试还有一天多准备时间。上一次面试因为一直在看 Spring 的资料,没准备自我介绍,英文的自我介绍讲得不太好,这次得准备一下;剩下的主要是怎么用英语把简历上的经历说清楚。周四一整天和面试当天的白天,基本都在准备这些。 周五当天出了个小插曲。我之前很少用 zoom,16:20 就打开了,输会议号、输密码、调整摄像头和麦克风,然后等面试官上线。等到 16:27 还没看见人,才注意到窗口右下角有一个 join 按钮——原来我一直在会议外面 -_- 赶紧点了 join,发现两位面试官已经在线了。 面试过程还算顺利。我的英文不是特别好,中间有问题没听清楚就麻烦面试官再讲一遍,对方会很友好地放慢语速重复。有些问题答得还好,也有因为准备不够没能表达清楚的。面试官也问到了个人项目,我讲了 enx,说计划部署到 AWS 上,或许能帮到其他人。 快结束的时候我还纠结了一下,要不要解释一句我其实一直在会议外面等着。但这么复杂的句子,我的英文暂时还撑不住,就直接 say goodbye 了。

Created 9096-08-09 · Updated 9096-08-09 · 1 min · 62 words · -

2026 W30-W31

这两周的周记合成一篇。 enx:先把功能做完,上线再说 enx 是我自己的项目,一个查词加翻译的 Chrome 扩展。这两周它是主线。 前一周主要在部署链路上:调查词性能(SQLite 相关的调整、给 words 表加索引的 migration),构建时把 version 注入镜像,Go API、Java API 和 UI 三条镜像链路陆续跑通,部署到 homelab。 后一周是功能推进: 合入本地 ECDICT 词典,查词路径不再依赖外部接口 单词弹窗迁到 React + Shadow DOM,样式不会再被宿主页面污染 加了句子翻译的 Side Panel,翻译后端做成多 provider(MiniMax、Bedrock、Kimi),用 factory 加 handler 分派 修扩展的 session 超时问题,Cognito 那边改成手写刷新逻辑 周六在岛上想清楚一件事:微信登录先不做了。现在已经有 Google 登录,把 Stripe 接上就可以上线部署。把基本功能做完先上线,不需要等到完美。 一个朋友的文档校验工具 之前写过的、给一位朋友做的文档校验工具,这两周继续往前走。 前一周主要是仓库骨架落地:架构 ADR、几个模块的 Spec、测试,周末开始写 Web UI 的 SDD 和 lint 配置。后一周把它部署进了测试环境。 另外这个项目按正常的需求分析、报价流程走一遍。因为如果以后真的在线接单,报价是绕不开的问题,正好拿它测试一下。 starlabrys 官网 公司官网这两周加了历史项目展示页,还有一个 Site Advisor 的雏形,包括意图识别、知识库、tools 和对话几个部分。ew8 帮我改的那部分周五合并进来了。 w_dashboard:一个玩具级的仓库看板 周末起了个新项目 w_dashboard,Linux 端用 Rust 加 Slint,macOS 端用 Swift 加 SwiftUI,两端各写了一版初版。 ...

Created 2026-08-02 · Updated 2026-08-02 · 1 min · 130 words · -