ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

t3code:打造本地优先的命令行代码片段管理工具

t3code:打造本地优先的命令行代码片段管理工具 做开发这些年我电脑里散落着大量“自己写过但后来找不到”的代码片段。有些是好不容易调通的 nginx 反代配置有些是压在收藏夹深处的 curl 命令还有些讲不清来源但每次查都要重新搜的 shell 套路。直到最近整理工具链我动手写了 t3code 这个命令行工具来根治这个问题。简单说t3code 是一个本地优先的代码片段管理 CLI在终端里一句话把代码收进去打好标签几秒钟后用关键词捞出来直接复制或者通过管道喂给下一个命令。它适合像我这样整天泡在终端里的开发者也适合那些不想开浏览器、不想被弹窗打扰、只希望代码干干净净待在本地的人。t3 这个前缀是我给自己一系列小工具的代号做到第三个顺手就沿用了这个名字。项目跑了一个多月已经稳定支撑我日常的开发习惯几个同事也在试用反馈都不错。这篇就来完整拆一下 t3code 的设计思路、核心实现和踩过的问题包括关键代码和排障过程给同样想折腾个人工具链的朋友一个参考。1. t3code 是什么从复制粘贴到检索式复用1.1 先说痛点那些“找代码”的尴尬时刻先举一个真实例子。上周给一个服务补反代配置我记得 Nginx 里有一段同时去掉旧缓存头的写法非常确定自己以前写过可就是翻不出来。先翻项目提交记录再翻在线代码托管平台的 secret gist最后靠全盘 grep 才捞出来前后花了七八分钟。这种经历多几次你就会意识到代码片段本质上是一个“二次消费”的知识库存的时候花十秒找的时候如果不能一秒命中那这个知识库就没有意义。除了配置类片段还有另一类高频对象一条 ffmpeg 转码命令、一个 docker-compose 环境变量集合、一组 git 别名组合。它们不是某个项目专属的代码写在 README 里很容易过时放在脚本里又没人维护时间一长就成了记忆负担。过去我试过很多方案有在仓库里建 markdown 清单的有用在线代码托管平台存片的也有扔进通用笔记软件里的结果要么搜索太弱要么复制路径太长要么必须要联网。t3code 的出发点很朴素所有片段住进本地一个 SQLite 数据库文件用命令行完成从采集到取用的闭环。1.2 和现成工具对比为什么还要再造一个轮子动手之前我认真对比过四类主流方案结论不是它们不好而是它们都缺同一块拼图“终端原生 本地离线 结构化数据”。方案优点短板VS Code 用户片段和编辑器无缝衔接存储散落在 settings 文件里CLI 不可控规模一大很难维护GitHub Gist分享方便、有版本记录必须联网网页交互要鼠标点好几下通用笔记软件多端同步、支持富文本不区分代码语言复制要动右键菜单没法直接在管道里用自己的 markdown 库可以纳入版本管理检索基本靠 grep标签体系和语言标记全靠自觉t3code 走的是更“程序员味”的路线数据进 SQLite提供常规子命令导出就是 JSON 文件恢复只是复制一个数据库文件。它不绑定某个编辑器也不要求联网你在任何一台有自己配置的机器上都可以用同一套操作逻辑。这算不算重复造轮子我的判断是只要现有轮子没有刚好符合你的使用路径这个轮子就值得造——何况它造得还很轻。2. 设计思路CLI 优先、离线存储和可脚本化2.1 为什么是 CLI而不是 GUI 或编辑器插件这个问题我纠结过。如果做成 VS Code 插件看起来确实更“集成”但仔细想想取代码的高频场景大多发生在终端里你正在敲一个 shell 命令或者蹲在日志文件前排查问题这时突然需要一段正则最自然的行为是在当前终端里把东西找出来而不是切到另一个窗口去点插件面板。CLI 最大的优势是可组合性。比如想直接执行某条网络请求片段可以写成t3 use --id 12 | sh机器重装后想批量恢复所有常用命令也可以写个循环脚本逐条调用。GUI 和插件默认用户是“人肉点击”不会为程序预留接口所以它们天生给不出这种管道能力。我给自己定了几条原则默认输出人类可读但所有命令保留--json输出每个子命令都能被管道消费数据只写一次允许跨设备同步但不被任何云服务绑架。这几条原则直接决定后续所有代码结构。2.2 技术栈选型Node.js TypeScript better-sqlite3选型是基于“个人工具”的务实判断。项目体量不大不需要追求极致性能我更在乎开发效率和跨平台安装便利。Node.js 几十毫秒的启动时间对我完全可接受而且 npm 生态里有现成的commander、clipboardy、picocolors我只需要把精力放在业务逻辑上。数据库选型上我一开始想过直接用 JSON 文件理由是简单、可读、合并冲突也好处理。但很快放弃了因为 JSON 文件在并发写入时容易损坏Append 和重写都要自己处理事务每加一个字段还得手写迁移。SQLite 把这些问题全部解决了单文件仍然是文件完整复制就能完成备份底层事务保证原子性更重要的是它自带 FTS5 全文搜索这比自己维护内存索引靠谱得多。如果你更喜欢静态二进制、不依赖 Node 运行时后面完全可以换 Go 或 Rust 重写核心数据结构无非就是几条SELECT迁移成本很低。但作为第一版TypeScript better-sqlite3 是投入产出比最稳的组合。2.3 数据模型主表、标签表和一张“反直觉”的关系表存储路径我定为~/.t3code/data.db不存在各种隐藏缓存文件这个目录就是全部家当。代码片段的核心模型简单清晰CREATE TABLE IF NOT EXISTS fragments ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, title TEXT NOT NULL, content TEXT NOT NULL, language TEXT NOT NULL DEFAULT text, created_at TEXT NOT NULL DEFAULT (datetime(now)), updated_at TEXT NOT NULL DEFAULT (datetime(now)) ); CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE ); CREATE TABLE IF NOT EXISTS fragment_tags ( fragment_id INTEGER NOT NULL REFERENCES fragments(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (fragment_id, tag_id) );为什么标签要单独两张表而不是在 fragments 里放一个逗号分隔字段因为“给我所有打了 nginx 标签的片段”这种筛选会经常出现。逗号分隔在 SQL 里只能用LIKE扫全表数据量上千就开始吃力。多对多关系表看起来“重”但对本地工具来说语义清晰比省两张表重要得多。这里有个容易被忽略的字段slug。它的作用类似 URL 里的短标识例如nginx-cache-off。一开始只用自增 id 也能跑但 id 在数据库迁移和跨设备恢复时可能变化而 slug 是稳定的业务命名方便我在别名和快捷脚本里引用某条固定片段。2.4 子命令编排六个动作一套习惯命令设计完全参考 Git 风格第一版只做了六个子命令命令作用示例t3 add新增片段从交互或管道读入echo alias ggit | t3 add --title git命令 --tag shellt3 list列出片段支持标签过滤t3 list --tag nginxt3 find全文搜索高亮命中词t3 find 缓存t3 use复制片段或输出到 stdoutt3 use --id 3t3 edit修改标题、标签或内容t3 edit --id 3 --title 新标题t3 rm删除片段t3 rm --id 3我没有做很复杂的配置系统一切以“少犹豫”为原则。每条命令不带参数时直接提示最核心的用法新用户不需要花时间读 README。命令数量克制是有意的因为每多一个子命令就多一份心智负担个人工具尤其不能做成一言难尽的瑞士军刀。2.5 为脚本而生的--json输出这个设计值得单独讲一下。很多人做 CLI 工具只关注“人能不能看懂”忽略了“程序能不能消费”。t3code 在设计阶段就把--json作为一等公民。默认输出是高亮的人类可读文本但只要加一个--json所有内容都会变成结构化数据。[ { id: 12, slug: curl-headers, title: 探测HTTP头, language: bash, tags: [curl, http], content: curl -I https://example.com } ]有了这个输出我就能在 shell 里做各种组合操作比如只提取所有 curl 相关片段的内容t3 list --tag curl --json | jq -r .[].content这个抽象层才是 t3code 能融入整个命令行工作流的关键。以后不管接编辑器、接自动化脚本还是写统计工具都不用再改服务端逻辑。3. 核心实现add、find、use 的完整搭建过程3.1 工程初始化和依赖安装先把工程结构搭起来。我用 npm init 建了一个 TypeScript 项目目录如下t3code/ ├── bin/ │ └── t3.js # 可执行入口 ├── src/ │ ├── db.ts # 数据库初始化 │ ├── commands/ │ │ ├── add.ts │ │ ├── find.ts │ │ ├── use.ts │ │ └── list.ts │ └── utils.ts # 剪贴板、输出高亮等 ├── package.json └── tsconfig.json依赖只装了四个npm install commander better-sqlite3 clipboardy picocolors npm install -D typescript tsx types/better-sqlite3commander负责子命令和参数解析better-sqlite3是同步 API 的 SQLite 驱动clipboardy让剪贴板操作跨平台保持一致picocolors给终端输出上色。这几个库足够成熟API 稳定不会频繁改动破坏我的代码。TypeScript 在这种小项目里最大的价值不是类型安全而是给函数命名和参数传递加了一层隐形的文档长期维护时非常有用。3.2 add接受 stdin也能打开编辑器add的核心是“尽可能少打断输入”。如果我在写管道当然不希望弹出一个编辑器但如果临时想贴一段长代码直接往命令行里黏又太反人类。所以我的设计是默认从 stdin 读内容支持-m/--message直接传短内容也支持-e/--editor打开编辑器输入。// src/commands/add.ts 核心逻辑 import { Command } from commander import db from ../db function slugify(title: string): string { return title .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]/g, -) .replace(/(^-|-$)/g, ) } async function readInput(options: { message?: string; editor?: boolean }): Promisestring { if (options.message) return options.message if (options.editor) { const { openInEditor } await import(../utils) return openInEditor() } const chunks: Buffer[] [] for await (const chunk of process.stdin) chunks.push(chunk) return Buffer.concat(chunks).toString(utf-8).trim() }实际使用时一句话就能完成采集echo curl -I example.com | t3 add --title 探测HTTP头 --tag curl如果不传标题我会取内容的前 40 个字符并去掉空白作为标题再据此生成 slug。slug 冲突时自动追加序号避免唯一约束直接报错。这套处理放在线代码托管平台上确实不够严谨但对个人使用场景已经非常稳。3.3 findLIKE 起步、FTS5 进阶、中文单独打补丁搜索是 t3code 的灵魂这部分我先后写了两版。第一版直接用content LIKE %关键词%几百条数据时体验很好但用几周膨胀到几千条后明显变慢而且没法按相关度排序。第二版我引入了 SQLite FTS5 虚拟表。-- 创建 FTS5 外部内容表 CREATE VIRTUAL TABLE IF NOT EXISTS fragments_fts USING fts5( title, content, language, contentfragments, content_rowidid ); INSERT INTO fragments_fts(rowid, title, content, language) SELECT id, title, content, language FROM fragments;FTS5 的MATCH查询比LIKE快很多还支持snippet()输出命中摘要。搜索函数的实现可以说是在这虚拟表上做的function search(query: string, limit 20) { const sql SELECT f.id, f.slug, f.title, f.language, snippet(fragments_fts, 1, [, ], …, 8) AS snippet FROM fragments_fts JOIN fragments f ON f.id fragments_fts.rowid WHERE fragments_fts MATCH query ORDER BY rank LIMIT limit try { return db.prepare(sql).all({ query: tokenizeQuery(query), limit }) } catch { return fallbackLikeSearch(query, limit) } }这里提到了tokenizeQuery因为中文场景必须单独打补丁下一章会详细说。3.4 use复制、stdout 和管道三合一use命令承担“拿得出”这最后一步。第一版我只做一件事把片段内容写入剪贴板。但很快发现这有问题——剪贴板在远程服务器上经常不可用而且我常常想要的是“直接插入当前命令行”而不是切出去粘贴。于是我把默认行为改成输出到 stdout只有显式加--clip才写剪贴板。这样一来管道玩法就活了t3 find docker restart --json | jq -r .[0].content | sh为了减少记忆负担use同时接受--id和--slug两种定位方式前者适合临时交互后者适合写进脚本稳定引用。我在真实使用中还会配合 fzf 做一次模糊选择t3 use --id $(t3 list --json | fzf | jq -r .id)这条命令组合让 t3code 不只是一个剪贴板管理器而是整个命令行流水线里的一个普通节点想插哪里插哪里。3.5 list 和 edit剩下的两个常用命令保持克制list的实现很直白支持--tag过滤、--json输出、--limit和--offset分页。默认格式是两列前面显示 id 和标题后面显示标签和语言再按更新时间倒序排列。因为加入了--jsonlist 其实已经覆盖了“导出全部数据”的能力后面对接统计分析都不用额外开发。edit命令我保持了最小必要功能没有做成一个全屏 TUI。修改标题或标签用参数直接更新修改内容则通过$EDITOR打开一个临时文件保存后回写数据库。这样设计的好处是维护成本极低而且和系统里已有的编辑习惯保持一致不需要用户再学一套新的交互。命令行工具最容易犯的错就是每个命令都用力过猛最后把人挡在复杂度外面。4. 真实使用中的问题与排查技巧4.1 Linux 下剪贴板失效一段 xclip 的教训第一版在我自己的工作电脑上剪贴板正常但放到最小化 Ubuntu 服务器上后t3 use --clip直接报了错clipboardy: Couldnt find the required executable xclip。这个错误很典型clipboardy 在 Linux 下依赖xclip或者xsel而纯命令行服务器往往什么都没装。排查时我先确认了which xclip答案为空装好以后问题看似解决了。但这里还藏着第二个坑如果 SSH 会话没有图形会话xclip 依然会失败。最终的稳妥做法是让use永远优先支持 stdout 输出剪贴板只作为可选增强。服务器上我一般直接管道到目标命令不再依赖剪贴板。这次踩坑加速我把“默认 stdout”的决策落地算是一次意外的收获。4.2 中文搜索不准从 SQL 调试到分词修复前面已经提到 FTS5 对中文不友好这里补充我第一次定位问题的过程。搜“数组”返回不到预期结果我就在find命令里临时加了--debug输出实际执行的 SQL。看到 SQL 后发现MATCH查询成了content MATCH 数组去重而索引库里的 token 是数组去重这一个整体 token自然对不上。FTS5 默认的 unicode61 分词器会把连续汉字当成一个完整的 token中文检索必须自己处理。我的解决方案是查询前做一次“中英文混合分词”英文、数字、符号保留原样中文按单字拆分中间补空格。function tokenizeQuery(text: string): string { return text .replace(/[\u4e00-\u9fa5]/g, (ch) ${ch} ) .replace(/\s/g, ) .trim() }这样“数组去重”会转换成“数 组 去 重”匹配命中率大幅提升。代价是牺牲部分短语语义但对片段检索场景完全够用。另外我又加了一层回退如果查询含中文且 FTS 结果为空自动再走一次LIKE查询作为保底。两者结合后实测“缓存”“数组”“配置”这类高频中文词都能秒出结果。4.3 备份、恢复和高危的 FTS 索引重建t3code 没使用远程数据库备份逻辑因此简单到令人发指一个cp命令就能搞定cp ~/.t3code/data.db ~/.t3code/backup-$(date %Y%m%d).db为了不让备份目录失控我加了一个t3 backup子命令每次备份只保留最近七份多余文件自动清理。恢复时只需要把备份文件复制回原位置然后重新跑任意一条 t3 命令即可。这里有一个很容易被忽略的细节因为fragments_fts是 external content 表如果直接替换数据库文件FTS 索引和主表之间可能出现不一致。恢复完成后的第一件事应该是重建索引INSERT INTO fragments_fts(fragments_fts, rowid, title, content, language) VALUES(rebuild, NULL, NULL, NULL, NULL);不做这一步后面搜索会莫名其妙漏数据。我是从一次“备份后搜索变少”的怪现象里总结出来的教训现在凡是涉及整库替换的操作都会自动附带一次 rebuild。4.4 编辑器与 Shell 组合把 t3code 变成贴身快捷键真实工作中我已经用 t3code 替代了一部分快捷键记忆。在 Vim 里把常用命令存成片段需要时执行:r !t3 use --id 3直接把内容拉到光标处在 VS Code 里通过 Task 配置了一个“插入常用片段”的快捷任务。这套组合比任何 GUI 插件都顺手因为完全不依赖某个编辑器的特定 API换任何编辑器都能套用。更有意思的是给它配一个 zsh 函数快速从终端选中文字入库t3add() { local content$(cat) t3 add --title $1 --tag $2 $content }这样在终端里选中一段文本再调用函数就能直接入库不需要切换窗口也不会打断当前命令的执行流。我建议所有想折腾个人工具的人先花半小时把这些快捷键体系串起来工具本身的价值会上一个台阶。4.5 Windows 和 PowerShell 下的兼容性处理虽然我日常主要在 macOS 和 Linux 上跑但还是顺手做了 Windows 兼容。第一优先级是路径~/.t3code/data.db在 Windows 下会被解析成C:\Users\用户名\.t3code\data.dbNode.js 会自己处理好路径分隔符问题不大。第二优先级是剪贴板clipboardy在 Windows 下调用系统 API不需要额外安装二进制。第三优先级是 PowerShell 管道PowerShell 的文本编码默认可能是 UTF-16LE直接管道进 t3code 会出现中文乱码需要在脚本里显式指定$OutputEncoding [System.Text.Encoding]::UTF8。还有一个经验PowerShell 接收 JSON 管道很别扭我通常建议 Windows 用户安装jq的 Win 版本或者直接用ConvertFrom-Json。这个兼容性改造花的时间不多但让 t3code 的适用面从“个人 Mac 工具”扩成了“开发环境通用工具”。5. 迭代记录与后续扩展5.1 从 v0.1 到 v0.3三个版本分别改了什么复盘一下这个项目的演进对后来者很有参考价值。v0.1 只有add、find、use三条命令数据直接用 JSON 文件存代码一共不到三百行投入使用后第一周就发现了两个问题并发写容易丢内容搜索速度不理想。于是 v0.2 把存储层迁到 SQLite顺手加了标签系统和list、edit、rm命令才凑齐了六件套。v0.3 引入了 FTS5 全文搜索同时修复了中文分词和 FTS 索引重建问题。这轮迭代最大的收获是不要在第一版就想清楚所有功能把核心链路跑通让真实使用来暴露问题。我在 v0.1 时花费大量时间纠结要不要做同步、要不要做分享结果这些功能在核心体验稳定之前根本没有存在的必要。5.2 我正在考虑但还没做的功能目前最想加的是t3 run子命令把内容以脚本方式直接执行省掉显式的临时文件管理其次是 markdown 批量导出把整个片段库变成一份技术文档团队内沉淀规则和模板时很实用。多设备同步我坚持不做云端而是让用户把~/.t3code整个目录纳入自己的文件同步工具单文件 SQLite 天然适合这种同步方式。结合一个月的高频使用我最满意的其实不是功能多而是它“安安静静待在那里”。大多数工具都在跟你要注意力t3code 反而在你最需要的时候几秒钟就把东西送到手边。如果你也想搭一个类似的个人工具我最后的建议是先把add、find、use三条主链路跑顺其他功能都会在后面慢慢长出来。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进