[{"content":"做一个内部系统，最烦的不是写功能，是每次改完都得手动点一遍确认没坏。装 Selenium 或者 Playwright 又不划算 —— 环境要配、driver 要管、团队里非开发同学根本用不起来。\nnForce AutoTest 是想走另一条路：用浏览器扩展做测试工具。装了就能用，不用 npm install，不用 driver 版本管理，而且天然跑在你已经登录的浏览器里。\n图标是机器人头 —— nForce 系列里闪电已经被 Tools 占了，自动化就用了机器人 顺带说下配色：nForce 系列的品牌渐变是 #06B6D4 → #3B82F6 → #7C3AED，四款扩展共用这一套。\n两条产出用例的路 手写：表格化编辑 控制台左侧是用例列表，右侧是步骤表格。每行一个动作，下拉框选动作类型，填目标元素和值。跑整个流程按一下「运行此用例」。\n内置了一份示例用例（examples/demo-cases.json），装上就能跑，先看结果长什么样再决定要不要自己写。\n录制：正常操作页面 控制台右上角点「● 录制用例」，然后在打开的标签页里正常操作 —— 点击、输入、选下拉、勾选、跳页。步骤会实时出现在控制台里，点「■ 停止并保存用例」就生成一条可用例。\n录制时被测页面底部会出现一条悬浮控制条，红点 + 已捕获步数 + 「■ 结束并保存」/「✕ 放弃」。这是被真实使用场景逼出来的设计：一开始停止按钮只在控制台里，但用户录的时候焦点在被测页面 —— 操作完根本找不到结束的按钮。把停止入口放到眼前，问题就没了。\n录制处理了几个容易出错的地方：\n支持 \u0026lt;div\u0026gt; / \u0026lt;span\u0026gt; 这类伪按钮（现代前端框架里到处都是），不只认 \u0026lt;button\u0026gt; 和 \u0026lt;a\u0026gt;。 输入不需要失焦，停止输入大约 0.6 秒后自动记录。 密码框不会明文记录，存成 ******（运行前需要在编辑器里填回真值）。 同一输入框连续修改只保留最后一次。 页面跳转自动记成 goto 步骤，跳转后录制继续生效。 悬浮条自己的点击不会被录进用例 —— 捕获监听里先判断事件目标是不是控制条本身。 用例是什么 一条用例就是一个 JSON，可以直接导入导出：\n{ \u0026#34;name\u0026#34;: \u0026#34;登录流程\u0026#34;, \u0026#34;delayMs\u0026#34;: 150, \u0026#34;steps\u0026#34;: [ { \u0026#34;action\u0026#34;: \u0026#34;goto\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;https://the-internet.herokuapp.com/login\u0026#34; }, { \u0026#34;action\u0026#34;: \u0026#34;input\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;#username\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;tomsmith\u0026#34; }, { \u0026#34;action\u0026#34;: \u0026#34;input\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;#password\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;SuperSecretPassword!\u0026#34; }, { \u0026#34;action\u0026#34;: \u0026#34;click\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;css=button[type=submit]\u0026#34; }, { \u0026#34;action\u0026#34;: \u0026#34;assertUrl\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;/secure\u0026#34; } ] } delayMs 是步骤之间的缓冲延迟（默认 150ms），给 SPA 渲染留点时间，降低时序类 flake。设成 0 可以关掉。\n运行过程中工具栏的「运行全部」会变成「■ 停止运行」，点了会在当前步骤执行完之后中止 —— 而不是硬切断留一个半死不活的页面。\n目标选择器支持四种写法 写法 含义 例子 css=xxx 或裸写 CSS 选择器 css=button.primary、#login id=xxx 按 id 匹配 id=username text=xxx 按可见文本匹配（优先挑可交互元素） text=登录 xpath=xxx XPath xpath=//a[@href='/home'] text= 这个在实际用的时候最省事 —— 页面上「登录」两个字可能同时出现在按钮和标题里，按可见文本找会优先挑按钮。\n内置动作 分组 动作 页面 goto refresh back forward screenshot 交互 click dblclick input clear pressKey select check uncheck hover scroll 等待 wait（固定毫秒） waitFor（等元素出现，可设超时） 断言 assertText assertValue assertVisible assertHidden assertUrl assertTitle assertCount 高级 eval（在页面里执行自定义异步 JS） 运行规则很直白：遇到失败自动截图留证，后续步骤标记为「跳过」。不继续往下跑，是因为后面的步骤大概率会连环报错，一堆红色反而看不出真正的问题在哪。\niframe 也能测 录制和回放都支持 iframe 里的元素，这块做的时候踩了不少坑：\n录制时记下该步骤所属的 frame（层级索引 + URL），存进用例的 _frame 字段。跨域 iframe 也能读 parent.frames[i] === window 做索引比较。 回放时用 chrome.webNavigation.getAllFrames() 先按 URL 精确匹配、再按路径索引匹配，解析出 frameId 再精确派发消息。 派发消息时必须显式带 frameId。 不带的话广播会被任意 frame 抢答，所以这里没有任何回退空间。也正因为带了 frameId，才敢把之前用来防串台的 if (window !== window.top) return false 保护去掉 —— 不然 iframe 里的步骤根本执行不了。 页面结构变了匹配不到 frame 时，退回主框架执行并在报告里报错，而不是静默跳过。 导出成 Playwright 脚本 这是我觉得整个项目里最有价值的一块。\n插件形态有个绕不过的硬约束：上不了 CI。它需要桌面 Chrome，MV3 的 Service Worker 闲置 30 秒就被回收，单浏览器串行跑不了并行 —— 这些都是架构性的，改不掉。\n但那不代表它没价值，只是位置不同：插件负责产出用例，Playwright 负责执行。所以用例编辑页加了个「导出脚本」按钮，直接把 JSON 转成能 pytest 跑的 Python：\ndef test_example_com(page: Page): page.goto(\u0026#34;https://example.com\u0026#34;) expect(page.locator(\u0026#34;h1\u0026#34;)).to_contain_text(\u0026#34;Example Domain\u0026#34;) page.get_by_text(\u0026#34;More information...\u0026#34;).click() expect(page).to_have_url(re.compile(\u0026#34;iana.org\u0026#34;)) 映射规则：\n插件动作 Playwright (Python) goto / refresh / back / forward page.goto() / reload() / go_back() / go_forward() click / dblclick / hover locator.click() / dblclick() / hover() input / clear locator.fill(v) / fill(\u0026quot;\u0026quot;) select / check / uncheck select_option(v) / check() / uncheck() wait / waitFor wait_for_timeout(ms) / expect(loc).to_be_visible(timeout=) assertText / assertValue expect(loc).to_contain_text(v) / to_have_value(v) assertVisible / assertHidden expect(loc).to_be_visible() / to_be_hidden() assertUrl / assertTitle / assertCount expect(page).to_have_url(...) / to_have_title(...) / to_have_count(n) screenshot / eval page.screenshot(path=...) / page.evaluate(...) 选择器也会翻译成惯用写法：text=登录 → get_by_text(\u0026quot;登录\u0026quot;)，[data-testid=\u0026quot;x\u0026quot;] → get_by_test_id(\u0026quot;x\u0026quot;)，id=a / css=a → locator(\u0026quot;#a\u0026quot;) / locator(\u0026quot;a\u0026quot;)。\n顺带解释一下为什么当时选了 Playwright 而不是 Selenium：新项目基本没有理由再用纯 Selenium 了 —— 免 driver 版本管理、时序类 flake 少得多、单测跑得快。Selenium 现在的主要价值在多语言绑定和 Appium / 遗留浏览器那些场景。\n加一个新动作只要一个条目 所有动作集中注册在 lib/actions.js 的 NF.ACTIONS 里。加一个条目，编辑器下拉框、录制、运行、结果展示会全部自动认出来：\n// 例：新增「右键」动作 rightClick: { label: \u0026#34;右键\u0026#34;, group: \u0026#34;交互\u0026#34;, fields: { target: true, value: null }, desc: \u0026#34;派发 contextmenu 事件\u0026#34;, run: async (ctx) =\u0026gt; { const el = NF.findElement(ctx.target); NF.scrollIntoView(el); el.dispatchEvent(new MouseEvent(\u0026#34;contextmenu\u0026#34;, { bubbles: true })); } } 涉及导航、截图的页面级动作再加一个 tab: true，然后在 editor/runner.js 的 NF.TAB_ACTIONS 里实现。选择器辅助函数（findElement / setValue / isVisible / buildSelector）都在 lib/selector.js。\n注册表的写法是刻意的 —— 动作是这类工具唯一会持续增长的东西，让它扩展起来只改一个文件，比后期写文档省事。\n目录结构 ├── manifest.json # MV3 清单 ├── background/service-worker.js # 录制会话管理、导航记录、跨页重挂录制 ├── content/content-script.js # 页面内执行步骤 + 捕获用户操作 ├── lib/ │ ├── selector.js # 选择器解析 / 定位 / 生成 │ └── actions.js # ★ 动作注册表（核心扩展点） ├── editor/ │ ├── editor.html/css/js # 控制台：用例管理、步骤编辑、录制面板、结果汇总 │ └── runner.js # 用例运行器 ├── popup/ # 工具栏弹窗 └── examples/demo-cases.json # 可导入的示例用例 权限只要四个：storage、unlimitedStorage、tabs、scripting、webNavigation，加上 http://*/* 和 https://*/* 的 host 权限（测试工具必须能注入任意站点，这条避不开）。\nunlimitedStorage 是被逼出来的：失败截图存成 dataURL，很容易撑爆默认的 10MB 配额。加了这个权限之后安装提示会多一行，但测试工具的目标用户本来就清楚它要干什么，可以接受。\n安装 chrome://extensions/ → 打开开发者模式 加载已解压的扩展程序 → 选仓库根目录 工具栏出现 ⚡ 图标 点图标 → 「🧪 打开测试控制台」，左边选示例用例 → 「▶ 运行此用例」 已知限制 录制不覆盖 hover、拖拽、双击 —— 这三个在浏览器里没有可靠的捕获信号，需要手动在编辑器里补步骤。 iframe 支持了，但嵌套过深或者 frame URL 每次动态变化的页面可能匹配不到，会退回主框架并在报告里报错。 chrome://、Chrome 商店这类受限页面无法注入和录制。 密码录制为掩码，运行前要手动填回真实密码 —— 这是有意的，明文密码写进 JSON 用例里是个坑。 截的是可视区域，不是整页。 eval 动作受页面 CSP 限制，开了 unsafe-eval 限制的站点会失败。 小结 这个东西的定位一开始想错了，我本来想让它替代 Selenium。做了一半发现不对：它跟 Playwright 不在同一条链路上竞争。插件赢在用例产出（录制、复用已登录会话、零环境门槛），Playwright 赢在执行与规模化（无头 CI、并行分片、网络 mock）。\n想清楚之后方向就顺了：不跟 Playwright 抢执行，把自己做成它的上游 —— 录制出来的用例导出成脚本，CI 里跑。工具的边界在哪，比它有多少功能重要得多。\n代码在 github.com/iamnicwu/nForce-autoTest 。\n","permalink":"https://blog.nickwu.fun/post/nforce-autotest/","summary":"零依赖、免构建的 Chrome 自动化测试扩展：手写用例或用录制生成用例，跑完看每步通过/失败/耗时与失败截图，还能把用例导出成可直接 pytest 运行的 Playwright 脚本。","title":"nForce AutoTest：一个插件形态的 Web 自动化测试工具"},{"content":"平时要截个图或者录段操作演示，常规操作是：截图工具一个软件、录屏软件另一个、标注再换一个。装了三个东西，每个都只用到一小部分。\nnForce Screen Capture 是把这几件事合到一个 Chrome 扩展里。纯原生实现，没有构建步骤也没有第三方依赖 —— 克隆下来直接以「未打包扩展」加载就能用。\n弹窗上半是四种截图模式入口，下半是输出格式、延时、是否打开编辑器这些高频开关，最下面挂着最近一次截图和「编辑 / 下载 / 复制」 五种截图模式 模式 说明 默认快捷键 可视区域 截当前视口 Alt+Shift+S 整页长截图 自动逐屏滚动、捕获并拼接整页 Alt+Shift+F 滚动截屏 页面出现工具条，你手动滚动，停顿自动捕获，点「完成拼接」输出 Alt+Shift+C 框选区域 拖拽框选，按设备像素比精确裁剪 Alt+Shift+R 录制屏幕 见下一节 无默认键 除了快捷键，页面上右键也能选这几种模式。\n自动长截图和手动滚动为什么都要有 这两个不是重复功能。\n整页长截图适合普通页面：脚本自己往下滚，每屏截一次，最后拼起来。快，但遇到懒加载的图片就会截出空白 —— 因为滚动太快，图片还没加载完。\n滚动截屏就是给这种情况准备的：页面上浮出一条工具条，你自己滚，滚到哪停一下它就截一帧。图片该加载的加载完了，你满意了再点「完成拼接」。无限滚动页面、瀑布流、动态加载的列表都靠这个。\n拼接做的事情 无缝：按「实际滚动位置 × 缩放比」定位每一帧，相邻帧保留重叠区，杜绝接缝。 智能滚动容器探测：Gmail、Slack 这类把滚动放在内部 \u0026lt;div\u0026gt; 里的页面也能正常长截图 —— 不是所有页面都滚 document。 悬浮元素只留一次：长截图时 fixed / sticky 的导航栏只保留在首屏，不然会在每一屏顶部重复出现（可关掉）。 截图期间隐藏滚动条，画面干净（可关掉）。 进度气泡：页面上实时显示 正在截取整页 3/8，随时可以取消。 编辑器的工具条：矩形 / 椭圆 / 箭头 / 画笔 / 马赛克 / 文字，8 色板 \u0026#43; 粗细调节，撤销重做，缩放，复制和下载 标注编辑器 截图完成后自动打开（可以关掉这个行为）。工具是矩形、椭圆、箭头、画笔、马赛克、文字，8 色色板加粗细调节。\n快捷键手感做得比较接近设计软件：R/O/A/P/M/T 切工具，Ctrl+Z 撤销，Ctrl+Shift+Z 重做，Ctrl+S 下载，Ctrl+C 复制，0 恢复适应窗口。\n「复制」写进剪贴板的是 PNG，可以直接粘进微信、飞书、文档里。这一步比看起来重要 —— 很多截图工具复制出来是位图的某种奇怪格式，粘到聊天窗口就糊了。\n屏幕录制：给客户做演示用 这块是后来加的，但现在成了我用得最多的功能。给客户演示产品，不想装第三方录屏软件，就用它：\n点扩展图标 → 红色「录制屏幕」按钮，或右键菜单 → 「录制屏幕（Demo 视频）」。 Chrome 弹出原生选择器，挑整个屏幕 / 应用窗口 / 某个标签页。 录完有多种停止方式：点系统弹出的「● 正在录制屏幕」通知、页面右键菜单（录制中会自动变成「停止录制屏幕」）、或者打开扩展面板点停止。 停止后自动打开预览页，可以在线播放、下载 MP4/WebM，或者一键「再录一段」。 录制中面板有计时器、暂停和停止。暂停时计时器同步冻结 输出 MP4 而不是 WebM 设置里有两种格式：\n格式 适合 MP4 · H.264（默认） 微信 / 邮件 / 任何设备直接播放，给客户最省心。需要 Chrome 126+ WebM · VP9 同码率体积更小，Chrome / Edge 环境播放。老版本浏览器的首选 浏览器不支持所选格式时会自动回退为另一种，不用手动干预。\n有两个细节是专门为「发给客户」这个场景做的：\n有声音时 MP4 优先用 AAC。mime 里显式声明 mp4a.40.2，容器里才是真正的 AAC 音轨，微信、QuickTime、Windows 播放器都能出声。不声明的话 Chrome 会默认塞 Opus，苹果生态解不出来 —— 这就成了一个\u0026quot;我这边好好的，客户那边没声音\u0026quot;的问题。 MP4 优先用 avc3 编码。avc1 要求编解码描述（SPS/PPS）全程不变，录制中一旦捕获源分辨率变化（拖窗口、切全屏）就会中断录制；avc3 的参数集随码流更新，能容忍分辨率变化。不支持 avc3 的浏览器自动回退 avc1。 声音：麦克风旁白 + 标签页声音 录制时可以同时录麦克风旁白（给客户讲 Demo 时能听到你的讲解）和标签页/系统声音。两路会自动混音成一条音轨，不会互相覆盖。\n麦克风旁白开关与音量（20%–200%）、标签页/系统声音开关、麦克风授权与实时电平检测 有一个系统限制值得提前知道：macOS 的「整个屏幕 / 窗口」源本身不提供系统声音，这是 macOS 的限制不是扩展的问题。要录网页里的声音，得选「Chrome 标签页」并勾选「同时分享标签页音频」。\n麦克风授权这里也有个 Chrome 的坑：audioCapture 权限只允许打包应用（packaged app）在 manifest 里声明，扩展写进去会被 Chrome 拒绝。所以授权只能发生在可见的扩展页面上 —— 首次录制时弹窗请求、或者设置页点「允许麦克风」、或者预览页点「授权麦克风并重录」，触发浏览器原生授权框，点一次「允许」之后长期有效。\n真正执行录制的 offscreen 文档没有界面，弹不出授权框，所以这一步必须在可见页面上先完成。\n没录到声音时不会静默产出一段哑巴视频 —— 预览页会直接标注「声音：麦克风旁白 / 无（默音）」，如果是没授权，还会给出就地授权并重录的入口。这个设计是从一次实际尴尬里改出来的。\n码率三档 档位 码率 适合 流畅 2 Mbps 纯操作演示、界面文字为主，文件最小 均衡（默认） 6 Mbps 常规产品 Demo，画质与体积兼顾 高清 12 Mbps 含动画 / 渐变 / 视频画面的精细演示 1080p 均衡档大约每分钟 45MB，长演示注意磁盘空间。\n实现上最难的一块：MV3 不能录屏 Manifest V3 的 Service Worker 没有 DOM，而 MediaRecorder 需要 DOM 环境。这是整个项目最硬的约束，直接决定了架构。\n解法是 offscreen document。 MV3 提供了一个专门的 chrome.offscreen API 来创建隐藏文档，它是有 DOM 的。录制引擎整个跑在 offscreen 文档里，直接调 getDisplayMedia() 弹出 Chrome 选择器并编码。\n顺带说一句，这里没有用 chrome.desktopCapture —— 它返回的 streamId 被限制在目标标签页内，offscreen 文档消费不了。\n其他几处不得不绕的地方：\ncaptureVisibleTab 有每秒 2 次的配额限制（MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND）。长截图逐屏捕获必然撞上，所以引擎里内置了 ≥620ms 的节流和配额重试。\noffscreen 文档每次录完就销毁。 因为它的媒体权限状态在创建时就定下来了，无界面也弹不了授权框。录制结束即销毁，用户授权之后下一次录制会自动用上新权限。\n页面内的 UI 全部放在 closed Shadow DOM 里（气泡、工具条、框选遮罩），配 adoptedStyleSheets。这样不受页面样式和 CSP 影响，也不会污染页面。截图那一瞬间自动隐藏。\n大图通过 IndexedDB 在 Service Worker / 弹窗 / 编辑器之间传递。 SW 没有 DOM 也没有 URL 可以挂 blob，拼接用 OffscreenCanvas，下载走 data URL，几百 KB 以上的图走 IndexedDB。\n最小权限。 只申请 activeTab（用户点击或按快捷键时临时授权当前标签页），不申请 \u0026lt;all_urls\u0026gt; 常驻权限。\n出问题的时候能自己看日志 截图或录制失败时最烦的是不知道哪一环断了。所以做了个诊断日志页，把全链路每一步都记下来：选源 → 注入页面 → 逐帧捕获 → 拼接/编码 → 保存。\n环境信息（扩展版本 / 浏览器 / 系统 / 屏幕）\u0026#43; 最近 300 条运行日志，可按级别筛选、一键复制全部、清空 日志只存在本机 chrome.storage.local，不上传。入口在失败时弹窗底部出现的「查看日志」链接，或者设置页 →「诊断」。\n多个上下文（SW / content script / offscreen / 编辑器）的日志会汇聚到 Service Worker 再统一落盘 —— 不然前端出的问题和后台出的问题分散在不同地方，排查起来更费劲。\n安装 打开 chrome://extensions/ 打开右上角开发者模式 点加载已解压的扩展程序，选仓库根目录 把图标固定到工具栏 改快捷键要去 chrome://extensions/shortcuts —— Chrome 不允许扩展直接改键位。设置页里会列出当前生效的键位并给直达按钮。录制命令没有默认快捷键，因为 Chrome 限制 commands 最多 4 个默认键，已经有 4 个了。\n已知限制 chrome://、网上应用店这类浏览器内置页面无法截图，会明确提示而不是静默失败。 file:// 页面需要在扩展详情页开启「允许访问文件网址」。 页面里随时间变化的区域（视频、动画、广告轮播）在长截图拼接结果里可能出现差异 —— 这是拼接的固有属性，不是 bug。 声音：macOS 的「整个屏幕 / 窗口」源无系统声音，要录网页声音请选「Chrome 标签页」。 录制时窗口最小化会导致捕获失败。 小结 这个扩展技术含量最高的是绕限制的部分：MV3 不给的东西（DOM、录屏、麦克风声明权限）都得靠 offscreen document、Web Audio 混音、以及「在可见页面上先拿授权」这几招拼出来。功能本身反而都好写。\n做的过程中我记了一条规则，现在写别的扩展也在用：不要让用户面对一次「什么都没发生」。录到没声音就明说是默音并给重录入口，截图失败就给出具体是哪一环 —— 静默失败是最耗信任的一种 bug。\n代码在 github.com/iamnicwu/nForce-screenCapture 。\n","permalink":"https://blog.nickwu.fun/post/nforce-screencapture/","summary":"五种截图模式（可视区 / 整页长截图 / 手动滚动拼接 / 框选区域）+ 带旁白的 MP4 屏幕录制，内置标注编辑器，还用 offscreen document 绕开了 MV3 Service Worker 不能录屏的限制。","title":"nForce Screen Capture：截图和录屏，一个扩展全包了"},{"content":"待办工具我用过不少，最后都没坚持下来。原因基本是两个：记的时候要跳出去，以及所有数据只活在一个地方。第一个让记录这件事总是拖到最后，第二个让工具一旦出问题就是灾难。\nnForce Todo 是照着这两个坑做的。\n点工具栏图标就是弹窗：上面是输入框和今天清单，右上角可以切侧边栏和设置 弹窗是最高频的入口。记完立刻看到今天还剩几件。\n四条记录路径 按「离手边多近」排，不要求你先打开某个应用：\n方式 怎么操作 什么时候用 全局快捷键 任意页面按 ⌘/Ctrl+Shift+Y 正在别的窗口干活，不想切 右键存选中文字 选中文字 → 右键 → 把选中的文字存为任务 从网页上抄一件事，顺便带上出处 右键存整个网页 空白处右键 → 把当前网页存为任务 整篇网页就是一个待办 弹窗 点工具栏图标 坐下来集中记几条 右键存下来的任务会带上来源链接，任务行里显示域名，点一下跳回原网页。源地址只接受 http / https，别的协议直接丢掉 —— 它最后会被渲染成 \u0026lt;a href\u0026gt;，所以在入口处就把关，而不是等到渲染时再过滤。\n顺带一提，contextMenus 和 activeTab 这两个权限都不往安装提示里加字。前者只多一个右键菜单，后者只在你真的右键之后给这一次的标签页读取权，平时读不到任何页面。\n一行写完一件事 弹窗和侧边栏的输入框认一套语法，日期、优先级、象限一次设好，不用再点按钮：\n写法 结果 回复客户邮件 明天 !1 明天到期的 P1 整理发票 #委托做 放进「委托做」象限 评审材料 下周一 排到下周一 买咖啡 +3 三天后到期 健身 每天 20:00 每天 20:00 提醒（循环） 晨会 每周一、三、五 09:30 周一/三/五 09:30 支持的写法：\n日期：今天 明天 后天 大后天、周一~周日、下周X、+3、3天后、10月8日、10/8 优先级：!1 ~ !3（也认 !p1，以及输入法打出来的全角 ！1） 象限：#立即做 #计划做 #委托做 #有空做 #收集箱 循环：每天、每周X（可以写多个：每周一、三、五）、每月N号 时刻：HH:mm，24 小时制，分钟必须写（9:00 可以，9 只是个数字） 回车之前就能看见结果 输入框下方实时把解析出来的部分显示成胶囊，所以上面那几条规则不用背 —— 打一遍就懂。\n「回复客户邮件 明天 !1 #立即做」被拆成了 明天 / P1 / 立即做 三个胶囊，同时 P1 按钮自动点亮 三条边界处理得比较克制：\ntoken 必须独立成词。 中文没有词分隔符，今天很忙 是一个词，整句原样当标题；只有 今天 很忙 才排到今天。 看不懂的内容不会被删。 !9 会留在标题里，同时亮一枚黄色胶囊说明优先级只认 !1/!2/!3 —— 既不静默吞掉，也不静默留痕。 整行都是 token 时提示「还差一个标题」，省掉一次「按回车怎么没反应」的困惑。 优先级按钮和语法是同一件事：点 P2 就是把 !2 写进输入框，再点一次清掉。输入框里的文字是唯一的状态 —— 所以按钮亮着的时候，建出来的任务一定就是那个优先级，不可能对不上。\n三块视图 侧边栏里可以切三屏，会记住你上次停在哪一屏。\n今天 —— 按日期收拢的清单。今天的在上，更早没完成的分组在下面，勾选、双击改标题、拖动改象限都在这里。\n四象限 —— 重要紧急那套。按 #立即做 #计划做 #委托做 #有空做 分四格，剩下的进「收集箱」。\n看板 —— 三列对应任务的生命周期：\n列 收哪些任务 待办 所有未开始的（含逾期，不会因为日期过了就消失） 进行中 所有标记为进行中的 已完成 只收今天完成的 「已完成」只收当天是有意的。看板反映的是当前这一轮，把历史上完成过的全部堆进一列，会把刚做完的埋在底下。更早完成的会在列底注明条数，所以不会出现「任务去哪了」的困惑。\n卡片可以拖拽跨列，也可以用卡片上的下拉框移动 —— 拖拽只对有鼠标的人友好，下拉框对键盘和触屏同样可用。\n循环任务与到点提醒 循环与否由每条任务自己决定，不是全局开关：同一天里可以既有「每天喝水」，也有一条只做一次的「交材料」。\n设重复有两条路，但是同一条路 —— 在输入框里直接写 token，或者点下方「重复」面板点选。面板上每一次点击都会改写输入框里的 token，预览随之重新解析。所以面板显示的东西和最终建出来的任务不可能对不上。\n到点之后提醒分三层，都不打断你：\n层 表现 默认 图标徽章 有任务到点时，徽章从「今天还剩几件」收窄成「几件到点了」，颜色从主题青变玫红 开 插件内提醒条 弹窗和侧边栏顶部出现一条，最多列两条，更多的折成「还有 N 项在下面」 开 系统通知 一条原生通知，点开看到是哪件事 关，在设置页里拨开 提醒条故意没有「忽略」按钮：一条还成立的提醒被静音，正是徽章和提醒条开始互相矛盾的方式 —— 前者说 2 件到点，后者被你关掉了。让它消失的办法是完成它。\n系统通知默认关，是因为 notifications 是少数会往安装提示里加一行字的权限。它被放进可选权限，只有你在设置页拨开那个开关的那一刻才申请。不给它，上面两层照常工作。\n打勾一条循环任务 ≠ 完成它 而是原地推进到下一次：\n什么时候打勾 下一次落在 提前打完（09:00 的任务，08:00 就做完） 明天的 09:00 —— 不会把今天的坑位重新武装一遍 到期后拖了几天才打勾 从现在算起的下一次，绝不会算出一个已经过去的时刻 所以循环任务永远停在「待办」列：不进「已完成」、不计入完成率。它是一条一直在的规矩，不是一件做完的事。\n还有一处容易做错的地方：逾期未完成的循环任务不会被「顺延到今天」扫走。那会把昨天错过的坑位变成今天的，提醒时间就指向了规则从没选过的一天。它会留在错过的那个日期上，显示为已到期。\n闹钟是按 chrome.alarms 精确设到那一刻的，不是每分钟轮询 —— MV3 的 worker 空闲 30 秒就被回收，每分钟叫醒一次等于每天重启 1440 次。浏览器关着的时候闹钟不会响，所以下次启动会补一次检查，错过的那条还是会出现在徽章和提醒条里。\n数据存哪：本机 + ima 默认所有任务存在浏览器本地。够用，但有两个隐患：清缓存会丢，换电脑要导出导入。\n所以做了 ima 备份。连上之后，一天一篇 Markdown 笔记，当天的改动按时间往下追加。\nima 的开放接口只能新建笔记和追加内容，不能修改也不能删除 —— 所以不是「同步」而是「追加快照」。当天第一次改动新建一篇，之后每次改动往同一篇末尾追加一节，跨天自动开新的。每一节都是当时任务状态的完整快照，单独看也能还原：\n# TODO 快照 · 2026-10-01 周四 ## 09:12 · 9 项任务 \u0026gt; 共 9 项任务 · 待办 6 项 · 进行中 1 项 · 已完成 2 项 \u0026gt; 今天 5 项，已完成 2 项（40%） ### 今天 · 10月1日 周四 - [ ] 整理本周的项目周报 · P1 - [ ] 准备下周评审会的材料 · 进行中 · P2 - [x] 把设计稿同步给前端 ### 更早未完成 · 3 项 - [ ] 补上昨天的会议纪要 · P1 - [ ] 整理发票报销 · 每天 09:00 内容没变就跳过，网络抖动会自动重试。连接步骤：去 ima 的第三方接入页拿 Client ID 和 API Key，填进扩展设置页，点「授权并测试连接」，然后允许一次 ima.qq.com 的域名授权。\n扩展默认不申请任何网站权限，只有在你点连接的时候才按需申请这一个域名 —— 所以安装时不会出现「读取你在所有网站上的数据」这种吓人的提示。\n深色模式是跟着系统的，也能在标题栏手动切。\n安装 git clone https://github.com/iamnicwu/nForce-Todo.git 然后 chrome://extensions/ → 开发者模式 → 加载已解压的扩展程序 → 选仓库根目录（就是有 manifest.json 的那一层）。\n要 Chrome 114 以上，侧边栏 API 的最低版本。\n这里有个我特意压平的结构：扩展根 == 仓库根，全仓只有一份生效的 manifest.json，没有 src/。因为误选子目录时 Chrome 只会甩一句 Manifest file is missing or unreadable，不告诉你要选哪一层 —— 与其写文档让人猜，不如把目录结构做成只有一个答案。不确定就跑 npm run check，它会直接把该选的路径打出来。\nnpm run build 是可选的打包步骤，产出 dist/ 用于分发。日常开发用仓库根就好，改完点一下扩展页的刷新键，不用构建。\n几个实现上的取舍 零构建也能跑。 源码树本身就是一份可以直接加载的完整扩展，克隆下来就能用。打包只是给分发用的，不是运行的前提。少一个构建步骤，就少一类「为什么我的改动没生效」的问题。\n「必须发生」的状态落库不放 UI 页面里。 这条是踩出来的：一开始把「录制/编辑」类的持久化逻辑写在了页面脚本里，结果页面一关数据就丢。现在凡是必须落盘的事情都在 Service Worker 里做，页面只负责展示和消费。\n备份用追加而不是覆盖。 因为 ima 的接口不支持修改，反而逼出一个更好的设计 —— 快照序列本身就是历史记录，翻上去能看到某一天上午和下午任务状态的变化。\n已知限制 不连 ima 就纯粹是本地数据，换浏览器不会同步。 ima 侧只增不减，笔记会随时间变长；重装或者想清理得去 ima 里手动处理。 系统通知需要单独授权；不给也能用，只是少一层提醒。 浏览器完全关闭时闹钟不会响，靠下次启动补检查。 小结 这大概是四款扩展里我自己每天真正在用的一个 —— 因为它解决的是我自己的问题：记的时候不要有摩擦，存的时候不要只有一个副本。一行语法是为了第一条，ima 备份是为了第二条。\n代码在 github.com/iamnicwu/nForce-Todo 。\n","permalink":"https://blog.nickwu.fun/post/nforce-todo/","summary":"一个 Chrome 待办扩展。四种入口随手记录，一行语法写清日期优先级象限，看板推进，循环任务到点提醒，并把每天的任务快照自动备份进 ima 知识库 —— 清缓存、换电脑、误删都还能找回。","title":"nForce Todo：一行语法记下待办，然后自动备份到 ima"},{"content":"做 Salesforce 相关的工作，一天里很大一块时间花在重复动作上：把一批订单号粘进 SOQL 里查一遍、导个报表看哪个订单卡住了、翻 Data Loader 查一条记录状态。每次都要开新标签、登录、拼查询，来回几次思路就断了。\nnForce Tools 就是冲这件事做的：一个 Chrome 扩展，把那些\u0026quot;顺手要查一下\u0026quot;的操作收到浏览器侧边栏里。数据从浏览器直接打到 Salesforce，中间不经过任何服务器。\n首页是一个图标化的功能中心，按「连接与环境 / LTS / 数据分析 / OTT / PCD / CVP7 / 工具」分组。最近用得多功能会自动排到「常用功能」那一栏里去 连接：两种方式，都不需要配 Connected App 扩展不要求你在 Salesforce 里注册 Connected App、拿 Client ID 走 OAuth —— 那种流程对个人工具来说太重了。它走的是另一条路：\n手动贴 Session ID：从开发者控制台或者浏览器里复制一串 Session ID 粘进去，点「保存 Session ID」再「测试连接」。 自动获取会话：如果你浏览器里本来就在 Salesforce 登录着，扩展可以直接读 Cookie 换一个新会话，不用手动粘。 顶部一直挂着两枚状态：连上没连上、连的是 Production 还是 Sandbox。这两个是分开的，看错环境发错请求的代价比多数人想的高。\n插件偏好里可以锁定 Salesforce API 版本（默认 64.0，可自定义），以及确认侧边栏当前在左边还是右边 对应关系里的第一件事是 API 版本。组织不支持某个版本时请求会返 404，这里能降级重试。\n会话会过期，所以留了一套自愈 Session ID 是短命的。隔一段时间不用，或者 Salesforce 那边主动失效，接口就开始返 401。多数同类工具的处理是弹个报错让你回去重新粘一次，久了很烦。\n这里的做法是分两步：\n先自己救。检测到失效时，扩展会尝试从浏览器 Cookie 里重新换一个会话（cookies 权限）。换到了就静默继续，你甚至感觉不到。 救不了才叫人。Cookie 那条路走不通（比如浏览器里压根没登录态，或者组织禁了 Cookie 取 token），就明确告诉你缺的是什么，并给一个「打开登录页」的按钮 —— 登录完回来点一下「自动获取 Session 并重新连接」就行。 自动修复失败时给的是具体原因和一条可走的路，而不是一句「连接失败」 另外 MV3 的 Service Worker 空闲 30 秒就被回收，所以「启动时补一次检查」是必须的 —— 否则重启浏览器之后第一下操作必然撞在失效会话上。\n功能地图 侧边栏按业务域分组，一共七组。下面是各组里主要做了什么。\n连接与环境 功能 做什么 连接设置 Session ID 录入、连接测试、当前用户与组织信息 Session 管理 会话状态查看、自动重连、登录页快捷入口 LTS LTS（Long Term Sales）相关的订单查询是这个扩展最早做的一块，也是用得最多的。\n功能 做什么 LTS 概览 Fallout Summary 的案例汇总与业务流程图 获取当日数据 一键拉当日 LTS 订单，自动去重后落到表格里 获取报表数据 快速取预定义报表，不用再去 Reports 里点开 获取文件数据 上传含订单号的 Excel，批量查最新状态与明细 VVIP 上传含 Account ID 的 Excel，同时查 PCD 与 LTS 订单并合并 T-4 Outstanding 取 T-4 报表并筛出符合规则的记录 T-4 分析 在 T-4 结果上做规则分析 数据分析 上传一份 Excel，再（可选）传一份规则 JSON，扩展按规则逐行判断并写回状态列。规则是可配的 —— 业务规则变的时候改 JSON，不用改代码重新发版。\nOTT / PCD / CVP7 分组 内容 OTT OTT 数据获取 PCD 当日 PCD 数据、PID Fallout 排查、QC Issue 排查 CVP7 T-4 连通性检测 工具 功能 做什么 Bulk 操作 输入 SOQL，走 Salesforce Bulk API 2.0 建批任务，轮询进度，完成后下载结果 CSV。自动分页取全量，适合几十万行那种一次查询拉不完的量 Execute Anonymous 直接在扩展里提交 Apex 匿名代码块并看执行日志 Schedule Jobs 列出、创建、暂停、恢复、清除定时任务 Debug Log 查看器 读 Apex Debug Log，省掉一层 Developer Console 的加载 中午食乜 上传餐厅列表 Excel，抽签决定今天中午吃什么 最后那个是真事 —— 团队每天中午要纠结十分钟，不如让代码决定。\n首页是可以自己排的 功能多起来之后，平铺一长条列表就很难找。所以首页做成了图标网格，并且可以改：\n每个图标可以固定到「常用功能」，也可以隐藏。 「常用功能」另外会按你的实际使用频率自动排序，手动固定的排在自动排序前面。 布局配置页里能整组拖动、改名、调顺序。 分组顺序、组内图标、常用区都能改。改完直接反映到首页 设置页则只管三件事：连接、布局、项目说明。刻意的 —— 设置页一旦什么都往里塞，就变成了第二个首页。\n顺带做了个 Org Limits 看板 排查问题时经常要确认一件事：到底是逻辑写错了，还是这个组织的 API 配额打满了？Salesforce 把这个信息藏在 Setup 里很深的位置，来回点好几层。\n侧边栏顶部直接放了一个仪表盘，用 ECharts 的 gauge 画的：\nDaily API Requests、Daily Async Apex Executions、Daily Bulk API Requests、Data Storage 四项，超阈值会变红并汇总成顶部一条告警 颜色不是按数值大小简单映射，而是把上面那三档阈值当锚点插值出来的 —— 因为「用了 16%」和「用了 75%」的区别不在数字上，而在还剩多少余量。右上角那个「1 项超额」的汇总条会直接告诉你哪几项越线了，不用自己一格一格看。\n顺带说一句，这个区域的数据来源是 /services/data/vXX.X/limits，只要会话有效就能读，不额外消耗资源。\n安装 扩展没上架 Chrome 商店（内部工具，审核那套流程不划算），走开发者模式加载：\n去 GitHub 仓库 把代码拉下来。 打开 chrome://extensions/，右上角打开开发者模式。 点加载已解压的扩展程序，选仓库根目录。 想要打包产物就用 dist/：npm install \u0026amp;\u0026amp; npm run build，然后把 dist/ 加载进去。源码目录也可以直接加载，改完在扩展页点一下刷新就生效，改逻辑的时候这个比重新构建快得多。\n装好后点工具栏图标 → 「打开应用」，主界面会在新标签页里打开；点侧边栏图标则常驻在右侧。\n技术上的几处选择 从 webpack 搬到了 Vite。 一开始用 webpack，配置越堆越长，改一个 loader 要重启等半天。换成 Vite 之后构建从十几秒降到一两秒，配置也短了很多。顺手做了个 Ant Design CSS 的 purge 步骤（npm run purge:css）—— 只挑了实际用到的几十个类，未压缩的 antd 样式表直接从几百 KB 掉下来。\n数据表格用了 Handsontable。 查询结果动辄几百上千行，得上排序、列宽拖动、筛选、单元格复制。自己拿原生 table 拼这些功能不划算。\nExcel 走 SheetJS，日期走 Day.js。 前者负责解析上传的文件和导出结果，后者负责所有跟日期有关的判断。这两个都不大，直接用比自己写强。\n所有请求都在扩展的 Service Worker 里发。 页面脚本拿不到 Session ID，也就无从泄漏；扩展只对这些域名申请 host 权限：\nhttps://*.salesforce.com/* https://*.force.com/* https://*.salesforce-setup.com/* https://*.visualforce.com/* https://*.cloudforce.com/* ...（含 .cn / .mil / .mcas.ms 等变体） 测试用 Jest。 规则判定、日期解析、Excel 列名识别这些纯逻辑抽出来单测，UI 层不测 —— UI 的正确性靠人看，靠脚本断言 DOM 结构收益不大。\n已知的限制 没上架商店，装的时候有「开发者模式」的心理成本，也没法用商店的自动更新。 会话依赖浏览器登录态。如果组织策略禁止用 Cookie 换 token，就只能手动贴 Session ID。 批量查询走的是实时 API，一次查太多会被 Salesforce 的并发与调用配额拦住。真要拉几十万行请走 Bulk 操作那个模块。 界面基本只做了桌面宽度。Salesforce 本身在手机上也不太好用，这块就没花力气。 小结 这东西没什么技术奇迹，主要是把「每次都要点五层菜单」变成「点一下」。真正花时间的地方不在写代码，在搞清楚业务规则到底怎么判 —— 以及把规则做成可配置的 JSON，让业务变了的时候不用重新发版。\n代码在 github.com/iamnicwu/nForce-tools ，MIT 协议。有需要可以自己 fork 改。\n","permalink":"https://blog.nickwu.fun/post/nforce-tools/","summary":"一款给 Salesforce 用户用的浏览器内数据工作台 —— 连接组织、批量查单、拉报表、跑 Bulk API、盯 Org 用量，全部在本地完成，不经任何中转服务器。","title":"nForce Tools：把 Salesforce 的日常操作收进一个侧边栏"},{"content":"这个博客的第一篇，留在这里当个书签 —— 那会儿还在折腾怎么让静态站生成器的目录结构和我脑子里的分类对上。\n七年过去，中间换过三次引擎。现在的这一版是 Hugo + PaperMod，文章是纯 Markdown，模板在仓库里，./deploy.sh 一跑就上线。\n后来的事都写在关于 里了。\n","permalink":"https://blog.nickwu.fun/post/hello-world/","summary":"\u003cp\u003e这个博客的第一篇，留在这里当个书签 —— 那会儿还在折腾怎么让静态站生成器的目录结构和我脑子里的分类对上。\u003c/p\u003e\n\u003cp\u003e七年过去，中间换过三次引擎。现在的这一版是 Hugo + PaperMod，文章是纯 Markdown，模板在仓库里，\u003ccode\u003e./deploy.sh\u003c/code\u003e 一跑就上线。\u003c/p\u003e\n\u003cp\u003e后来的事都写在\u003ca href=\"/about/\"\u003e关于\u003c/a\u003e\n里了。\u003c/p\u003e","title":"Hello World"},{"content":"你好，我是 Nick。\n这个博客断断续续开了很多年，中间换过几次引擎 —— 从 Gridea 到 Hugo，落到现在这套 Hugo + PaperMod。留下的都是做完东西之后的复盘。\n閒看兒童捉柳花\n这里会有什么 做过的东西。 主要是浏览器扩展和给日常业务用的小工具，写完会把设计取舍和踩过的坑一起记下来。目前最集中的是 nForce 系列的四款 Chrome 扩展 。 技术笔记。 Chrome Manifest V3 那一堆约束、Salesforce 的 API 边界，以及我自己判断失误之后改回来的地方。 偶尔也有些跟技术无关的碎念，不多。 我在意的事 写 JavaScript 为主，打交道最多的是浏览器扩展、前端工程，和一些把重复劳动变自动化的脚本。相信工具应该长在顺手的地方：需要的时候它就在那儿，不需要的时候最好别挡路。上面那几款扩展基本都是这条原则的产物。\n放到写代码上也一样 —— 不要让用户面对一次「什么都没发生」。失败要说清楚是哪一环断了，这是我做工具时反复回到的一条线。\n关于留言 早先接过 Gitalk，后来嫌它要 GitHub 授权、还会把 clientSecret 暴露在前端，就去掉了。想聊的话发邮件，或者直接来 GitHub 开 issue，都比在评论区里等加载靠谱。\n找到我 GitHub：iamnicwu RSS：订阅本站 站内搜索：直接搜 ，Alt + / 也能唤起 ","permalink":"https://blog.nickwu.fun/about/","summary":"\u003cp\u003e你好，我是 Nick。\u003c/p\u003e\n\u003cp\u003e这个博客断断续续开了很多年，中间换过几次引擎 —— 从 Gridea 到 Hugo，落到现在这套 Hugo + PaperMod。留下的都是做完东西之后的复盘。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e閒看兒童捉柳花\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"这里会有什么\"\u003e这里会有什么\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e做过的东西。\u003c/strong\u003e 主要是浏览器扩展和给日常业务用的小工具，写完会把设计取舍和踩过的坑一起记下来。目前最集中的是 \u003ca href=\"/projects/\"\u003enForce 系列的四款 Chrome 扩展\u003c/a\u003e\n。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术笔记。\u003c/strong\u003e Chrome Manifest V3 那一堆约束、Salesforce 的 API 边界，以及我自己判断失误之后改回来的地方。\u003c/li\u003e\n\u003cli\u003e偶尔也有些跟技术无关的碎念，不多。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"我在意的事\"\u003e我在意的事\u003c/h2\u003e\n\u003cp\u003e写 JavaScript 为主，打交道最多的是浏览器扩展、前端工程，和一些把重复劳动变自动化的脚本。相信工具应该长在顺手的地方：需要的时候它就在那儿，不需要的时候最好别挡路。上面那几款扩展基本都是这条原则的产物。\u003c/p\u003e\n\u003cp\u003e放到写代码上也一样 —— \u003cstrong\u003e不要让用户面对一次「什么都没发生」\u003c/strong\u003e。失败要说清楚是哪一环断了，这是我做工具时反复回到的一条线。\u003c/p\u003e\n\u003ch2 id=\"关于留言\"\u003e关于留言\u003c/h2\u003e\n\u003cp\u003e早先接过 Gitalk，后来嫌它要 GitHub 授权、还会把 clientSecret 暴露在前端，就去掉了。想聊的话发邮件，或者直接来 GitHub 开 issue，都比在评论区里等加载靠谱。\u003c/p\u003e\n\u003ch2 id=\"找到我\"\u003e找到我\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eGitHub：\u003ca href=\"https://github.com/iamnicwu\" target=\"_blank\" rel=\"noopener noreferrer\"\u003eiamnicwu\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003eRSS：\u003ca href=\"/index.xml\"\u003e订阅本站\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e站内搜索：\u003ca href=\"/search/\"\u003e直接搜\u003c/a\u003e\n，\u003ccode\u003eAlt + /\u003c/code\u003e 也能唤起\u003c/li\u003e\n\u003c/ul\u003e","title":"关于"},{"content":"这些都是围绕「在浏览器里把活干完」这条线做出来的东西。每一款都配了一篇写了取舍和踩坑过程的介绍，代码在 GitHub 上。\nnForce Toolsv4.4.0 给 Salesforce 用户的一套浏览器内数据工作台：连接组织、批量查单、拉报表、跑 Bulk API，全在本地完成。\n读介绍 源码 nForce Todov1.1.0 一行语法记下待办，看板与四象限推进，循环任务到点提醒，并把每天的任务快照自动备份进 ima 知识库。\n读介绍 源码 nForce Screen Capturev1.0.0 截图与录屏二合一：可视区、整页长截图、手动滚动拼接、框选区域，以及带旁白的 MP4 演示录制。\n读介绍 源码 nForce AutoTestv1.0.0 轻量 Web 自动化测试扩展：手写用例或录制成用例，跑完看结果汇总，还能导出成可直接跑的 Playwright 脚本。\n读介绍 源码 四款扩展的共同点 它们共用同一套品牌渐变（#06B6D4 → #3B82F6 → #7C3AED），也都是 Manifest V3、零依赖或极少依赖的路线。共同的设计原则只有两条：\n能在浏览器里做完的事，不要多开一个软件。 四款扩展都长在工具栏和侧边栏上，需要的时候就在那儿。 不要让用户面对一次「什么都没发生」。 失败要说清楚是哪一环断了 —— 截图失败给诊断日志，录制没声音就明说是默音并给重录入口，会话失效就说明是缺登录态还是缺权限。 全部文章按标签浏览：见 标签页。\n","permalink":"https://blog.nickwu.fun/projects/","summary":"写过的 Chrome 扩展与内部工具","title":"项目"}]