refactor: 品牌重命名 + maxTokens 支持 + 设置面板简化
- CodeGuard → Code Purifier / 净码特工(displayName、命令、配置标题) - 新增 ai.maxTokens 配置项,所有 Provider 及 fixer 传入 maxTokens - AI 引擎增强:repairJsonEscapes + JSON 解析 fallback + 详细错误信息 - 设置面板规则管理改为文件级(list/delete .yaml),addRule 改为 AI 从 Markdown 生成 YAML - yaml-parser 简化:移除 config.yaml 的 enable/disable 过滤逻辑 - 审查报告面板:errorBanner 优先显示具体错误、lint 诊断显示 suggestion、移除 translatedDiagnostics 独立渲染 - merger 中 translatedDiagnostics 覆盖原始 lint 诊断 message/suggestion - HTML linter 配置项、测试用例重写、typescript-eslint 移入 dependencies
This commit is contained in:
@@ -13,4 +13,4 @@ VS Code 代码审查与规范检查一体化插件。集成 ESLint、Stylelint
|
||||
|
||||
## 配置
|
||||
|
||||
通过 `Ctrl+Shift+P` → `CodeGuard: 打开设置面板` 进行配置,或编辑 `.vscode/settings.json`。
|
||||
通过 `Ctrl+Shift+P` → `净码特工: 打开设置面板` 进行配置,或编辑 `.vscode/settings.json`。
|
||||
|
||||
@@ -58,3 +58,31 @@
|
||||
| 2026-07-16 19:06 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 保存并测试连接时,Base URL 未配置也报错提示,不再静默使用默认值 | 无 | src/views/setupView.ts | deepseek-v4-flash |
|
||||
| 2026-07-16 21:18 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 快速开始三步序号动态高亮:完成=绿色(#3fb950)、有后继步骤完成但本步未完成=黄色(#f0883e)、默认=灰色 | 三态逻辑先在"仅步骤3触发才变黄"和"任意后继触发变黄"间迭代一次 | src/views/setupView.ts src/views/setupView.js | deepseek-v4-flash |
|
||||
| 2026-07-16 21:57 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复 addRule/deleteRule 不同步规则定义文件:addRule 现在在 .code-review/rules/user-rules.yaml 创建模板定义;deleteRule 从 user-rules.yaml 移除规则项 | 无 | src/views/setupView.ts | deepseek-v4-flash |
|
||||
| 2026-07-16 22:36 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 | 品牌命名:CodeGuard → 净码特工 · Code Purifier,更新 package.json/extension.ts/README.md 及所有历史文档 | brainstorm 过渡方案: 码鉴 CodeLens / 智评 AIReview / 码析 CodeSense 均被废弃 | package.json src/extension.ts README.md docs/superpowers/specs/*.md | deepseek-v4-flash |
|
||||
| 2026-07-17 20:21 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 | 设置面板显示所有规则(含禁用):CustomRule 增加 enabled 字段,loadActiveRules 返回全部规则并用 enabled 标记启用状态,设置面板展示全部规则(禁用开关为 off),AI 审查前过滤仅启用规则 | 无 | src/types.ts src/rules/yaml-parser.ts src/activation/commands.ts src/views/setupView.ts src/views/setupView.js | deepseek-v4-flash |
|
||||
| 2026-07-17 20:39 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 设置面板改为文件列表展示(无开关无规则详情):去掉 enable/disable 机制;新增 listRuleFiles();addRule 改为选择 .md 文件 → AI 生成规则 YAML → 写入文件;deleteFile 删除整个 .yaml 文件 | 方案经历 3 轮迭代:①按文件分组+开关 → ②只显示文件名+删除+添加 → ③添加改为 AI 读取.md 生成 | src/types.ts src/rules/yaml-parser.ts src/activation/commands.ts src/views/setupView.ts src/views/setupView.js | deepseek-v4-flash |
|
||||
| 2026-07-17 21:03 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Phase 6.3 测试:7 个测试文件(fixtures/ 3 样本 + 4 测试 suite)+ 修复 webview JS 和 TS 输出碰撞问题 | config.test.ts 按实际 API 调整(getLinterConfig→getLinterForLanguage 等);extension.test.ts 改用 getCommands 绕过 publisher 缺失;发现并修复 setupView.js 被 copy 脚本覆盖 TS 编译产物致扩展无法激活的 bug | src/test/fixtures/sample.js src/test/fixtures/sample.css src/test/fixtures/Sample.java src/test/adapter.test.ts src/test/config.test.ts src/test/merger.test.ts src/test/pipeline.test.ts src/test/extension.test.ts scripts/copy-webview-js.mjs scripts/build.mjs src/views/setupView.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 14:19 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 | 手动测试样本:每种语言带真实 bug 的文件,供用户安装插件后手动验证审查功能 | 无 | src/test/manual/buggy.js src/test/manual/buggy.css src/test/manual/buggy.sql src/test/manual/Buggy.java src/test/manual/buggy.jsp | deepseek-v4-flash |
|
||||
| 2026-07-18 14:51 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | bug fix: stylelint 无配置文件导致静态分析跳过 CSS bug。在 StylelintAdapter 中内置 11 条默认规则,检测到 workingDir 无外部 .stylelintrc 时自动 fallback | 无 | src/adapters/stylelint.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 15:23 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 | 修正内置规则:移除 stylelint v17 无效的 color-hex-case,替换为 color-hex-length + color-named | 先用 color-hex-case('lower')发现无效 → 测试验证后替换 | src/adapters/stylelint.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 15:42 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | bug fix: import('stylelint') 返回 ESM namespace {default:{lint}},直接调 i.lint 为 undefined 抛 TypeError 被静默吞掉;同时修复静态分析错误不显示的问题(error 挂在 degraded 下,AI成功则不显示) | 第1轮只改 stylelint.ts,用户测试仍无问题→第2轮发现 import 未取 .default + panel 缺独立错误横幅 | src/adapters/stylelint.ts src/panel/webview.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 15:51 | ① 用户提出 → ⑤ 编码实现 | AI 审查解析失败时附带原始响应文本方便排查;修复错误横幅重复显示(errorBanner + degradedBanner 合并为单条) | 无 | src/ai/engine.ts src/panel/webview.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 16:20 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 静态分析标签页 linter 消息按 outputLanguage 翻译:在 buildLinterList() 和 reportToMarkdown() 中从 translatedDiagnostics 查找匹配翻译替换原始消息 | 无 | src/panel/webview.ts src/utils/report.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 16:22 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复 ruleId/originalRuleId 不匹配:同时注册带前缀和不带前缀两种 key 到 transMap | 第一次只用 exact match 失效,改为双 key | src/panel/webview.ts src/utils/report.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 16:24 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 重构:翻译逻辑从视图层移到数据层 merger.ts mergeResults 中直接替换 linterDiagnostics message | 视图层 transMap 方案废弃;改用数据层替换 | src/merger/merger.ts src/panel/webview.ts src/utils/report.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 16:36 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 静态分析标签页同步显示 translatedSuggestion:merger.ts 匹配翻译时一并设置 suggestion 字段 | 无 | src/merger/merger.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 16:38 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 静态分析标签页 WebView 渲染 suggestion:buildLinterList 中增加建议行显示 | 无 | src/panel/webview.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 16:40 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | AI 响应解析失败时附带原始响应文本:catch 中传入 e.message 而非固定字符串 | 无 | src/ai/engine.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 16:42 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 修复 AI 返回 JSON 中未转义双引号导致解析失败:prompt 增加转义说明 + repairJsonEscapes 容错后处理 | 无 | src/ai/engine.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 19:17 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: AI 审查标签页不再显示 translatedDiagnostics(静态分析翻译结果重复渲染),只保留 aiFindings | 无 | src/panel/webview.ts | deepseek-v4-flash |
|
||||
| 2026-07-18 19:28 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 诊断 PMD 报"Java 11+ 未安装"原因(缺少 PMD JAR + PmdRunner.java 未编译 + API 不兼容 PMD 7.26.0);下载 PMD 7.26.0 JAR 包 4 个到 jars/pmd/lib/;重写 PmdRunner.java 适配 7.26.0 API(addRuleSet(Path)→addRuleSet(String),setInputFilePath/setReportWriter→config.createRenderer()+pmd.files().addFile());编译生成 PmdRunner.class | 初版编译原 PmdRunner.java 报 API 不兼容(2 个方法不存在),改为 PMD 7.26.0 新版 API | jars/pmd/lib/pmd-core-7.26.0.jar jars/pmd/lib/pmd-java-7.26.0.jar jars/pmd/lib/pmd-javascript-7.26.0.jar jars/pmd/lib/pmd-jsp-7.26.0.jar jars/pmd/PmdRunner.java jars/pmd/PmdRunner.class | deepseek-v4-flash |
|
||||
| 2026-07-19 13:38 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 真正根因:缺少 12 个 PMD transitive 依赖(slf4j/antlr/saxon 等);CycloneDX SBOM 解析依赖版本→下载全部 16 JAR;简化 PmdRunner.java;修复 pmd.ts catch 误判(message.includes('java') 太宽);重构 build.mjs 去掉 jars→out 重复拷贝 | 先试 pmd-dist ZIP(404)→改 GitHub Release(网络不通)→最后用 Maven CycloneDX 解析依赖版本 | jars/pmd/lib/*.jar(16) jars/pmd/PmdRunner.java src/adapters/pmd.ts scripts/build.mjs scripts/download-pmd.mjs vscode-code-reviewer-0.0.1.vsix | deepseek-v4-flash |
|
||||
| 2026-07-19 13:48 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 修复 PmdRunner.class 找不到:重构路径解析为 resolvePmdDir(),探测 4 个候选位置并用 fs.existsSync(PmdRunner.class) 验证;简化 getExtensionRoot 回退逻辑 | 无 | src/adapters/pmd.ts | deepseek-v4-flash |
|
||||
| 2026-07-19 14:33 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 修复 AI 审查 JSON 解析失败:repairJsonEscapes 漏了 `:` 导致 key 结束 `"` 被误加 `\`;保留 prompt 转义指令,预处理增加 `/\\"/g` → `"` 处理过度转义 | 无 | src/ai/engine.ts | deepseek-v4-flash |
|
||||
| 2026-07-19 14:56 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | ESLintAdapter 集成 eslint:recommended + @typescript-eslint/recommended 作为内置规则集;typescript-eslint 从 devDependencies 移至 dependencies | 方案 1(只改项目 eslint.config.mjs)被否;方案 2(适配器兜底自定义规则集)用户质疑"谁定义的推荐"→改为官方推荐规则集 | src/adapters/eslint.ts package.json | deepseek-v4-flash |
|
||||
| 2026-07-19 21:50 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: JSP 中 `<script>` 块 JS 代码误用 `.ts` 后缀 lint 导致 `no-redeclare` 漏检 — ESLintAdapter 兜底 filePath 从硬编码 `'untitled.ts'` 改为检查 `document.uri.scheme === 'untitled'` 决定扩展名 | 第一轮用 `document.fileName || 'untitled.js'` 不够 — virtual doc 的 fileName 是 `'Untitled-1'` (truthy),兜底永远用不上;第二轮改为检查 `document.uri.scheme === 'untitled'` | src/adapters/eslint.ts | deepseek-v4-flash |
|
||||
| 2026-07-19 22:04 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: VS Code 将 `.jsp` 识别为 `html` 语言 ID 导致 Orchestrator 静默返回空 — 尝试用 `contributes.languages` 注册 `.jsp`/`.jspx` 为 `jsp` 语言 | 方案曾被采纳,后因副作用被替换 | package.json | deepseek-v4-flash |
|
||||
| 2026-07-19 22:25 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 改用 `JspAdapter` 支持 `html` 语言 ID + 新增 `linters.html` 配置(默认 `jsp`),移除 `contributes.languages`,保留 HTML 编辑功能;`JspAdapter` 改为 mockDocument 替代 `openTextDocument`,避免 untitled 临时文档 | `contributes.languages` → 丢失 HTML 语言特性 → 弃用;`closeTextDocument` → API 不存在 → 改为 mockDocument | src/adapters/jsp.ts package.json | deepseek-v4-flash |
|
||||
| 2026-07-19 22:35 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 根因修复:AI 响应被截断因未设置 `max_tokens`。ChatOptions 加 maxTokens 字段;config 加 getAIMaxTokens()(默认 8192);package.json 加配置项;所有 provider 实现使用该值 | 无 | src/ai/providers/base.ts src/config/ai.ts package.json src/ai/providers/openai-compatible.ts src/ai/providers/claude.ts src/ai/providers/gemini.ts src/ai/engine.ts src/fixer/fixer.ts src/views/setupView.ts src/test/config.test.ts | deepseek-v4-flash |
|
||||
| 2026-07-20 20:01 | ① 用户提出 → ⑤ 编码实现 | Bug fix: SqlFluffViolation 接口字段与 sqlfluff JSON 输出不匹配(line_no→start_line_no,line_pos→start_line_pos,rule_code→code,缺 end 字段),导致行号显示 LNaN、ruleId 显示 undefined | 无 | src/adapters/sql-lint.ts | deepseek-v4-flash |
|
||||
| 2026-07-20 20:17 | ① 用户提出 → ⑤ 编码实现 | Bug fix: mergeResults 中用 originalRuleId 作 Map key 导致同 ruleId 的多条诊断(如两个 PRS)后覆盖前,翻译错误;改为按索引位置一一对应匹配 | 无 | src/merger/merger.ts | deepseek-v4-flash |
|
||||
|
||||
@@ -444,7 +444,7 @@ interface AIFinding {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "code-reviewer",
|
||||
"title": "CodeGuard 代码审查",
|
||||
"title": "净码特工",
|
||||
"icon": "images/icon.png"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -461,7 +461,7 @@ applyFix() 应用修复到编辑器(单条 / 批量倒序)
|
||||
"viewsContainers": {
|
||||
"activitybar": [{
|
||||
"id": "code-reviewer",
|
||||
"title": "CodeGuard 代码审查",
|
||||
"title": "净码特工",
|
||||
"icon": "images/icon.png"
|
||||
}]
|
||||
},
|
||||
|
||||
@@ -173,7 +173,7 @@ import { registerCommands } from './activation/commands';
|
||||
let orchestrator: Orchestrator;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('CodeGuard 代码审查插件已激活');
|
||||
console.log('净码特工 · Code Purifier 已激活');
|
||||
|
||||
orchestrator = new Orchestrator();
|
||||
registerCommands(context, orchestrator);
|
||||
@@ -213,14 +213,14 @@ export function deactivate() {
|
||||
```json
|
||||
{
|
||||
"commands": [
|
||||
{ "command": "codeReviewer.review", "title": "CodeGuard: 运行代码审查" },
|
||||
{ "command": "codeReviewer.reviewSelection", "title": "CodeGuard: 审查选中代码" },
|
||||
{ "command": "codeReviewer.openPanel", "title": "CodeGuard: 打开审查面板" },
|
||||
{ "command": "codeReviewer.exportReport", "title": "CodeGuard: 导出报告" },
|
||||
{ "command": "codeReviewer.addCustomRule", "title": "CodeGuard: 添加自定义规则" },
|
||||
{ "command": "codeReviewer.fixIssue", "title": "CodeGuard: 修复此问题" },
|
||||
{ "command": "codeReviewer.fixAll", "title": "CodeGuard: 批量修复" },
|
||||
{ "command": "codeReviewer.openSetup", "title": "CodeGuard: 打开设置面板" }
|
||||
{ "command": "codeReviewer.review", "title": "净码特工: 运行代码审查" },
|
||||
{ "command": "codeReviewer.reviewSelection", "title": "净码特工: 审查选中代码" },
|
||||
{ "command": "codeReviewer.openPanel", "title": "净码特工: 打开审查面板" },
|
||||
{ "command": "codeReviewer.exportReport", "title": "净码特工: 导出报告" },
|
||||
{ "command": "codeReviewer.addCustomRule", "title": "净码特工: 添加自定义规则" },
|
||||
{ "command": "codeReviewer.fixIssue", "title": "净码特工: 修复此问题" },
|
||||
{ "command": "codeReviewer.fixAll", "title": "净码特工: 批量修复" },
|
||||
{ "command": "codeReviewer.openSetup", "title": "净码特工: 打开设置面板" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -411,7 +411,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
## 验收
|
||||
|
||||
- [ ] 侧边栏显示 CodeGuard 设置视图
|
||||
- [ ] 侧边栏显示 净码特工 设置视图
|
||||
- [ ] 快速开始三步引导可见
|
||||
- [ ] API Key 可输入并保存到 SecretStorage
|
||||
- [ ] 提供商/模型可切换
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import typescriptEslint from "typescript-eslint";
|
||||
|
||||
export default [{
|
||||
files: ["**/*.ts"],
|
||||
files: ["**/*.js", "**/*.ts"],
|
||||
}, {
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint.plugin,
|
||||
|
||||
@@ -24,15 +24,17 @@ public class PmdRunner {
|
||||
|
||||
try {
|
||||
PMDConfiguration config = new PMDConfiguration();
|
||||
config.setInputFilePath(Path.of(filePath));
|
||||
config.addRuleSet(Path.of(rulesetPath));
|
||||
config.setReportFormat("json");
|
||||
config.addRuleSet(rulesetPath);
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
config.setReportWriter(writer);
|
||||
Writer writer = new StringWriter();
|
||||
JsonRenderer renderer = new JsonRenderer();
|
||||
renderer.setWriter(writer);
|
||||
|
||||
PmdAnalysis pmd = PmdAnalysis.create(config);
|
||||
pmd.performAnalysis();
|
||||
try (PmdAnalysis pmd = PmdAnalysis.create(config)) {
|
||||
pmd.files().addFile(Path.of(filePath));
|
||||
pmd.addRenderer(renderer);
|
||||
pmd.performAnalysis();
|
||||
}
|
||||
|
||||
System.out.print(writer.toString());
|
||||
} finally {
|
||||
|
||||
+29
-15
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vscode-code-reviewer",
|
||||
"displayName": "vscode-code-reviewer",
|
||||
"description": "VS Code 插件设计 —— B3:代码审查与规范检查一体化",
|
||||
"displayName": "净码特工 · Code Purifier",
|
||||
"description": "AI 驱动的代码审查与规范检查一体化 VSCode 插件",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.120.0"
|
||||
@@ -15,35 +15,35 @@
|
||||
"commands": [
|
||||
{
|
||||
"command": "codeReviewer.review",
|
||||
"title": "CodeGuard: 运行代码审查"
|
||||
"title": "Code Purifier: 运行代码审查"
|
||||
},
|
||||
{
|
||||
"command": "codeReviewer.reviewSelection",
|
||||
"title": "CodeGuard: 审查选中代码"
|
||||
"title": "Code Purifier: 审查选中代码"
|
||||
},
|
||||
{
|
||||
"command": "codeReviewer.openPanel",
|
||||
"title": "CodeGuard: 打开审查面板"
|
||||
"title": "Code Purifier: 打开审查面板"
|
||||
},
|
||||
{
|
||||
"command": "codeReviewer.exportReport",
|
||||
"title": "CodeGuard: 导出报告"
|
||||
"title": "Code Purifier: 导出报告"
|
||||
},
|
||||
{
|
||||
"command": "codeReviewer.addCustomRule",
|
||||
"title": "CodeGuard: 添加自定义规则"
|
||||
"title": "Code Purifier: 添加自定义规则"
|
||||
},
|
||||
{
|
||||
"command": "codeReviewer.fixIssue",
|
||||
"title": "CodeGuard: 修复此问题"
|
||||
"title": "Code Purifier: 修复此问题"
|
||||
},
|
||||
{
|
||||
"command": "codeReviewer.fixAll",
|
||||
"title": "CodeGuard: 批量修复"
|
||||
"title": "Code Purifier: 批量修复"
|
||||
},
|
||||
{
|
||||
"command": "codeReviewer.openSetup",
|
||||
"title": "CodeGuard: 打开设置面板"
|
||||
"title": "Code Purifier: 打开设置面板"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
@@ -57,7 +57,7 @@
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "code-reviewer",
|
||||
"title": "CodeGuard 代码审查",
|
||||
"title": "Code Purifier",
|
||||
"icon": "images/icon.png"
|
||||
}
|
||||
]
|
||||
@@ -84,7 +84,7 @@
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "CodeGuard",
|
||||
"title": "Code Purifier",
|
||||
"properties": {
|
||||
"vscode-code-reviewer.ai.provider": {
|
||||
"type": "string",
|
||||
@@ -110,6 +110,11 @@
|
||||
"default": 0.2,
|
||||
"description": "AI 温度参数"
|
||||
},
|
||||
"vscode-code-reviewer.ai.maxTokens": {
|
||||
"type": "number",
|
||||
"default": 8192,
|
||||
"description": "AI 单次请求最大输出 Token 数"
|
||||
},
|
||||
"vscode-code-reviewer.ai.timeout": {
|
||||
"type": "number",
|
||||
"default": 300,
|
||||
@@ -161,6 +166,15 @@
|
||||
],
|
||||
"description": "JSP linter"
|
||||
},
|
||||
"vscode-code-reviewer.linters.html": {
|
||||
"type": "string",
|
||||
"default": "jsp",
|
||||
"enum": [
|
||||
"",
|
||||
"jsp"
|
||||
],
|
||||
"description": "HTML linter"
|
||||
},
|
||||
"vscode-code-reviewer.linters.css": {
|
||||
"type": "string",
|
||||
"default": "stylelint",
|
||||
@@ -229,7 +243,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint": "^9.39.3",
|
||||
"stylelint": "^17.14.0"
|
||||
"stylelint": "^17.14.0",
|
||||
"typescript-eslint": "^8.56.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
@@ -239,7 +254,6 @@
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.9.2",
|
||||
"esbuild": "^0.28.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.1"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
+3
-9
@@ -1,5 +1,5 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import { copyFileSync, mkdirSync, existsSync, cpSync } from 'fs';
|
||||
import { copyFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -33,20 +33,14 @@ await esbuild.build({
|
||||
treeShaking: true,
|
||||
});
|
||||
|
||||
const jarsSrc = resolve(rootDir, 'jars');
|
||||
const jarsDest = resolve(outDir, 'jars');
|
||||
if (existsSync(jarsSrc)) {
|
||||
cpSync(jarsSrc, jarsDest, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
/* copy webview JS files */
|
||||
const viewsSrc = resolve(rootDir, 'src', 'views');
|
||||
const viewsDest = resolve(outDir, 'views');
|
||||
const webviewDir = resolve(outDir, 'webview');
|
||||
if (existsSync(viewsSrc)) {
|
||||
const files = ['setupView.js'];
|
||||
for (const f of files) {
|
||||
const src = resolve(viewsSrc, f);
|
||||
const dest = resolve(viewsDest, f);
|
||||
const dest = resolve(webviewDir, f);
|
||||
if (existsSync(src)) {
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
copyFileSync(src, dest);
|
||||
|
||||
@@ -10,7 +10,8 @@ const srcViews = resolve(rootDir, 'src', 'views');
|
||||
const files = ['setupView.js'];
|
||||
for (const f of files) {
|
||||
const src = resolve(srcViews, f);
|
||||
const dest = resolve(outViews, f);
|
||||
const webviewDir = resolve(rootDir, 'out', 'webview');
|
||||
const dest = resolve(webviewDir, f);
|
||||
if (existsSync(src)) {
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
copyFileSync(src, dest);
|
||||
|
||||
+32
-17
@@ -8,14 +8,26 @@ const rootDir = resolve(__dirname, '..');
|
||||
const libDir = resolve(rootDir, 'jars', 'pmd', 'lib');
|
||||
|
||||
const PMD_VERSION = '7.26.0';
|
||||
const PMD_JARS = [
|
||||
`pmd-core-${PMD_VERSION}.jar`,
|
||||
`pmd-java-${PMD_VERSION}.jar`,
|
||||
`pmd-javascript-${PMD_VERSION}.jar`,
|
||||
`pmd-jsp-${PMD_VERSION}.jar`,
|
||||
];
|
||||
const MAVEN_BASE = 'https://repo1.maven.org/maven2';
|
||||
|
||||
const MAVEN_BASE = `https://repo1.maven.org/maven2/net/sourceforge/pmd`;
|
||||
const JARS = [
|
||||
{ path: `net/sourceforge/pmd/pmd-core/${PMD_VERSION}/pmd-core-${PMD_VERSION}.jar` },
|
||||
{ path: `net/sourceforge/pmd/pmd-java/${PMD_VERSION}/pmd-java-${PMD_VERSION}.jar` },
|
||||
{ path: `net/sourceforge/pmd/pmd-javascript/${PMD_VERSION}/pmd-javascript-${PMD_VERSION}.jar` },
|
||||
{ path: `net/sourceforge/pmd/pmd-jsp/${PMD_VERSION}/pmd-jsp-${PMD_VERSION}.jar` },
|
||||
{ path: 'org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar' },
|
||||
{ path: 'org/slf4j/jul-to-slf4j/1.7.36/jul-to-slf4j-1.7.36.jar' },
|
||||
{ path: 'org/antlr/antlr4-runtime/4.13.2/antlr4-runtime-4.13.2.jar' },
|
||||
{ path: 'net/sf/saxon/Saxon-HE/12.9/Saxon-HE-12.9.jar' },
|
||||
{ path: 'org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar' },
|
||||
{ path: 'org/ow2/asm/asm/9.10.1/asm-9.10.1.jar' },
|
||||
{ path: 'com/google/code/gson/gson/2.14.0/gson-2.14.0.jar' },
|
||||
{ path: 'org/checkerframework/checker-qual/4.2.0/checker-qual-4.2.0.jar' },
|
||||
{ path: 'org/pcollections/pcollections/4.0.2/pcollections-4.0.2.jar' },
|
||||
{ path: 'com/github/oowekyala/ooxml/nice-xml-messages/3.1/nice-xml-messages-3.1.jar' },
|
||||
{ path: 'org/xmlresolver/xmlresolver/5.3.3/xmlresolver-5.3.3.jar' },
|
||||
{ path: 'com/google/errorprone/error_prone_annotations/2.47.0/error_prone_annotations-2.47.0.jar' },
|
||||
];
|
||||
|
||||
function downloadFile(url, dest) {
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
@@ -25,6 +37,10 @@ function downloadFile(url, dest) {
|
||||
downloadFile(response.headers.location, dest).then(resolvePromise).catch(reject);
|
||||
return;
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode} for ${url}`));
|
||||
return;
|
||||
}
|
||||
response.pipe(file);
|
||||
file.on('finish', () => { file.close(); resolvePromise(); });
|
||||
file.on('error', (err) => { unlinkSync(dest); reject(err); });
|
||||
@@ -36,19 +52,18 @@ if (!existsSync(libDir)) {
|
||||
mkdirSync(libDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (const jar of PMD_JARS) {
|
||||
const moduleName = jar.replace(`-${PMD_VERSION}.jar`, '').replace('pmd-', '');
|
||||
const url = `${MAVEN_BASE}/pmd-${moduleName}/${PMD_VERSION}/${jar}`;
|
||||
const dest = resolve(libDir, jar);
|
||||
|
||||
let count = 0;
|
||||
for (const jar of JARS) {
|
||||
const filename = jar.path.split('/').pop();
|
||||
const dest = resolve(libDir, filename);
|
||||
if (existsSync(dest)) {
|
||||
console.log(`Skip: ${jar} (exists)`);
|
||||
console.log(`Skip: ${filename}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Downloading: ${url}`);
|
||||
const url = `${MAVEN_BASE}/${jar.path}`;
|
||||
console.log(`Downloading ${filename}...`);
|
||||
await downloadFile(url, dest);
|
||||
console.log(`Done: ${jar}`);
|
||||
count++;
|
||||
}
|
||||
|
||||
console.log('PMD JARs download complete.');
|
||||
console.log(`Done. Downloaded ${count} new JARs. Total: ${JARS.length} JARs in ${libDir}`);
|
||||
|
||||
+22
-2
@@ -1,20 +1,40 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { ESLint } from 'eslint';
|
||||
import js from '@eslint/js';
|
||||
import ts from 'typescript-eslint';
|
||||
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
|
||||
|
||||
export class ESLintAdapter implements LinterAdapter {
|
||||
id = 'eslint';
|
||||
supportedLanguages = ['javascript', 'typescript'];
|
||||
|
||||
private static defaultConfig: any[] | null = null;
|
||||
|
||||
private static getDefaultConfig(): any[] {
|
||||
if (!ESLintAdapter.defaultConfig) {
|
||||
ESLintAdapter.defaultConfig = [
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
];
|
||||
}
|
||||
return ESLintAdapter.defaultConfig;
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
|
||||
try {
|
||||
const engine = new ESLint({ cwd: workingDir });
|
||||
const engine = new ESLint({
|
||||
cwd: workingDir,
|
||||
overrideConfigFile: true,
|
||||
overrideConfig: ESLintAdapter.getDefaultConfig(),
|
||||
});
|
||||
const ext = document.languageId === 'typescript' ? 'ts' : 'js';
|
||||
const isVirtual = document.uri.scheme === 'untitled';
|
||||
const results = await engine.lintText(document.getText(), {
|
||||
filePath: document.fileName || 'untitled.ts',
|
||||
filePath: isVirtual ? `untitled.${ext}` : document.fileName,
|
||||
});
|
||||
|
||||
const diagnostics: LinterDiagnostic[] = [];
|
||||
|
||||
+49
-6
@@ -6,9 +6,56 @@ import { StylelintAdapter } from './stylelint';
|
||||
import { extractJspSections } from '../jsp/jsp-extractor';
|
||||
import { getLinterForLanguage } from '../config';
|
||||
|
||||
function mockDocument(code: string, language: string): vscode.TextDocument {
|
||||
const lines = code.split('\n');
|
||||
const uri = vscode.Uri.parse('untitled:virtual');
|
||||
const ext = language === 'javascript' ? 'js' : language === 'typescript' ? 'ts' : language === 'css' ? 'css' : 'java';
|
||||
return {
|
||||
uri,
|
||||
fileName: `untitled.${ext}`,
|
||||
isUntitled: true,
|
||||
languageId: language,
|
||||
version: 1,
|
||||
isDirty: false,
|
||||
isClosed: false,
|
||||
eol: vscode.EndOfLine.LF,
|
||||
lineCount: lines.length,
|
||||
getText: () => code,
|
||||
lineAt: (arg: number | vscode.Position) => {
|
||||
const line = typeof arg === 'number' ? arg : arg.line;
|
||||
const text = lines[line] ?? '';
|
||||
return {
|
||||
lineNumber: line,
|
||||
text,
|
||||
range: new vscode.Range(line, 0, line, text.length),
|
||||
rangeIncludingLineBreak: new vscode.Range(line, 0, line, text.length),
|
||||
firstNonWhitespaceCharacterIndex: text.search(/\S|$/),
|
||||
isEmptyOrWhitespace: text.trim().length === 0,
|
||||
};
|
||||
},
|
||||
offsetAt: (p: vscode.Position) => {
|
||||
let offset = 0;
|
||||
for (let i = 0; i < p.line; i++) offset += lines[i].length + 1;
|
||||
return offset + p.character;
|
||||
},
|
||||
positionAt: (offset: number) => {
|
||||
let remaining = offset;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (remaining <= lines[i].length) return new vscode.Position(i, remaining);
|
||||
remaining -= lines[i].length + 1;
|
||||
}
|
||||
return new vscode.Position(lines.length - 1, lines[lines.length - 1].length);
|
||||
},
|
||||
getWordRangeAtPosition: () => undefined,
|
||||
validateRange: (r: vscode.Range) => r,
|
||||
validatePosition: (p: vscode.Position) => p,
|
||||
save: () => Promise.resolve(false),
|
||||
} as unknown as vscode.TextDocument;
|
||||
}
|
||||
|
||||
export class JspAdapter implements LinterAdapter {
|
||||
id = 'jsp';
|
||||
supportedLanguages = ['jsp'];
|
||||
supportedLanguages = ['jsp', 'html'];
|
||||
|
||||
private pmdAdapter = new PmdAdapter();
|
||||
private eslintAdapter = new ESLintAdapter();
|
||||
@@ -40,11 +87,7 @@ export class JspAdapter implements LinterAdapter {
|
||||
if (!adapter) { continue; }
|
||||
|
||||
try {
|
||||
const virtualDoc = await vscode.workspace.openTextDocument({
|
||||
content: section.code,
|
||||
language: section.language,
|
||||
});
|
||||
const result = await adapter.check(virtualDoc, workingDir);
|
||||
const result = await adapter.check(mockDocument(section.code, section.language), workingDir);
|
||||
|
||||
for (const diag of result.diagnostics) {
|
||||
const adjustedRange = new vscode.Range(
|
||||
|
||||
+27
-14
@@ -1,5 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import type { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
|
||||
import { getPMDRulesetPath } from '../config';
|
||||
@@ -7,24 +8,36 @@ import { getPMDRulesetPath } from '../config';
|
||||
export class PmdAdapter implements LinterAdapter {
|
||||
id = 'pmd';
|
||||
supportedLanguages = ['java'];
|
||||
private pmdDir: string | null = null;
|
||||
|
||||
private resolvePmdDir(): string {
|
||||
if (this.pmdDir) { return this.pmdDir; }
|
||||
const candidates: string[] = [];
|
||||
try {
|
||||
const ext = vscode.extensions.getExtension?.('vscode-code-reviewer');
|
||||
if (ext?.extensionPath) {
|
||||
candidates.push(path.join(ext.extensionPath, 'jars', 'pmd'));
|
||||
candidates.push(path.join(ext.extensionPath, 'out', 'jars', 'pmd'));
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
candidates.push(path.join(path.resolve(__dirname, '..'), 'jars', 'pmd'));
|
||||
candidates.push(path.join(path.resolve(__dirname, '..', '..'), 'jars', 'pmd'));
|
||||
for (const dir of candidates) {
|
||||
if (existsSync(path.join(dir, 'PmdRunner.class'))) {
|
||||
this.pmdDir = dir;
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
this.pmdDir = candidates[0];
|
||||
return this.pmdDir;
|
||||
}
|
||||
|
||||
private getPmdLibClasspath(): string {
|
||||
const extRoot = this.getExtensionRoot();
|
||||
const pmdLib = path.join(extRoot, 'jars', 'pmd', 'lib');
|
||||
return path.join(pmdLib, '*');
|
||||
return path.join(this.resolvePmdDir(), 'lib', '*');
|
||||
}
|
||||
|
||||
private getPmdRunnerClasspath(): string {
|
||||
const extRoot = this.getExtensionRoot();
|
||||
return path.join(extRoot, 'jars', 'pmd');
|
||||
}
|
||||
|
||||
private getExtensionRoot(): string {
|
||||
try {
|
||||
const extPath = vscode.extensions.getExtension?.('vscode-code-reviewer')?.extensionPath;
|
||||
if (extPath) { return extPath; }
|
||||
} catch { /* extension not available */ }
|
||||
return path.join(__dirname, '..', '..');
|
||||
return this.resolvePmdDir();
|
||||
}
|
||||
|
||||
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
|
||||
@@ -43,7 +56,7 @@ export class PmdAdapter implements LinterAdapter {
|
||||
return { diagnostics, status: 'ok' };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (message.includes('ENOENT') || message.includes('java')) {
|
||||
if (message.includes('ENOENT') || message.includes('java not found') || message.includes('Cannot find')) {
|
||||
return { diagnostics: [], status: 'tool-unavailable', errorMessage: 'Java 11+ 未安装或不在 PATH 中' };
|
||||
}
|
||||
return { diagnostics: [], status: 'execution-failed', errorMessage: message };
|
||||
|
||||
@@ -8,9 +8,11 @@ const DIALECT_MAP: Record<string, string> = {
|
||||
};
|
||||
|
||||
interface SqlFluffViolation {
|
||||
line_no: number;
|
||||
line_pos: number;
|
||||
rule_code: string;
|
||||
start_line_no: number;
|
||||
start_line_pos: number;
|
||||
end_line_no: number;
|
||||
end_line_pos: number;
|
||||
code: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -74,13 +76,13 @@ export class SqlLintAdapter implements LinterAdapter {
|
||||
for (const v of result.violations) {
|
||||
diagnostics.push({
|
||||
severity: 'warning',
|
||||
ruleId: `sql-lint:${v.rule_code}`,
|
||||
ruleId: `sql-lint:${v.code}`,
|
||||
message: v.description,
|
||||
range: new vscode.Range(
|
||||
v.line_no - 1,
|
||||
v.line_pos - 1,
|
||||
v.line_no - 1,
|
||||
v.line_pos - 1
|
||||
v.start_line_no - 1,
|
||||
v.start_line_pos - 1,
|
||||
v.end_line_no - 1,
|
||||
v.end_line_pos - 1
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
|
||||
|
||||
const CONFIG_FILE_NAMES = [
|
||||
'.stylelintrc',
|
||||
'.stylelintrc.json',
|
||||
'.stylelintrc.yaml',
|
||||
'.stylelintrc.yml',
|
||||
'.stylelintrc.js',
|
||||
'stylelint.config.js',
|
||||
'stylelint.config.mjs',
|
||||
'stylelint.config.cjs',
|
||||
];
|
||||
|
||||
const DEFAULT_CONFIG: Record<string, unknown> = {
|
||||
rules: {
|
||||
'color-hex-length': 'short',
|
||||
'color-named': 'never',
|
||||
'color-no-invalid-hex': true,
|
||||
'length-zero-no-unit': true,
|
||||
'font-family-no-missing-generic-family-keyword': true,
|
||||
'block-no-empty': true,
|
||||
'declaration-block-no-duplicate-properties': true,
|
||||
'no-descending-specificity': true,
|
||||
'unit-no-unknown': true,
|
||||
'property-no-unknown': true,
|
||||
'selector-pseudo-class-no-unknown': true,
|
||||
'selector-pseudo-element-no-unknown': true,
|
||||
},
|
||||
};
|
||||
|
||||
interface LinterOptions {
|
||||
code?: string;
|
||||
codeFilename?: string;
|
||||
cwd?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface LinterResult {
|
||||
@@ -21,6 +52,14 @@ interface LinterResult {
|
||||
}>;
|
||||
}
|
||||
|
||||
function hasExternalConfig(dir: string): boolean {
|
||||
try {
|
||||
return CONFIG_FILE_NAMES.some(name => fs.existsSync(path.join(dir, name)));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class StylelintAdapter implements LinterAdapter {
|
||||
id = 'stylelint';
|
||||
supportedLanguages = ['css'];
|
||||
@@ -29,7 +68,8 @@ export class StylelintAdapter implements LinterAdapter {
|
||||
|
||||
private async getModule(): Promise<{ lint: (opts: LinterOptions) => Promise<LinterResult> }> {
|
||||
if (!this._module) {
|
||||
this._module = await import('stylelint') as { lint: (opts: LinterOptions) => Promise<LinterResult> };
|
||||
const mod = await import('stylelint');
|
||||
this._module = (mod.default ?? mod) as { lint: (opts: LinterOptions) => Promise<LinterResult> };
|
||||
}
|
||||
return this._module;
|
||||
}
|
||||
@@ -41,11 +81,18 @@ export class StylelintAdapter implements LinterAdapter {
|
||||
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
|
||||
try {
|
||||
const stylelint = await this.getModule();
|
||||
const result = await stylelint.lint({
|
||||
|
||||
const lintOptions: LinterOptions = {
|
||||
code: document.getText(),
|
||||
codeFilename: document.fileName,
|
||||
cwd: workingDir,
|
||||
});
|
||||
};
|
||||
|
||||
if (!hasExternalConfig(workingDir)) {
|
||||
lintOptions.config = DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
const result = await stylelint.lint(lintOptions);
|
||||
|
||||
const diagnostics: LinterDiagnostic[] = [];
|
||||
for (const res of result.results) {
|
||||
|
||||
+50
-8
@@ -1,7 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import type { AIProvider } from './providers/base';
|
||||
import { createProvider } from './factory';
|
||||
import { getAIProvider, getAIModel, getAIBaseUrl, getAITemperature, getAITimeout, getAIOutputLanguage, getApiKey } from '../config';
|
||||
import { getAIProvider, getAIModel, getAIBaseUrl, getAITemperature, getAITimeout, getAIMaxTokens, getAIOutputLanguage, getApiKey } from '../config';
|
||||
import type { LinterDiagnostic, CustomRule } from '../types';
|
||||
import type {
|
||||
AIEngineResult,
|
||||
@@ -26,14 +26,54 @@ function addLineNumbers(code: string): string {
|
||||
return code.split('\n').map((line, i) => `${String(i + 1).padStart(4, ' ')}| ${line}`).join('\n');
|
||||
}
|
||||
|
||||
function repairJsonEscapes(str: string): string {
|
||||
let s = str.replace(/\\"\\n/g, '\\n').replace(/\\"/g, '"');
|
||||
let inString = false;
|
||||
let out = '';
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
if (ch === '\\') {
|
||||
out += ch;
|
||||
if (i + 1 < s.length) { out += s[++i]; }
|
||||
} else if (ch === '"') {
|
||||
if (!inString) {
|
||||
inString = true;
|
||||
out += ch;
|
||||
} else {
|
||||
let j = i + 1;
|
||||
while (j < s.length && s[j] === ' ') { j++; }
|
||||
if (j < s.length && ':,\]}'.includes(s[j])) {
|
||||
inString = false;
|
||||
out += ch;
|
||||
} else {
|
||||
out += '\\"';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out += ch;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseJsonResponse(raw: string): object {
|
||||
const trimmed = raw.trim();
|
||||
const start = trimmed.indexOf('{');
|
||||
const end = trimmed.lastIndexOf('}');
|
||||
if (start === -1 || end === -1) {
|
||||
throw new Error('响应中未找到 JSON');
|
||||
throw new Error(`响应中未找到 JSON。原始响应(前200字符):${trimmed.slice(0, 200)}`);
|
||||
}
|
||||
let jsonStr = trimmed.substring(start, end + 1);
|
||||
try {
|
||||
return JSON.parse(jsonStr);
|
||||
} catch {
|
||||
jsonStr = repairJsonEscapes(jsonStr);
|
||||
try {
|
||||
return JSON.parse(jsonStr);
|
||||
} catch {
|
||||
throw new Error(`JSON 解析失败。原始响应(前200字符):${jsonStr.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
return JSON.parse(trimmed.substring(start, end + 1));
|
||||
}
|
||||
|
||||
const CUSTOM_RULE_SYSTEM_PROMPT = `你是代码规则审查员,只评估以下自定义规则是否被违反。
|
||||
@@ -48,7 +88,8 @@ const DEEP_REVIEW_SYSTEM_PROMPT = `你是资深代码审查专家,完成两个
|
||||
重点:安全漏洞、逻辑错误、性能问题、设计缺陷
|
||||
不要重复静态分析已报告的问题。
|
||||
|
||||
仅输出 JSON,格式:
|
||||
仅输出 JSON,字符串中的双引号必须用 \" 转义。
|
||||
格式:
|
||||
{
|
||||
"translatedDiagnostics": [{ "originalRuleId": "原始ID", "translatedMessage": "翻译", "translatedSuggestion": "建议", "codeDiff": "可选" }],
|
||||
"findings": [{ "ruleId": "kebab-case", "severity": "error|warning|info", "category": "bug|performance|security|style|design", "title": "标题", "description": "描述", "suggestion": "建议", "codeDiff": "可选", "line": 行号 }]
|
||||
@@ -91,6 +132,7 @@ export async function runAIReview(
|
||||
const options = {
|
||||
model: getAIModel(),
|
||||
temperature: getAITemperature(),
|
||||
maxTokens: getAIMaxTokens(),
|
||||
timeoutMs: getAITimeout() * 1000,
|
||||
};
|
||||
|
||||
@@ -123,8 +165,8 @@ export async function runAIReview(
|
||||
...r,
|
||||
ruleId: `custom:${r.ruleId}`,
|
||||
}));
|
||||
} catch {
|
||||
errors.push('自定义规则响应解析失败');
|
||||
} catch (e) {
|
||||
errors.push(`自定义规则响应解析失败: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
} else {
|
||||
errors.push(`自定义规则请求失败: ${resultA.reason}`);
|
||||
@@ -140,8 +182,8 @@ export async function runAIReview(
|
||||
};
|
||||
translatedDiagnostics = parsed.translatedDiagnostics ?? [];
|
||||
findings = parsed.findings ?? [];
|
||||
} catch {
|
||||
errors.push('AI 审查响应解析失败');
|
||||
} catch (e) {
|
||||
errors.push(`AI 审查响应解析失败: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
} else {
|
||||
errors.push(`AI 审查请求失败: ${resultB.reason}`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface ChatOptions {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export class ClaudeProvider extends AIProvider {
|
||||
|
||||
const body = JSON.stringify({
|
||||
model: options.model,
|
||||
max_tokens: 4096,
|
||||
max_tokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
|
||||
@@ -18,6 +18,7 @@ export class GeminiProvider extends AIProvider {
|
||||
],
|
||||
generationConfig: {
|
||||
temperature: options.temperature,
|
||||
maxOutputTokens: options.maxTokens,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export class OpenAICompatibleProvider extends AIProvider {
|
||||
|
||||
const body = JSON.stringify({
|
||||
model: options.model,
|
||||
max_tokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
|
||||
@@ -22,6 +22,10 @@ export function getAITimeout(): number {
|
||||
return vscode.workspace.getConfiguration(ROOT).get<number>('ai.timeout', 300);
|
||||
}
|
||||
|
||||
export function getAIMaxTokens(): number {
|
||||
return vscode.workspace.getConfiguration(ROOT).get<number>('ai.maxTokens', 8192);
|
||||
}
|
||||
|
||||
export function getAIOutputLanguage(): string {
|
||||
return vscode.workspace.getConfiguration(ROOT).get<string>('ai.outputLanguage', 'zh-CN');
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { SetupViewProvider } from './views/setupView';
|
||||
let orchestrator: Orchestrator;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('CodeGuard 代码审查插件已激活');
|
||||
console.log('净码特工 · Code Purifier 已激活');
|
||||
|
||||
orchestrator = new Orchestrator();
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ export async function generateFix(
|
||||
const response = await provider.chat(FIX_SYSTEM_PROMPT, userPrompt, {
|
||||
model,
|
||||
temperature,
|
||||
maxTokens: 4096,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ export function mergeResults(input: MergeInput): MergedReport {
|
||||
range: new vscode.Range(Math.max(0, r.line - 1), 0, Math.max(0, r.line - 1), 1),
|
||||
}));
|
||||
|
||||
const linterDiagnostics = input.staticDiagnostics.map((d, i) => {
|
||||
const td = input.translatedDiagnostics[i];
|
||||
if (td) {
|
||||
return { ...d, message: td.translatedMessage, suggestion: td.translatedSuggestion || d.suggestion };
|
||||
}
|
||||
return d;
|
||||
});
|
||||
|
||||
const linterCount = input.staticDiagnostics.length;
|
||||
const customRuleCount = customRuleDiagnostics.length;
|
||||
const aiCount = input.aiFindings.length;
|
||||
@@ -53,7 +61,7 @@ export function mergeResults(input: MergeInput): MergedReport {
|
||||
.map((_, i) => i);
|
||||
|
||||
return {
|
||||
linterDiagnostics: input.staticDiagnostics,
|
||||
linterDiagnostics,
|
||||
customRuleDiagnostics,
|
||||
translatedDiagnostics: input.translatedDiagnostics,
|
||||
aiFindings: input.aiFindings,
|
||||
|
||||
+8
-20
@@ -63,12 +63,11 @@ export class ReviewPanel {
|
||||
|
||||
const fileName = report.filePath.split(/[/\\]/).pop() ?? '';
|
||||
|
||||
const degradedBanner = report.degraded
|
||||
? `<div class="banner ${report.errors.length > 0 ? 'banner-error' : 'banner-warn'}">
|
||||
${report.errors.length > 0 ? '⚠ AI 审查失败' : '⚠ 部分 AI 功能不可用'}
|
||||
${report.errors.join('; ')}
|
||||
</div>`
|
||||
: '';
|
||||
const errorBanner = report.errors.length > 0
|
||||
? `<div class="banner banner-error">⚠ ${report.errors.join('; ')}</div>`
|
||||
: report.degraded
|
||||
? '<div class="banner banner-warn">⚠ 部分 AI 功能不可用</div>'
|
||||
: '';
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
@@ -119,7 +118,7 @@ export class ReviewPanel {
|
||||
<h1>📋 代码审查报告</h1>
|
||||
<div class="meta">${fileName} · ${report.language} · ${(report.duration / 1000).toFixed(1)}s</div>
|
||||
</div>
|
||||
${degradedBanner}
|
||||
${errorBanner}
|
||||
<div class="stats">
|
||||
<div class="stat-card stat-total"><div class="num">${total}</div><div class="label">总计</div></div>
|
||||
<div class="stat-card stat-error"><div class="num">${errorCount}</div><div class="label">错误</div></div>
|
||||
@@ -171,6 +170,7 @@ ${degradedBanner}
|
||||
<div class="issue-left">
|
||||
<div class="issue-title"><span class="severity">${this.sevIcon(d.severity)}</span> <code>${this.escape(d.ruleId)}</code> L${d.range.start.line + 1}</div>
|
||||
<div class="issue-detail">${this.escape(d.message)}</div>
|
||||
${d.suggestion ? `<div class="issue-detail">建议: ${this.escape(d.suggestion)}</div>` : ''}
|
||||
</div>
|
||||
<div class="issue-actions">
|
||||
<button class="btn" onclick="event.stopPropagation();send('fix', ${d.range.start.line}, '${this.escape(d.ruleId)}', 'linter')">修复</button>
|
||||
@@ -195,23 +195,11 @@ ${degradedBanner}
|
||||
}
|
||||
|
||||
private buildAIList(report: MergedReport): string {
|
||||
const total = report.translatedDiagnostics.length + report.aiFindings.length;
|
||||
if (total === 0) {
|
||||
if (report.aiFindings.length === 0) {
|
||||
return '<div class="empty">🤖 AI 审查未发现新问题</div>';
|
||||
}
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const td of report.translatedDiagnostics) {
|
||||
parts.push(`
|
||||
<div class="issue">
|
||||
<div class="issue-left">
|
||||
<div class="issue-title"><span class="severity">🔵</span> <code>${this.escape(td.originalRuleId)}</code></div>
|
||||
<div class="issue-detail">${this.escape(td.translatedMessage)}</div>
|
||||
${td.translatedSuggestion ? `<div class="issue-detail">建议: ${this.escape(td.translatedSuggestion)}</div>` : ''}
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
for (const f of report.aiFindings) {
|
||||
parts.push(`
|
||||
<div class="issue" onclick="send('navigate', ${f.line}, '${this.escape(f.ruleId)}', 'ai')">
|
||||
|
||||
@@ -10,11 +10,6 @@ interface RuleYamlItem {
|
||||
languages?: string[];
|
||||
}
|
||||
|
||||
interface RuleConfig {
|
||||
enabled?: string[];
|
||||
rules?: Record<string, { enabled: boolean }>;
|
||||
}
|
||||
|
||||
function parseYamlSimple(content: string): object[] {
|
||||
const items: Array<Record<string, unknown>> = [];
|
||||
let current: Record<string, unknown> | null = null;
|
||||
@@ -60,89 +55,20 @@ function parseYamlSimple(content: string): object[] {
|
||||
return items;
|
||||
}
|
||||
|
||||
function parseConfigYaml(content: string): RuleConfig {
|
||||
const config: RuleConfig = { enabled: [], rules: {} };
|
||||
let section: string | null = null;
|
||||
let currentKey = '';
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) { continue; }
|
||||
|
||||
if (trimmed === 'enabled:') {
|
||||
section = 'enabled';
|
||||
continue;
|
||||
}
|
||||
if (trimmed === 'rules:') {
|
||||
section = 'rules';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section === 'enabled' && trimmed.startsWith('- ')) {
|
||||
const name = trimmed.substring(2).trim();
|
||||
if (!config.enabled) { config.enabled = []; }
|
||||
config.enabled!.push(name);
|
||||
}
|
||||
|
||||
if (section === 'rules') {
|
||||
const ruleMatch = trimmed.match(/^(\w[\w-]*)\s*:\s*$/);
|
||||
if (ruleMatch) {
|
||||
currentKey = ruleMatch[1];
|
||||
if (!config.rules) { config.rules = {}; }
|
||||
config.rules[currentKey] = { enabled: true };
|
||||
} else if (currentKey) {
|
||||
const propMatch = trimmed.match(/^(\w+)\s*:\s*(.*)$/);
|
||||
if (propMatch) {
|
||||
const key = propMatch[1];
|
||||
const value = propMatch[2].trim();
|
||||
if (!config.rules) { config.rules = {}; }
|
||||
if (!config.rules[currentKey]) { config.rules[currentKey] = { enabled: true }; }
|
||||
(config.rules[currentKey] as Record<string, unknown>)[key] =
|
||||
value === 'false' ? false : value === 'true' ? true : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function loadActiveRules(workspaceRoot: string): CustomRule[] {
|
||||
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
|
||||
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
|
||||
|
||||
if (!fs.existsSync(rulesDir)) { return []; }
|
||||
|
||||
let ruleConfig: RuleConfig = {};
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configContent = fs.readFileSync(configPath, 'utf-8');
|
||||
ruleConfig = parseConfigYaml(configContent);
|
||||
}
|
||||
|
||||
const enabledFiles = new Set(ruleConfig.enabled ?? []);
|
||||
const disabledRules = new Set(
|
||||
Object.entries(ruleConfig.rules ?? {})
|
||||
.filter(([, v]) => v.enabled === false)
|
||||
.map(([k]) => k)
|
||||
);
|
||||
|
||||
const allRules: CustomRule[] = [];
|
||||
|
||||
const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
||||
for (const file of files) {
|
||||
if (enabledFiles.size > 0 && !enabledFiles.has(file)) { continue; }
|
||||
|
||||
const content = fs.readFileSync(path.join(rulesDir, file), 'utf-8');
|
||||
const items = parseYamlSimple(content) as RuleYamlItem[];
|
||||
|
||||
for (const item of items) {
|
||||
if (disabledRules.has(item.id)) { continue; }
|
||||
if (!item.id || !item.severity || !item.description || !item.message) { continue; }
|
||||
|
||||
const severity = (['error', 'warning', 'info'].includes(item.severity)
|
||||
? (item.severity as Severity)
|
||||
: 'warning');
|
||||
|
||||
allRules.push({
|
||||
id: item.id,
|
||||
severity,
|
||||
@@ -152,6 +78,11 @@ export function loadActiveRules(workspaceRoot: string): CustomRule[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allRules;
|
||||
}
|
||||
|
||||
export function listRuleFiles(workspaceRoot: string): string[] {
|
||||
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
|
||||
if (!fs.existsSync(rulesDir)) { return []; }
|
||||
return fs.readdirSync(rulesDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
// import * as myExtension from '../../extension';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Sample test', () => {
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||
test('Extension registers codeReviewer commands', async () => {
|
||||
const ext = vscode.extensions.getExtension('undefined.vscode-code-reviewer')
|
||||
|| vscode.extensions.all.find(e => e.id.includes('vscode-code-reviewer'));
|
||||
if (ext && !ext.isActive) {
|
||||
await ext.activate();
|
||||
}
|
||||
|
||||
const commands = await vscode.commands.getCommands(false);
|
||||
const reviewerCommands = commands.filter(c => c.startsWith('codeReviewer.'));
|
||||
assert.ok(reviewerCommands.length > 0, 'No codeReviewer.* commands found');
|
||||
assert.ok(reviewerCommands.includes('codeReviewer.review'));
|
||||
assert.ok(reviewerCommands.includes('codeReviewer.openPanel'));
|
||||
});
|
||||
});
|
||||
|
||||
+11
-17
@@ -16,12 +16,8 @@
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function toggleRule(ruleId) {
|
||||
vscode.postMessage({ type: 'toggleRule', ruleId: ruleId });
|
||||
}
|
||||
|
||||
function deleteRule(ruleId) {
|
||||
vscode.postMessage({ type: 'deleteRule', ruleId: ruleId });
|
||||
function deleteFile(fileName) {
|
||||
vscode.postMessage({ type: 'deleteFile', fileName: fileName });
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -113,23 +109,22 @@
|
||||
|
||||
var ruleList = document.getElementById('ruleList');
|
||||
var countBadge = document.getElementById('ruleCountBadge');
|
||||
if (msg.rules && msg.rules.length > 0) {
|
||||
countBadge.textContent = msg.rules.length + ' 条';
|
||||
if (msg.ruleFiles && msg.ruleFiles.length > 0) {
|
||||
countBadge.textContent = msg.ruleFiles.length + ' 个文件';
|
||||
countBadge.className = 'badge badge-configured';
|
||||
ruleList.innerHTML = msg.rules.map(function (r) {
|
||||
ruleList.innerHTML = msg.ruleFiles.map(function (f) {
|
||||
return '<div class="rule-item">' +
|
||||
'<label class="switch"><input type="checkbox" onclick="toggleRule(\'' + r.id + '\')" ' + (r.severity !== 'info' ? 'checked' : '') + '><div class="switch-track"></div><div class="switch-thumb"></div></label>' +
|
||||
'<span class="rule-name">' + escapeHtml(r.id) + '</span>' +
|
||||
'<button class="rule-del" onclick="deleteRule(\'' + r.id + '\')">×</button></div>';
|
||||
'<span class="rule-name">' + escapeHtml(f) + '</span>' +
|
||||
'<button class="rule-del" onclick="deleteFile(\'' + escapeHtml(f) + '\')">×</button></div>';
|
||||
}).join('');
|
||||
} else {
|
||||
countBadge.textContent = '0 条';
|
||||
countBadge.textContent = '0 个文件';
|
||||
countBadge.className = 'badge badge-unconfigured';
|
||||
ruleList.innerHTML = '<div style="font-size:12px;color:#484f58;padding:8px 0;text-align:center;">暂无规则</div>';
|
||||
ruleList.innerHTML = '<div style="font-size:12px;color:#484f58;padding:8px 0;text-align:center;">暂无规则文件</div>';
|
||||
}
|
||||
|
||||
var step1Done = c.provider && c.model && c.apiKeyConfigured && c.baseUrlConfigured;
|
||||
var step2Done = msg.rules && msg.rules.length > 0;
|
||||
var step2Done = msg.ruleFiles && msg.ruleFiles.length > 0;
|
||||
var step3Done = msg.connectionTested && msg.connectionSuccess;
|
||||
var steps = [step1Done, step2Done, step3Done];
|
||||
for (var i = 0; i < steps.length; i++) {
|
||||
@@ -164,6 +159,5 @@
|
||||
|
||||
window.postMsg = postMsg;
|
||||
window.addRule = addRule;
|
||||
window.toggleRule = toggleRule;
|
||||
window.deleteRule = deleteRule;
|
||||
window.deleteFile = deleteFile;
|
||||
})();
|
||||
|
||||
+81
-67
@@ -4,8 +4,7 @@ import * as fs from 'fs';
|
||||
import { getAIProvider, getAIModel, getAIOutputLanguage, getAIConfig } from '../config/ai';
|
||||
import { getApiKey, setApiKey } from '../config/secret';
|
||||
import { createProvider, getAllProviderMeta, getProviderModels } from '../ai/factory';
|
||||
import { loadActiveRules } from '../rules/yaml-parser';
|
||||
import type { CustomRule } from '../types';
|
||||
import { listRuleFiles } from '../rules/yaml-parser';
|
||||
|
||||
const languageLabels: Record<string, string> = {
|
||||
'zh-CN': '中文(简体)',
|
||||
@@ -35,7 +34,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
const config = getAIConfig();
|
||||
const providers = getAllProviderMeta();
|
||||
const scriptUri = webviewView.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.context.extensionUri, 'out', 'views', 'setupView.js')
|
||||
vscode.Uri.joinPath(this.context.extensionUri, 'out', 'webview', 'setupView.js')
|
||||
);
|
||||
const aiConfig = getAIConfig();
|
||||
webviewView.webview.html = this.getHtml(providers, aiConfig, scriptUri);
|
||||
@@ -79,12 +78,8 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
case 'saveAndTest':
|
||||
await this.testConnection();
|
||||
break;
|
||||
case 'toggleRule':
|
||||
await this.toggleRule(msg.ruleId);
|
||||
await this.pushConfig();
|
||||
break;
|
||||
case 'deleteRule':
|
||||
await this.deleteRule(msg.ruleId);
|
||||
case 'deleteFile':
|
||||
await this.deleteFile(msg.fileName);
|
||||
await this.pushConfig();
|
||||
break;
|
||||
case 'addRule':
|
||||
@@ -105,13 +100,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
const config = getAIConfig();
|
||||
const apiKeyConfigured = await isApiKeyConfigured(this.context);
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';
|
||||
const rules = loadActiveRules(workspaceRoot);
|
||||
|
||||
const ruleItems = rules.map(r => ({
|
||||
id: r.id,
|
||||
severity: r.severity,
|
||||
description: r.description,
|
||||
}));
|
||||
const ruleFiles = listRuleFiles(workspaceRoot);
|
||||
|
||||
const baseUrlConfigured = isBaseUrlConfigured();
|
||||
|
||||
@@ -127,7 +116,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
apiKeyConfigured,
|
||||
},
|
||||
providers: getAllProviderMeta(),
|
||||
rules: ruleItems,
|
||||
ruleFiles,
|
||||
connectionTested: this.connectionTested,
|
||||
connectionSuccess: this.connectionSuccess,
|
||||
});
|
||||
@@ -152,6 +141,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
await provider.chat('回复 ok', 'ping', {
|
||||
model: config.model,
|
||||
temperature: 0,
|
||||
maxTokens: 1024,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
this.connectionTested = true;
|
||||
@@ -167,47 +157,13 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
await this.pushConfig();
|
||||
}
|
||||
|
||||
private async toggleRule(ruleId: string): Promise<void> {
|
||||
private async deleteFile(fileName: string): Promise<void> {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) {return;}
|
||||
|
||||
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
|
||||
if (!fs.existsSync(configPath)) {return;}
|
||||
|
||||
let content = fs.readFileSync(configPath, 'utf-8');
|
||||
const pattern = new RegExp(`^(\\s*${ruleId}\\s*:\\s*\\n\\s*enabled\\s*:\\s*)(true|false)`, 'm');
|
||||
|
||||
if (pattern.test(content)) {
|
||||
const match = pattern.exec(content);
|
||||
if (match) {
|
||||
const newValue = match[2] === 'true' ? 'false' : 'true';
|
||||
content = content.replace(pattern, `$1${newValue}`);
|
||||
}
|
||||
} else {
|
||||
content += `\n ${ruleId}:\n enabled: false\n`;
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, content, 'utf-8');
|
||||
}
|
||||
|
||||
private async deleteRule(ruleId: string): Promise<void> {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) {return;}
|
||||
|
||||
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
|
||||
if (!fs.existsSync(configPath)) {return;}
|
||||
|
||||
let content = fs.readFileSync(configPath, 'utf-8');
|
||||
const pattern = new RegExp(`^\\s*${ruleId}\\s*:\\n(?:\\s+.*\\n)*`, 'm');
|
||||
content = content.replace(pattern, '');
|
||||
fs.writeFileSync(configPath, content, 'utf-8');
|
||||
|
||||
const userRulesPath = path.join(workspaceRoot, '.code-review', 'rules', 'user-rules.yaml');
|
||||
if (fs.existsSync(userRulesPath)) {
|
||||
let rulesContent = fs.readFileSync(userRulesPath, 'utf-8');
|
||||
const rulePattern = new RegExp(`(?:^|\\n)- id: ${ruleId}\\n(?: .*\\n)*`, '');
|
||||
rulesContent = rulesContent.replace(rulePattern, '');
|
||||
fs.writeFileSync(userRulesPath, rulesContent, 'utf-8');
|
||||
const filePath = path.join(workspaceRoot, '.code-review', 'rules', fileName);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,27 +173,85 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) {return;}
|
||||
|
||||
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
|
||||
if (!fs.existsSync(configPath)) {
|
||||
fs.writeFileSync(configPath, 'rules:\n', 'utf-8');
|
||||
const result = await vscode.window.showOpenDialog({
|
||||
canSelectMany: false,
|
||||
openLabel: '选择 Markdown 规则文件',
|
||||
filters: { 'Markdown': ['md'] },
|
||||
});
|
||||
if (!result || result.length === 0) {return;}
|
||||
|
||||
const mdPath = result[0].fsPath;
|
||||
const mdContent = fs.readFileSync(mdPath, 'utf-8');
|
||||
if (!mdContent.trim()) {
|
||||
vscode.window.showErrorMessage('所选文件为空');
|
||||
return;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(configPath, 'utf-8');
|
||||
if (!content.includes('rules:')) {
|
||||
content += '\nrules:\n';
|
||||
const apiKey = await getApiKey(this.context);
|
||||
if (!apiKey) {
|
||||
vscode.window.showErrorMessage('请先在设置面板中配置 API Key');
|
||||
return;
|
||||
}
|
||||
|
||||
content += ` ${name}:\n enabled: true\n`;
|
||||
fs.writeFileSync(configPath, content, 'utf-8');
|
||||
|
||||
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
|
||||
if (!fs.existsSync(rulesDir)) {
|
||||
fs.mkdirSync(rulesDir, { recursive: true });
|
||||
}
|
||||
|
||||
const userRulesPath = path.join(rulesDir, 'user-rules.yaml');
|
||||
const ruleEntry = `\n- id: ${name}\n severity: warning\n description: 请编辑规则描述\n message: 违反规则,请修改\n`;
|
||||
fs.appendFileSync(userRulesPath, ruleEntry, 'utf-8');
|
||||
const yamlFileName = name.endsWith('.yaml') ? name : `${name}.yaml`;
|
||||
const yamlPath = path.join(rulesDir, yamlFileName);
|
||||
|
||||
if (fs.existsSync(yamlPath)) {
|
||||
vscode.window.showErrorMessage(`文件 ${yamlFileName} 已存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getAIConfig();
|
||||
const provider = createProvider(config.provider, apiKey, config.baseUrl);
|
||||
|
||||
const systemPrompt = `你是一个代码审查规则转换器。将用户提供的自然语言规则描述,转换为结构化的 YAML 格式,用于代码审查工具。
|
||||
|
||||
每条规则需要包含以下字段:
|
||||
- id: 规则唯一标识(kebab-case 英文)
|
||||
- severity: 严重级别(error / warning / info)
|
||||
- description: 规则简短描述(中文)
|
||||
- message: 违反时的提示消息(中文)
|
||||
- languages: 适用语言数组(可选,如 [javascript, typescript])
|
||||
|
||||
输出格式示例:
|
||||
- id: no-console-log
|
||||
severity: warning
|
||||
description: 禁止使用 console.log
|
||||
message: 请使用 logger 工具替代 console.log
|
||||
languages: [javascript, typescript]
|
||||
|
||||
仅输出 YAML,不要额外说明。`;
|
||||
|
||||
let yamlOutput: string;
|
||||
try {
|
||||
yamlOutput = await provider.chat(systemPrompt, mdContent, {
|
||||
model: config.model,
|
||||
temperature: 0.1,
|
||||
maxTokens: 4096,
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
vscode.window.showErrorMessage(`AI 生成规则失败: ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleaned = yamlOutput
|
||||
.replace(/```(yaml|yml)?\s*/gi, '')
|
||||
.replace(/```\s*$/gm, '')
|
||||
.trim();
|
||||
|
||||
if (!cleaned) {
|
||||
vscode.window.showErrorMessage('AI 返回内容为空');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(yamlPath, cleaned, 'utf-8');
|
||||
}
|
||||
|
||||
private async resetConfig(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user