1. 为什么我要在 VSCode 里自动维护注释修改时间
团队协作里有个很烦的场景:文件头注释写着@Last Modified: 2024-01-01 10:00:00,结果代码改了七八轮,时间戳还停在去年。Code Review 时看到这个时间,根本判断不出这个文件最近有没有被动过。手动改吧,改完 A 文件忘了 B 文件,一个模块十几个文件,改到最后自己都记不清哪个更新了哪个没更新。
我试过用 Git hook 在 commit 时批量刷时间戳,但问题是:注释里的时间戳一改,Git 就认为文件变了,容易和真实业务改动混在一起,diff 看起来特别乱。而且有些文件只是格式化了一下,并不想触发时间戳更新。
所以更合理的做法是:只在注释内容真正发生变化时,才更新时间戳。这就需要插件能识别「注释块指纹」——把时间戳行排除掉,对剩余内容做哈希,哈希变了才说明注释真的改了。这个逻辑放在 VSCode 插件里做最合适,因为保存事件(onWillSaveTextDocument)能拿到文档全文,还能在保存前插入TextEdit,用户几乎无感知。
这篇文章要解决的核心问题就三个:固定格式注释怎么用正则精确匹配、时间戳行怎么在「有」和「没有」两种情况下分别处理、以及怎么在本地工作区快速验证插件真的生效了。适合正在写 VSCode 插件、或者想给自己项目加一套注释规范自动化的同学。下面所有代码都可以直接复制到你的插件工程里跑。
2. TaoToken 在插件开发调试链路里的位置
写插件时经常需要让模型帮忙补全正则、解释TextEdit的 Range 计算、或者排查onWillSaveTextDocument为什么没触发。这些零散的问答如果每次都去翻文档,效率很低。我的做法是把 TaoToken 当成一个统一的模型入口,在 VSCode 里通过插件或命令行调用,专门处理这类「边写边问」的场景。
TaoToken 本身是一个模型调用网关,你拿到 API Key 之后,可以用它来调用不同的模型。对插件开发来说,最实用的两个入口是:
- 模型对话:用来快速验证正则表达式、解释 VSCode API 行为。比如你把一段注释文本贴进去,问「这个正则能不能匹配到 @memo 后面的内容」,比自己在控制台反复试快很多。
- Coding Plan:如果你在插件里集成了 Agent 能力(比如自动补全注释模板、批量重构注释格式),可以用它来跑长期的编码任务。
需要先说明的是,TaoToken 不是替代 VSCode 编辑器的工具,它只是模型调用的通道。你的插件逻辑、文件读写、保存事件监听,全部还是在 VSCode 本地完成的。TaoToken 负责的是「当你需要模型能力时,提供一个稳定的调用地址」。
接入前你需要准备三样东西:Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api,API Key 在控制台的 API Keys 页面生成,Model ID 根据你实际要调的模型填。这三件套在后面的配置片段里会具体写。
如果你还没生成 Key,可以先到 TaoToken API Keys 页面创建一个。创建时注意权限范围,插件调试场景只需要基础的对话权限就够了,不需要开太高的配额。
3. 可复制的插件配置与时间戳正则规则
这一节是全文的核心,直接给你能跑的代码。整个插件分三块:package.json里的配置项声明、extension.js里的核心逻辑、以及时间戳正则的匹配规则。
3.1 package.json 配置片段
先看配置声明。这段决定了用户在 VSCode 设置里能看到哪些选项:
{ "name": "autoupdatetime", "displayName": "autoUpdateTime", "description": "update time auto when change comment", "version": "0.0.1", "engines": { "vscode": "^1.90.2" }, "categories": ["Other"], "activationEvents": ["onStartupFinished"], "main": "./extension.js", "contributes": { "configuration": { "title": "Auto Comment Updater", "properties": { "commentUpdater.enable": { "type": "boolean", "default": true, "description": "Enable/disable automatic comment updating" }, "commentUpdater.timeFormat": { "type": "string", "default": "YYYY-MM-DD HH:mm:ss", "description": "Time format (using moment.js format)" }, "commentUpdater.tagName": { "type": "string", "default": "@Last Modified", "description": "Tag name for the timestamp line" }, "commentUpdater.showNotification": { "type": "boolean", "default": true, "description": "Show notification when timestamp is updated" } } }, "commands": [ { "command": "commentUpdater.forceUpdate", "title": "Force Update Comment Timestamps" } ] }, "dependencies": { "crypto-js": "^4.2.0", "moment": "^2.30.1" } }注意activationEvents我改成了onStartupFinished,这样插件在 VSCode 启动完成后就会激活,不需要等用户打开特定文件。如果你希望更省资源,也可以改成onLanguage:javascript之类的按语言激活。
3.2 时间戳正则匹配规则
这是整个插件最容易出错的地方。固定格式注释长这样:
/** * @auth: 张三 * @fnName: getUserInfo * @image: user-avatar.png * @memo: 获取用户基本信息 * @Last Modified: 2024-01-01 10:00:00 */匹配这个注释块的正则是:
const commentPattern = /\/\*\s*\*\s*@auth:[^\n]+\s*\*\s*@fnName:[^\n]+\s*\*\s*@image:[^\n]+\s*\*\s*@memo:[^\n]+[\s\S]*?\*\//g;拆开看几个关键点:
\/\*\s*\*匹配/**开头,\s*允许中间有空格。@auth:[^\n]+匹配到行尾,[^\n]+保证不会跨行。[\s\S]*?\*\//非贪婪匹配到*/结束,[\s\S]是为了兼容换行符。- 最后的
g标志让exec能循环匹配多个注释块。
时间戳行的匹配和替换用这个:
// 检测是否已有时间戳 const hasTimestamp = /@Last Modified:/.test(fullText); // 替换已有时间戳 commentText.replace(/(@Last Modified: )[\d :-]+/, `$1${currentTime}`); // 在 @memo 行后插入新时间戳 const memoIndex = commentText.indexOf('@memo:'); const memoLineEnd = commentText.indexOf('\n', memoIndex); const indentMatch = commentText.match(/\n(\s*)\*/); const indent = indentMatch ? indentMatch[1] : ' '; const newLine = `\n${indent}* ${tagName}: ${currentTime}`;这里有个坑:indent的提取。如果你的注释块缩进不一致,比如有的文件用 2 空格、有的用 4 空格,indentMatch可能匹配到错误的位置。更稳的做法是取@memo行前面的缩进:
const memoLine = commentText.substring(commentText.lastIndexOf('\n', memoIndex) + 1, memoIndex); const indent = memoLine.match(/^\s*/)[0];3.3 指纹计算与缓存逻辑
指纹的作用是判断注释内容有没有变。计算时要把时间戳行排除掉:
function calculateCommentFingerprint(commentText) { const normalized = commentText .replace(/\n\s*\* @Last Modified: [^\n]+/g, '') .replace(/\s+/g, ' ') .trim(); return CryptoJS.SHA256(normalized).toString(); }replace先把时间戳行删掉,再把连续空白压成一个空格,最后 trim。这样只要@auth、@fnName、@memo这些内容没变,指纹就不变,时间戳就不会被更新。
缓存用Map存,key 是文档 URI,value 是「指纹 -> 注释块」的映射:
const originalCommentStates = new Map(); function cacheOriginalComments(document) { const uri = document.uri.toString(); const text = document.getText(); const comments = extractComments(text); const commentMap = new Map(); for (const comment of comments) { const fingerprint = calculateCommentFingerprint(comment.fullText); commentMap.set(fingerprint, comment); } originalCommentStates.set(uri, commentMap); }保存前对比当前指纹和缓存指纹,不一致就生成TextEdit:
vscode.workspace.onWillSaveTextDocument(event => { const config = vscode.workspace.getConfiguration('commentUpdater'); if (!config.get('enable')) return; event.waitUntil(updateCommentTimestamps(event.document)); });event.waitUntil是关键,它让 VSCode 等你的TextEdit应用完再保存文件。如果你忘了写waitUntil,时间戳改了但不会写进磁盘。
4. 在本地工作区验证注释自动刷新
代码写完了,怎么确认它真的生效?我一般分四步验证。
4.1 启动插件调试宿主
在插件工程根目录按F5,VSCode 会打开一个新的「扩展开发宿主」窗口。这个窗口里加载了你正在开发的插件。如果F5没反应,检查.vscode/launch.json里有没有配extensionHost:
{ "version": "0.2.0", "configurations": [ { "name": "Run Extension", "type": "extensionHost", "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}"] } ] }4.2 准备测试文件
在新窗口里新建一个test.js,写入固定格式注释:
/** * @auth: 测试用户 * @fnName: testFunc * @image: test.png * @memo: 这是一个测试注释 */ function testFunc() { return 1; }注意这里没有@Last Modified行,我们要验证插件能不能自动加上。
4.3 触发保存并观察
按Ctrl+S保存。如果配置正确,你应该看到:
- 状态栏右侧出现
$(watch) Update Timestamp。 - 保存后弹出通知:
Updated 1 comment timestamp(s) in test.js。 - 注释块变成:
/** * @auth: 测试用户 * @fnName: testFunc * @image: test.png * @memo: 这是一个测试注释 * @Last Modified: 2024-01-01 10:00:00 */4.4 验证「内容不变不更新」
再按一次Ctrl+S。这次不应该有任何通知,时间戳也不变。因为指纹没变,插件认为注释内容没改。
然后修改@memo的内容,比如改成「这是一个修改后的测试注释」,再保存。这次时间戳应该更新到当前时间。
如果以上四步都通过,说明插件核心逻辑没问题。接下来可以测多文件场景:同时打开三个文件,分别修改注释,看是否每个文件独立更新。
5. 常见报错与排查对照
5.1 保存后时间戳没变
最常见的原因是onWillSaveTextDocument里忘了event.waitUntil。如果你写的是:
vscode.workspace.onWillSaveTextDocument(event => { updateCommentTimestamps(event.document); // 没有 waitUntil });updateCommentTimestamps返回的是 Promise,但 VSCode 不会等它。改成:
event.waitUntil(updateCommentTimestamps(event.document));另一个可能是commentUpdater.enable被设成了false。在设置里搜commentUpdater.enable确认一下。
5.2 报错Cannot read property 'getConfiguration' of undefined
这个通常是因为vscode模块没正确引入。检查extension.js第一行:
const vscode = require('vscode');如果你用的是 ESM 写法import * as vscode from 'vscode',需要确认package.json里有没有"type": "module",以及 VSCode 版本是否支持。
5.3 时间戳插入位置不对
如果@Last Modified插到了*/后面,说明memoLineEnd计算错了。检查:
const memoIndex = commentText.indexOf('@memo:'); const memoLineEnd = commentText.indexOf('\n', memoIndex);如果@memo是注释块最后一行,memoLineEnd可能指向*/那一行。更稳的做法是找*/的位置,在它前面插入:
const closeIndex = commentText.lastIndexOf('*/'); const before = commentText.substring(0, closeIndex); const after = commentText.substring(closeIndex); const newLine = `${indent}* ${tagName}: ${currentTime}\n`; return before + newLine + after;5.4 多文件时缓存串了
如果你发现 A 文件的时间戳更新影响了 B 文件,检查originalCommentStates的 key 是不是用了document.uri.toString()。有些场景下 URI 会带查询参数,导致同一个文件被当成两个 key。可以用document.uri.fsPath替代。
5.5 接入 TaoToken 时的 401
如果你在插件里集成了 TaoToken 调用,遇到 401 先检查三件套:
{ "baseURL": "https://taotoken.net/api", "apiKey": "sk-xxxxxxxx", "model": "claude-3-5-sonnet" }Base URL 不要带 UTM 参数,API Key 确认没有多余空格,Model ID 要和控制台里显示的一致。如果还是 401,到 TaoToken 控制台 看一下 Key 的状态是不是被禁用了。
6. 把模型能力接进你的插件工作流
插件本身跑通之后,下一步可以考虑把模型能力接进来,处理更复杂的场景。比如:自动生成注释模板、根据函数签名补全@memo、或者批量重构旧注释格式。
接入方式很简单,在插件里加一个命令,调用 TaoToken 的 API:
const response = await fetch('https://taotoken.net/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'claude-3-5-sonnet', messages: [ { role: 'user', content: `请为以下函数生成固定格式注释:\n${functionCode}` } ] }) });拿到返回后,用TextEdit插入到函数上方。这样你的插件就从「只维护时间戳」升级成了「注释全自动维护」。
如果你打算长期在插件里跑 Agent 任务,比如自动扫描整个工作区的注释并批量更新,可以用 Coding Plan 来管理调用配额。它比按次调用更适合这种批量场景。
最后提醒一点:插件里调用模型时,不要把整个文件内容都传上去。只传注释块和函数签名就够了,既省 token 又避免泄露业务逻辑。具体传什么,可以参考 TaoToken 接入文档 里的最佳实践。
整套流程跑下来,你会发现注释时间戳维护这件事,从「每次手动改」变成了「保存时自动处理」。插件逻辑不复杂,关键是正则要写准、指纹要算对、waitUntil不能忘。剩下的就是按你的项目规范调整tagName和timeFormat了。