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:
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user