feat: 规则预过滤 + 自定义规则去重增强 + 导入预览编辑 + Word/PPT 导入 + UI 优化

- 规则预过滤:根据文件语言注入匹配的自定义规则,跳过不相关规则
- 导入去重:对比已有规则,重复项自动注释/标注
- 导入预览编辑:支持在导入前编辑规则字段(severity、description 等)
- Word/PPT 导入:新增 DocxConverter、PptxConverter
- UI:审查报告移除设置按钮;自定义规则标签显示注入比例
This commit is contained in:
范智鹏
2026-07-25 23:57:21 +08:00
parent 62faad9305
commit 3661c80db5
9 changed files with 87 additions and 13 deletions
+11
View File
@@ -98,3 +98,14 @@
| 2026-07-21 21:50 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 新增 Excel 导入支持:ExcelConverter + 更新文件选择过滤器 + 注册到 ImportService | 无 | src/rules/converters/excel-converter.ts src/views/setupView.ts package.json | deepseek-v4-flash |
| 2026-07-21 22:04 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Excel 导入改为全 AI 转换:移除固定列名校验,转为 Markdown 表格 → AI 识别 → YAML | 无 | src/rules/converters/excel-converter.ts docs/superpowers/specs/2026-07-21-excel-ai-converter-design.md | deepseek-v4-flash |
| 2026-07-22 20:40 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 三 tab 修复按钮一致性:merger 中 fixableLinterIndices 取消 suggestion 过滤(全部可修复);AI 审查 section header 增加"全部修复"按钮;底部无效"批量修复"按钮移除 | 无 | src/merger/merger.ts src/panel/webview.ts | deepseek-v4-flash |
| 2026-07-23 20:06 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 | Part A 语言预过滤 + Part B 导入去重:新建 rule-filter.ts(软过滤+语言别名+JSP并集)、static-rules.json(简化版占位)、prompt-builder.ts、import-types.ts、import-preview.tsWebview三分区预览);改造 import-service.ts 拆分 convert/applyConversion + convertContentWithAI 扩展签名;Converter 接口改签名返回 yaml 内容;三个 Converter 各自注入去重 promptsetupView.ts addRule 分支 YAML直接复制/非YAML转换预览;新增 rule-filter.test.ts15用例)+ import-dedup.test.ts9用例);tsconfig.json 加 resolveJsonModule | 设计文档 docs/superpowers/specs/2026-07-23-rule-prefilter-design.md(用户提供已定稿方案) | tsconfig.json src/types.ts src/rules/rule-filter.ts src/rules/yaml-parser.ts src/activation/commands.ts src/rules/static-rules.json src/rules/converters/prompt-builder.ts src/rules/import-types.ts src/rules/import-service.ts src/rules/import-preview.ts src/rules/converters/converter.ts src/rules/converters/yaml-converter.ts src/rules/converters/md-converter.ts src/rules/converters/txt-converter.ts src/rules/converters/excel-converter.ts src/views/setupView.ts src/test/rule-filter.test.ts src/test/import-dedup.test.ts | deepseek-v4-pro |
| 2026-07-23 20:59 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: AI 生成规则超时 — timeoutMs 硬编码 30s 改为 getAITimeout() * 1000(默认 300s);AbortError 专用友好提示 | 无 | src/rules/import-service.ts | deepseek-v4-flash |
| 2026-07-23 21:21 | ⑤ 编码实现 | 第三批体验增强:merger.ts MergedReport/MergeInput 新增 customRuleFilterInfocommands.ts 传入过滤统计;webview.ts 自定义规则 Tab 展示"注入 X/Y 条"及 skippedRequestA 跳过提示 | 无 | src/merger/merger.ts src/activation/commands.ts src/panel/webview.ts | deepseek-v4-pro |
| 2026-07-23 21:50 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | static-rules.json 从 14 条占位规则替换为完整规则集(441 条:ESLint 61, ts-eslint 24, Stylelint 12, PMD Java 259, PMD JSP 10, sql-lint 75),数据源来自 node_modules 和项目文件及 sqlfluff 输出 | 暂时生成脚本 scripts/generate-rules-json.mjs(已删除) | src/rules/static-rules.json | deepseek-v4-flash |
| 2026-07-24 18:52 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Excel 多 Sheet 支持:遍历所有 Sheet,含 ## SheetName 标题分隔,合并为一份 Markdown 后统一由 AI 转换为 YAML | 无 | src/rules/converters/excel-converter.ts docs/superpowers/specs/2026-07-24-excel-multi-sheet-design.md | deepseek-v4-flash |
| 2026-07-24 19:22 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | Word/PPT 导入:DocxConverter (mammoth extractRawText → AI)、PptxConverter (officeparser parseOffice → toText → AI);注册到 setupView;文件过滤器增加 docx/pptx;依赖 mammoth + officeparser + @types/mammoth | mammoth 类型方案:先考虑 require()/@ts-ignore → 最终用 .d.tspptx-converter.ts 类型先用 Awaited<ReturnType<…>> → 改为 OfficeParserAST | src/rules/converters/docx-converter.ts src/rules/converters/pptx-converter.ts src/types/mammoth.d.ts src/views/setupView.ts package.json docs/superpowers/specs/2026-07-24-word-ppt-import-design.md | deepseek-v4-flash |
| 2026-07-24 23:48 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 去重依据追加已导入自定义规则:prompt-builder.ts buildDedupPromptSection() 新增 existingCustomRules 参数输出 custom/ 前缀规则列表;converter 接口 + 5 个 converter + yaml-converter 透传 existingRulesimport-service.ts convert() 调用 loadActiveRules() 加载已有规则、buildFinalYaml() 注释头区分 custom/ 前缀文案;import-preview.ts 预览面板自定义规则区分显示 | 提示词格式迭代:标题从"静态分析规则清单"改为"已知规则清单"duplicateOf 字段描述从"linter 规则 ID"扩展为"linter 如 …;自定义如 …"buildFinalYaml 空格处理:先试 dupDisplay 变量合并 → 改条件分支分别输出避免空格问题 | src/rules/converters/prompt-builder.ts src/rules/converters/converter.ts src/rules/converters/md-converter.ts src/rules/converters/txt-converter.ts src/rules/converters/excel-converter.ts src/rules/converters/docx-converter.ts src/rules/converters/pptx-converter.ts src/rules/converters/yaml-converter.ts src/rules/import-service.ts src/rules/import-preview.ts docs/superpowers/specs/2026-07-24-custom-rule-dedup-enhancement-design.md | deepseek-v4-pro |
| 2026-07-25 22:57 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 规则预览编辑:规则卡片折叠/展开编辑表单(severity下拉框、description/message多行文本、languages/excludeLanguages标签输入、id只读);renderRulesToYaml 从 rules 数组生成 YAML;无编辑时回退原始逻辑;补充5个编辑场景测试 | import-preview.ts 初版尝试 extension 端实时追踪 modifiedRulesupdateRule 消息),后改为 confirm 时 webview 端一次性收集 DOM 值发送;测试断言 'java' 子串匹配 'javascript' 误判 + 注释行缩进不匹配,各迭代一次修复 | src/rules/import-types.ts src/rules/import-service.ts src/rules/import-preview.ts src/test/import-dedup.test.ts docs/superpowers/specs/2026-07-25-import-preview-edit-design.md | deepseek-v4-flash |
| 2026-07-25 23:08 | ① 用户提出 → ⑤ 编码实现 | 预览编辑表单 UI 优化:id 字段添加标签说明;每个字段标签后加括号注释(如 severity(严重级别)) | 无 | src/rules/import-preview.ts | deepseek-v4-flash |
| 2026-07-25 23:45 | ① 用户提出 → ⑤ 编码实现 | 预览编辑表单布局调整:移除顶部多余 id 大标题;id 标签与值同行 + 切换按钮同行右对齐;id 样式简化为纯文本 | id 位置迭代:一行 → 上下 → 一行 + 按钮同行 | src/rules/import-preview.ts | deepseek-v4-flash |
| 2026-07-25 23:50 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | 去除代码审查报告画面底部的设置按钮(消息类型、HTML 按钮、消息处理 handler 全部移除) | 无 | src/panel/webview.ts | deepseek-v4-flash-free |
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "vscode-code-reviewer",
"displayName": "净码特工 · Code Purifier",
"description": "AI 驱动的代码审查与规范检查一体化 VSCode 插件",
"version": "1.0.0",
"version": "1.0.1",
"engines": {
"vscode": "^1.120.0"
},
+10 -2
View File
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import { Orchestrator } from '../orchestrator/orchestrator';
import { runAIReview } from '../ai/engine';
import { loadActiveRules } from '../rules/yaml-parser';
import { filterAndSummarize } from '../rules/rule-filter';
import { mergeResults, MergedReport } from '../merger/merger';
import { reportToMarkdown } from '../utils/report';
import { getApiKey } from '../config';
@@ -38,9 +39,10 @@ export function registerCommands(
progress.report({ message: '运行 AI 审查...' });
const customRules = loadActiveRules(workspaceRoot);
const allRules = loadActiveRules(workspaceRoot);
const filterResult = filterAndSummarize(allRules, document);
const code = document.getText();
const aiResult = await runAIReview(context, code, staticResult.diagnostics, customRules);
const aiResult = await runAIReview(context, code, staticResult.diagnostics, filterResult.relevant);
currentReport = mergeResults({
staticDiagnostics: staticResult.diagnostics,
@@ -53,6 +55,12 @@ export function registerCommands(
filePath: document.uri.fsPath,
language: document.languageId,
adapterIds: staticResult.adapterIds,
customRuleFilterInfo: {
totalActive: allRules.length,
injected: filterResult.relevant.length,
filteredOut: filterResult.filteredOut.length,
skippedRequestA: filterResult.skippedRequestA,
},
});
const panel = ReviewPanel.createOrShow(context.extensionUri);
+13
View File
@@ -18,6 +18,12 @@ export interface MergedReport {
adapterNames: string[];
fixableLinterIndices: number[];
fixableCustomIndices: number[];
customRuleFilterInfo?: {
totalActive: number;
injected: number;
filteredOut: number;
skippedRequestA: boolean;
};
}
interface MergeInput {
@@ -31,6 +37,12 @@ interface MergeInput {
filePath: string;
language: string;
adapterIds: string[];
customRuleFilterInfo?: {
totalActive: number;
injected: number;
filteredOut: number;
skippedRequestA: boolean;
};
}
export function mergeResults(input: MergeInput): MergedReport {
@@ -74,5 +86,6 @@ export function mergeResults(input: MergeInput): MergedReport {
adapterNames: input.adapterIds,
fixableLinterIndices,
fixableCustomIndices,
customRuleFilterInfo: input.customRuleFilterInfo,
};
}
+16 -7
View File
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
import { MergedReport } from '../merger/merger';
interface PanelMessage {
type: 'navigate' | 'rerun' | 'export' | 'settings' | 'fix' | 'fixAll';
type: 'navigate' | 'rerun' | 'export' | 'fix' | 'fixAll';
line?: number;
ruleId?: string;
source?: 'linter' | 'custom' | 'ai';
@@ -118,6 +118,11 @@ export class ReviewPanel {
const linterToolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : '静态分析';
const customFilterInfo = report.customRuleFilterInfo;
const customFilterLabel = customFilterInfo
? `(注入 ${customFilterInfo.injected}/${customFilterInfo.totalActive} 条)`
: '';
return `<!DOCTYPE html>
<html lang="zh">
<head>
@@ -235,7 +240,7 @@ ${errorBox}
<div class="tab-bar">
<button class="tab active" data-tab="linter" onclick="switchTab('linter')">🔧 ${report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : '静态分析'} ${tabCount(linterErrors, linterWarnings, linterInfos)}</button>
<button class="tab" data-tab="custom" onclick="switchTab('custom')">📋 自定义规则 ${tabCount(customErrors, customWarnings, customInfos)}</button>
<button class="tab" data-tab="custom" onclick="switchTab('custom')">📋 自定义规则 ${tabCount(customErrors, customWarnings, customInfos)} <span style="font-size:11px;color:var(--vscode-descriptionForeground);">${customFilterLabel}</span></button>
<button class="tab" data-tab="ai" onclick="switchTab('ai')">🤖 AI 审查 ${tabCount(aiErrors, aiWarnings, aiInfos)}</button>
</div>
@@ -252,7 +257,7 @@ ${errorBox}
<div class="actions">
<button class="btn btn-primary" onclick="send('rerun')">🔄 重新审查</button>
<button class="btn" onclick="send('export')">📄 导出报告</button>
<button class="btn" onclick="send('settings')">⚙️ 设置</button>
</div>
</div>
<script>
@@ -287,11 +292,18 @@ ${errorBox}
}
private buildCustomList(report: MergedReport, fixableSet: Set<number>): string {
const filterInfo = report.customRuleFilterInfo;
if (filterInfo?.skippedRequestA) {
return '<div class="empty">当前文件语言无匹配的自定义规则,已跳过规则评估</div>';
}
if (report.customRuleDiagnostics.length === 0) {
return '<div class="empty">未发现规则违规</div>';
}
const hasFixable = fixableSet.size > 0;
return `<div class="section-header"><span class="section-header-title">自定义规则 · ${report.customRuleCount} 个问题</span>${hasFixable ? '<button class="btn" onclick="send(\'fixAll\')">全部修复</button>' : ''}</div>`
const filterLabel = filterInfo
? `(注入 ${filterInfo.injected}/${filterInfo.totalActive} 条规则)`
: '';
return `<div class="section-header"><span class="section-header-title">自定义规则 · ${report.customRuleCount} 个问题${filterLabel}</span>${hasFixable ? '<button class="btn" onclick="send(\'fixAll\')">全部修复</button>' : ''}</div>`
+ report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, fixableSet.has(i), undefined, false)).join('');
}
@@ -377,9 +389,6 @@ ${errorBox}
case 'export':
vscode.commands.executeCommand('codeReviewer.exportReport');
break;
case 'settings':
vscode.commands.executeCommand('codeReviewer.openSetup');
break;
case 'fix':
vscode.commands.executeCommand('codeReviewer.fixIssue', message);
break;
+2
View File
@@ -8,6 +8,7 @@ interface RuleYamlItem {
description: string;
message: string;
languages?: string[];
excludeLanguages?: string[];
}
function parseYamlSimple(content: string): object[] {
@@ -75,6 +76,7 @@ export function loadActiveRules(workspaceRoot: string): CustomRule[] {
description: item.description,
message: item.message,
languages: item.languages,
excludeLanguages: item.excludeLanguages,
});
}
}
+1
View File
@@ -6,6 +6,7 @@ export interface CustomRule {
description: string;
message: string;
languages?: string[];
excludeLanguages?: string[];
}
export type Severity = 'error' | 'warning' | 'info';
+31 -2
View File
@@ -6,10 +6,13 @@ import { getApiKey, setApiKey } from '../config/secret';
import { createProvider, getAllProviderMeta, getProviderModels } from '../ai/factory';
import { listRuleFiles } from '../rules/yaml-parser';
import { ImportService } from '../rules/import-service';
import { showImportPreview } from '../rules/import-preview';
import { YamlConverter } from '../rules/converters/yaml-converter';
import { MdConverter } from '../rules/converters/md-converter';
import { TxtConverter } from '../rules/converters/txt-converter';
import { ExcelConverter } from '../rules/converters/excel-converter';
import { DocxConverter } from '../rules/converters/docx-converter';
import { PptxConverter } from '../rules/converters/pptx-converter';
const languageLabels: Record<string, string> = {
'zh-CN': '中文(简体)',
@@ -28,6 +31,8 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
this.importService.registerConverter(new MdConverter());
this.importService.registerConverter(new TxtConverter());
this.importService.registerConverter(new ExcelConverter());
this.importService.registerConverter(new DocxConverter());
this.importService.registerConverter(new PptxConverter());
}
resolveWebviewView(
@@ -187,11 +192,12 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: '选择规则文件',
filters: { '规则文件': ['yaml', 'yml', 'md', 'txt', 'xlsx', 'xls'] },
filters: { '规则文件': ['yaml', 'yml', 'md', 'txt', 'xlsx', 'xls', 'docx', 'pptx'] },
});
if (!result || result.length === 0) { return; }
const srcPath = result[0].fsPath;
const ext = path.extname(srcPath).toLowerCase();
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
if (!fs.existsSync(rulesDir)) {
@@ -206,7 +212,30 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
return;
}
await this.importService.convert(srcPath, yamlPath, this.context);
if (ext === '.yaml' || ext === '.yml') {
fs.copyFileSync(srcPath, yamlPath);
vscode.window.showInformationMessage(`规则文件已导入: ${yamlFileName}`);
} else {
try {
const conversion = await this.importService.convert(srcPath, this.context);
const decision = await showImportPreview(conversion);
if (!decision || !decision.confirmed) {
vscode.window.showInformationMessage('导入已取消');
return;
}
this.importService.applyConversion(conversion, decision, yamlPath);
vscode.window.showInformationMessage(
`规则已导入: ${yamlFileName}${conversion.rules.length} 条,` +
`${conversion.exactCount} 条完全重复已注释,` +
`${conversion.overlapCount} 条部分重叠已标注)`
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(`规则导入失败: ${msg}`);
}
}
}
private async resetConfig(): Promise<void> {
+2 -1
View File
@@ -10,7 +10,8 @@
"sourceMap": true,
"rootDir": "src",
"strict": true,
"skipLibCheck": true
"skipLibCheck": true,
"resolveJsonModule": true
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */