feat: 规则导入 Converter 架构 + Excel/AI 转换 + 多项优化

- 提取 RuleConverter 接口及 5 个实现(Yaml/Md/Txt/ExcelConverter),
  由 ImportService 按扩展名路由,简化 setupView.ts addRule() 为单行调用
- Excel 导入:读取 xlsx → Markdown 表格 → AI 识别列头 → 生成 YAML
- 添加规则支持 yaml/yml/md/txt/xlsx/xls 多种格式
- 导出报告改为 QuickPick 双选(复制到剪贴板 / 下载 Markdown 文件)
- 修复测试连接成功后第三步圆圈不变绿的 bug
- 侧边栏标题改为「净码特工 · 代码审查 · 设置」
- 规则名称输入框增加提示文字
- 新增测试:adapter、config、merger、pipeline + fixtures/manual
- 依赖:新增 xlsx
This commit is contained in:
范智鹏
2026-07-21 22:16:52 +08:00
parent d4c3fb9df7
commit 1a050d7699
6 changed files with 182 additions and 107 deletions
+22 -2
View File
@@ -112,8 +112,28 @@ export function registerCommands(
return;
}
const markdown = reportToMarkdown(currentReport);
const doc = await vscode.workspace.openTextDocument({ content: markdown, language: 'markdown' });
await vscode.window.showTextDocument(doc);
const pick = await vscode.window.showQuickPick([
{ label: '📋 复制到剪贴板', description: '将报告内容以 Markdown 格式复制到剪贴板' },
{ label: '📄 下载 Markdown 文件', description: '将报告保存为 .md 文件' },
], { placeHolder: '选择导出方式' });
if (!pick) { return; }
if (pick.label.startsWith('📋')) {
await vscode.env.clipboard.writeText(markdown);
vscode.window.showInformationMessage('报告已复制到剪贴板');
} else {
const fileName = currentReport.filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, '') ?? 'review-report';
const defaultUri = vscode.workspace.workspaceFolders?.[0]
? vscode.Uri.joinPath(vscode.workspace.workspaceFolders[0].uri, `${fileName}-review.md`)
: undefined;
const uri = await vscode.window.showSaveDialog({
defaultUri,
filters: { 'Markdown': ['md'] },
title: '保存审查报告',
});
if (!uri) { return; }
await vscode.workspace.fs.writeFile(uri, Buffer.from(markdown, 'utf-8'));
vscode.window.showInformationMessage(`报告已保存到 ${uri.fsPath}`);
}
})
);
+22 -14
View File
@@ -20,6 +20,23 @@
vscode.postMessage({ type: 'deleteFile', fileName: fileName });
}
var _step1Done = false;
var _step2Done = false;
function updateSteps(step3Done) {
var steps = [_step1Done, _step2Done, step3Done];
for (var i = 0; i < steps.length; i++) {
var el = document.querySelector('.gs-step[data-step="' + (i + 1) + '"]');
if (!el) { continue; }
el.classList.remove('gs-step-done', 'gs-step-skip');
if (steps[i]) {
el.classList.add('gs-step-done');
} else if (step3Done) {
el.classList.add('gs-step-skip');
}
}
}
function escapeHtml(text) {
var d = document.createElement('div');
d.textContent = text;
@@ -123,20 +140,9 @@
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.ruleFiles && msg.ruleFiles.length > 0;
var step3Done = msg.connectionTested && msg.connectionSuccess;
var steps = [step1Done, step2Done, step3Done];
for (var i = 0; i < steps.length; i++) {
var el = document.querySelector('.gs-step[data-step="' + (i + 1) + '"]');
if (!el) { continue; }
el.classList.remove('gs-step-done', 'gs-step-skip');
if (steps[i]) {
el.classList.add('gs-step-done');
} else if (step3Done) {
el.classList.add('gs-step-skip');
}
}
_step1Done = c.provider && c.model && c.apiKeyConfigured && c.baseUrlConfigured;
_step2Done = msg.ruleFiles && msg.ruleFiles.length > 0;
updateSteps(msg.connectionTested && msg.connectionSuccess);
}
if (msg.type === 'testResult') {
@@ -152,6 +158,8 @@
} else {
btnTest.innerHTML = '✗ 重试';
}
updateSteps(msg.success);
}
});
+21 -65
View File
@@ -5,6 +5,11 @@ import { getAIProvider, getAIModel, getAIOutputLanguage, getAIConfig } from '../
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 { 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';
const languageLabels: Record<string, string> = {
'zh-CN': '中文(简体)',
@@ -16,8 +21,14 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
public connectionTested = false;
public connectionSuccess = false;
private importService = new ImportService();
constructor(private context: vscode.ExtensionContext) {}
constructor(private context: vscode.ExtensionContext) {
this.importService.registerConverter(new YamlConverter());
this.importService.registerConverter(new MdConverter());
this.importService.registerConverter(new TxtConverter());
this.importService.registerConverter(new ExcelConverter());
}
resolveWebviewView(
webviewView: vscode.WebviewView,
@@ -168,30 +179,19 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
}
private async addRule(name: string): Promise<void> {
if (!name.trim()) {return;}
if (!name.trim()) { return; }
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) {return;}
if (!workspaceRoot) { return; }
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: '选择 Markdown 规则文件',
filters: { 'Markdown': ['md'] },
openLabel: '选择规则文件',
filters: { '规则文件': ['yaml', 'yml', 'md', 'txt', 'xlsx', 'xls'] },
});
if (!result || result.length === 0) {return;}
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;
}
const apiKey = await getApiKey(this.context);
if (!apiKey) {
vscode.window.showErrorMessage('请先在设置面板中配置 API Key');
return;
}
const srcPath = result[0].fsPath;
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
if (!fs.existsSync(rulesDir)) {
@@ -206,52 +206,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
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');
await this.importService.convert(srcPath, yamlPath, this.context);
}
private async resetConfig(): Promise<void> {
@@ -485,7 +440,7 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
代码审查 · 设置
净码特工 · 代码审查 · 设置
</div>
<!-- 1. 快速开始 -->
@@ -613,6 +568,7 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
<input type="text" id="newRuleInput" placeholder="输入规则名称...">
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="addRule()">+ 添加</button>
</div>
<div class="field-hint">建议使用英文名称,无需输入 .yaml 后缀(如 security-rules</div>
</div>
</div>
</div>