i18n 国际化全量接入 + 修复与 UI 增强
- 引入 i18n 系统,所有用户可见字符串替换为 t() 调用,支持中/EN/日三语 - 插件激活时读取 ai.outputLanguage 配置初始化语言,onDidChangeConfiguration 监听变更自动切换 - ReviewPanel 与 SetupViewProvider 注册 onLanguageChange 回调,语言切换时全量重渲染 - package.json description:"AI 输出语言" -> "插件语言" - 修复 repairJsonEscapes 无差别解引号导致 JSON 解析失败 - 设置页规则名称空值时输入框红框 + 错误提示 - 规则导入流程添加 withProgress 加载提示
This commit is contained in:
+7
-1
@@ -1,4 +1,4 @@
|
||||
# AI Usage Log
|
||||
# AI Usage Log
|
||||
|
||||
| 日期时间 | 范式步骤 | 修改摘要 | 中间产物 | 涉及文件 | 使用模型 |
|
||||
|---------|---------|---------|---------|---------|---------|
|
||||
@@ -109,3 +109,9 @@
|
||||
| 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 |
|
||||
| 2026-07-26 00:43 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | i18n 国际化:复用 ai.outputLanguage 控制 UI 语言;src/i18n/messages.ts 单文件注册 ~100 条文案(Language 类型、t/setLanguage/onLanguageChange/getLanguage);配置变更 → onDidChangeConfiguration → 自动切换;webview 全量重渲染 + 语言下拉即时生效;commands.ts/report.ts/setupView.ts/webview.ts/import-preview.ts + 适配器/AI引擎/converter 错误消息全部替换为 t();提示词与 package.json 保持不变 | 提示词翻译 vs 不翻译(最终不翻译);方案 A 单文件 vs 方案 B 多 JSON(最终选 A);语言切换同步 setLanguage vs 纯配置驱动(最终选配置驱动单一入口);import-preview.ts 尝试用 esc() 导致编译错误 → 改为直接传入 sourceFileName | docs/superpowers/specs/2026-07-26-i18n-design.md src/i18n/messages.ts src/test/messages.test.ts src/extension.ts src/activation/commands.ts src/panel/webview.ts src/views/setupView.ts src/views/import-preview.ts src/utils/report.ts src/adapters/pmd.ts src/adapters/sql-lint.ts src/ai/engine.ts src/ai/providers/openai-compatible.ts src/rules/import-service.ts src/rules/converters/excel-converter.ts src/rules/converters/docx-converter.ts src/rules/converters/pptx-converter.ts | deepseek-v4-pro |
|
||||
| 2026-07-26 13:00 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 语言设置标签修正:节标题"输出语言"→"语言"、字段提示"AI 审查结果输出语言"→"更改插件的显示语言"、package.json description"AI 输出语言"→"插件语言"(三语同步更新) | 无 | src/i18n/messages.ts package.json | deepseek-v4-flash |
|
||||
| 2026-07-26 13:27 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | i18n 补全:report 面板(injectedCount/injectedRules/issuesCount/itemsCount)和 import-preview(全部文本标签/统计/校验消息)共 26 条新键三语,替换 webview.ts 4 处 + import-preview.ts 全部硬编码中文 | 无 | src/i18n/messages.ts src/panel/webview.ts src/rules/import-preview.ts | deepseek-v4-flash |
|
||||
| 2026-07-26 14:16 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | AI 转换器输出语言修复:5 个 converter(md/txt/excel/docx/pptx)和 import-service.ts fallback prompt 去掉字段描述中的(中文)硬编码,追加 `\n输出语言:${getAIOutputLanguage()}` | 无 | 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/import-service.ts | deepseek-v4-flash |
|
||||
| 2026-07-26 14:54 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 规则名称空值时输入框红框+错误提示文字 | 无 | src/i18n/messages.ts src/views/setupView.ts src/views/setupView.js | deepseek-v4-flash-free |
|
||||
| 2026-07-26 15:26 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | repairJsonEscapes 去掉 replace 无差别解引号,防止 AI 正确转义的 \" 被破坏导致 JSON 解析失败 | 无 | src/ai/engine.ts | deepseek-v4-flash-free |
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@
|
||||
"en",
|
||||
"ja"
|
||||
],
|
||||
"description": "AI 输出语言"
|
||||
"description": "插件语言"
|
||||
},
|
||||
"vscode-code-reviewer.linters.javascript": {
|
||||
"type": "string",
|
||||
|
||||
+24
-23
@@ -7,6 +7,7 @@ import { mergeResults, MergedReport } from '../merger/merger';
|
||||
import { reportToMarkdown } from '../utils/report';
|
||||
import { getApiKey } from '../config';
|
||||
import { ReviewPanel } from '../panel/webview';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
let currentReport: MergedReport | null = null;
|
||||
|
||||
@@ -19,7 +20,7 @@ export function registerCommands(
|
||||
vscode.commands.registerCommand('codeReviewer.review', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showWarningMessage('请先打开一个文件');
|
||||
vscode.window.showWarningMessage(t('review.noEditor'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,15 +30,15 @@ export function registerCommands(
|
||||
|
||||
await vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: '正在审查...',
|
||||
title: t('review.running'),
|
||||
cancellable: false,
|
||||
}, async (progress) => {
|
||||
progress.report({ message: '运行静态分析...' });
|
||||
progress.report({ message: t('review.staticAnalysis') });
|
||||
|
||||
const startTime = Date.now();
|
||||
const staticResult = await orchestrator.runStaticAnalysis(document, workingDir);
|
||||
|
||||
progress.report({ message: '运行 AI 审查...' });
|
||||
progress.report({ message: t('review.aiReview') });
|
||||
|
||||
const allRules = loadActiveRules(workspaceRoot);
|
||||
const filterResult = filterAndSummarize(allRules, document);
|
||||
@@ -76,14 +77,14 @@ export function registerCommands(
|
||||
|
||||
const selection = editor.selection;
|
||||
if (selection.isEmpty) {
|
||||
vscode.window.showWarningMessage('请先选中要审查的代码');
|
||||
vscode.window.showWarningMessage(t('review.noSelection'));
|
||||
return;
|
||||
}
|
||||
|
||||
const code = editor.document.getText(selection);
|
||||
const apiKey = await getApiKey(context);
|
||||
if (!apiKey) {
|
||||
vscode.window.showWarningMessage('请先在设置面板中配置 API Key');
|
||||
vscode.window.showWarningMessage(t('review.needApiKey'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,12 +93,12 @@ export function registerCommands(
|
||||
|
||||
await vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: '审查选中代码...',
|
||||
title: t('review.reviewingSelection'),
|
||||
cancellable: false,
|
||||
}, async () => {
|
||||
const aiResult = await runAIReview(context, code, [], customRules);
|
||||
vscode.window.showInformationMessage(
|
||||
`选中代码审查完成: ${aiResult.customRuleResults.length + aiResult.findings.length} 个问题`
|
||||
t('review.selectionComplete', { 0: String(aiResult.customRuleResults.length + aiResult.findings.length) })
|
||||
);
|
||||
});
|
||||
})
|
||||
@@ -116,18 +117,18 @@ export function registerCommands(
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('codeReviewer.exportReport', async () => {
|
||||
if (!currentReport) {
|
||||
vscode.window.showWarningMessage('请先运行完整审查生成报告');
|
||||
vscode.window.showWarningMessage(t('export.needRunFirst'));
|
||||
return;
|
||||
}
|
||||
const markdown = reportToMarkdown(currentReport);
|
||||
const pick = await vscode.window.showQuickPick([
|
||||
{ label: '📋 复制到剪贴板', description: '将报告内容以 Markdown 格式复制到剪贴板' },
|
||||
{ label: '📄 下载 Markdown 文件', description: '将报告保存为 .md 文件' },
|
||||
], { placeHolder: '选择导出方式' });
|
||||
{ label: t('export.copyToClipboard'), description: t('export.copyDescription') },
|
||||
{ label: t('export.downloadMarkdown'), description: t('export.saveDescription') },
|
||||
], { placeHolder: t('export.selectMethod') });
|
||||
if (!pick) { return; }
|
||||
if (pick.label.startsWith('📋')) {
|
||||
if (pick.label === t('export.copyToClipboard')) {
|
||||
await vscode.env.clipboard.writeText(markdown);
|
||||
vscode.window.showInformationMessage('报告已复制到剪贴板');
|
||||
vscode.window.showInformationMessage(t('export.copied'));
|
||||
} else {
|
||||
const fileName = currentReport.filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, '') ?? 'review-report';
|
||||
const defaultUri = vscode.workspace.workspaceFolders?.[0]
|
||||
@@ -136,11 +137,11 @@ export function registerCommands(
|
||||
const uri = await vscode.window.showSaveDialog({
|
||||
defaultUri,
|
||||
filters: { 'Markdown': ['md'] },
|
||||
title: '保存审查报告',
|
||||
title: t('export.saveDialogTitle'),
|
||||
});
|
||||
if (!uri) { return; }
|
||||
await vscode.workspace.fs.writeFile(uri, Buffer.from(markdown, 'utf-8'));
|
||||
vscode.window.showInformationMessage(`报告已保存到 ${uri.fsPath}`);
|
||||
vscode.window.showInformationMessage(t('export.saved', { 0: uri.fsPath }));
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -149,22 +150,22 @@ export function registerCommands(
|
||||
vscode.commands.registerCommand('codeReviewer.addCustomRule', () => {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) {
|
||||
vscode.window.showWarningMessage('请先打开工作区');
|
||||
vscode.window.showWarningMessage(t('setup.noWorkspace'));
|
||||
return;
|
||||
}
|
||||
vscode.window.showInformationMessage('请在设置面板中管理自定义规则');
|
||||
vscode.window.showInformationMessage(t('setup.manageRulesHint'));
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('codeReviewer.fixIssue', () => {
|
||||
vscode.window.showInformationMessage('单条修复功能开发中');
|
||||
vscode.window.showInformationMessage(t('review.fixNotAvailable'));
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('codeReviewer.fixAll', () => {
|
||||
vscode.window.showInformationMessage('批量修复功能开发中');
|
||||
vscode.window.showInformationMessage(t('review.fixAllNotAvailable'));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -174,10 +175,10 @@ export function registerCommands(
|
||||
await vscode.commands.executeCommand('workbench.view.extension.code-reviewer');
|
||||
} catch {
|
||||
const action = await vscode.window.showErrorMessage(
|
||||
'无法打开设置面板',
|
||||
'打开设置 (JSON)'
|
||||
t('setup.openSetupFail'),
|
||||
t('setup.openSettingsJson')
|
||||
);
|
||||
if (action === '打开设置 (JSON)') {
|
||||
if (action === t('setup.openSettingsJson')) {
|
||||
await vscode.commands.executeCommand('workbench.action.openSettingsJson');
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import { existsSync } from 'fs';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import type { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
|
||||
import { getPMDRulesetPath } from '../config';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
export class PmdAdapter implements LinterAdapter {
|
||||
id = 'pmd';
|
||||
@@ -57,7 +58,7 @@ export class PmdAdapter implements LinterAdapter {
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
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: 'tool-unavailable', errorMessage: t('adapter.javaNotInstalled') };
|
||||
}
|
||||
return { diagnostics: [], status: 'execution-failed', errorMessage: message };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { spawn } from 'child_process';
|
||||
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
const DIALECT_MAP: Record<string, string> = {
|
||||
sql: 'ansi',
|
||||
@@ -95,7 +96,7 @@ export class SqlLintAdapter implements LinterAdapter {
|
||||
return {
|
||||
diagnostics: [],
|
||||
status: 'tool-unavailable',
|
||||
errorMessage: 'sqlfluff 未安装,请执行 pip install sqlfluff',
|
||||
errorMessage: t('adapter.sqlfluffNotInstalled'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
+12
-12
@@ -9,6 +9,7 @@ import type {
|
||||
TranslatedDiagnostic,
|
||||
AIFinding,
|
||||
} from './schema';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
function buildCustomRulePrompt(rules: CustomRule[]): string {
|
||||
return rules.map(r =>
|
||||
@@ -27,22 +28,21 @@ function addLineNumbers(code: string): string {
|
||||
}
|
||||
|
||||
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];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str[i];
|
||||
if (ch === '\\') {
|
||||
out += ch;
|
||||
if (i + 1 < s.length) { out += s[++i]; }
|
||||
if (i + 1 < str.length) { out += str[++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])) {
|
||||
while (j < str.length && str[j] === ' ') { j++; }
|
||||
if (j < str.length && ':,\]}'.includes(str[j])) {
|
||||
inString = false;
|
||||
out += ch;
|
||||
} else {
|
||||
@@ -109,7 +109,7 @@ export async function runAIReview(
|
||||
translatedDiagnostics: [],
|
||||
findings: [],
|
||||
degraded: true,
|
||||
error: '未配置 API Key',
|
||||
error: t('adapter.noApiKey'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export async function runAIReview(
|
||||
translatedDiagnostics: [],
|
||||
findings: [],
|
||||
degraded: true,
|
||||
error: `创建 Provider 失败: ${err instanceof Error ? err.message : String(err)}`,
|
||||
error: t('adapter.createProviderFail', { 0: err instanceof Error ? err.message : String(err) }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,10 +166,10 @@ export async function runAIReview(
|
||||
ruleId: `custom:${r.ruleId}`,
|
||||
}));
|
||||
} catch (e) {
|
||||
errors.push(`自定义规则响应解析失败: ${e instanceof Error ? e.message : String(e)}`);
|
||||
errors.push(t('adapter.customRuleParseFail', { 0: e instanceof Error ? e.message : String(e) }));
|
||||
}
|
||||
} else {
|
||||
errors.push(`自定义规则请求失败: ${resultA.reason}`);
|
||||
errors.push(t('adapter.customRuleRequestFail', { 0: resultA.reason }));
|
||||
}
|
||||
|
||||
let translatedDiagnostics: TranslatedDiagnostic[] = [];
|
||||
@@ -183,10 +183,10 @@ export async function runAIReview(
|
||||
translatedDiagnostics = parsed.translatedDiagnostics ?? [];
|
||||
findings = parsed.findings ?? [];
|
||||
} catch (e) {
|
||||
errors.push(`AI 审查响应解析失败: ${e instanceof Error ? e.message : String(e)}`);
|
||||
errors.push(t('adapter.aiReviewParseFail', { 0: e instanceof Error ? e.message : String(e) }));
|
||||
}
|
||||
} else {
|
||||
errors.push(`AI 审查请求失败: ${resultB.reason}`);
|
||||
errors.push(t('adapter.aiReviewRequestFail', { 0: resultB.reason }));
|
||||
}
|
||||
|
||||
const degraded = errors.length > 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AIProvider, ChatOptions } from './base';
|
||||
import { t } from '../../i18n/messages';
|
||||
|
||||
export class OpenAICompatibleProvider extends AIProvider {
|
||||
id: string;
|
||||
@@ -40,7 +41,7 @@ export class OpenAICompatibleProvider extends AIProvider {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
if (response.status === 401) {
|
||||
throw new Error('API Key 无效,请重新设置');
|
||||
throw new Error(t('adapter.invalidApiKey'));
|
||||
}
|
||||
throw new Error(`API 请求失败 (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
+14
-1
@@ -2,11 +2,15 @@ import * as vscode from 'vscode';
|
||||
import { Orchestrator } from './orchestrator/orchestrator';
|
||||
import { registerCommands } from './activation/commands';
|
||||
import { SetupViewProvider } from './views/setupView';
|
||||
import { setLanguage, t, type Language } from './i18n/messages';
|
||||
import { getAIOutputLanguage } from './config';
|
||||
|
||||
let orchestrator: Orchestrator;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('净码特工 · Code Purifier 已激活');
|
||||
const lang = getAIOutputLanguage() as Language;
|
||||
setLanguage(lang);
|
||||
console.log(t('extension.activated'));
|
||||
|
||||
orchestrator = new Orchestrator();
|
||||
|
||||
@@ -35,6 +39,15 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
debounceTimers.set(key, timer);
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('vscode-code-reviewer.ai.outputLanguage')) {
|
||||
const newLang = getAIOutputLanguage() as Language;
|
||||
setLanguage(newLang);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
|
||||
+47
-29
@@ -1,5 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { MergedReport } from '../merger/merger';
|
||||
import { t, onLanguageChange, getLanguage } from '../i18n/messages';
|
||||
|
||||
interface PanelMessage {
|
||||
type: 'navigate' | 'rerun' | 'export' | 'fix' | 'fixAll';
|
||||
@@ -19,10 +20,16 @@ function svgIcon(): string {
|
||||
const SVG_HEADER_ICON = svgIcon();
|
||||
|
||||
const BADGE_CLASS: Record<string, string> = { linter: 'badge-linter', custom: 'badge-custom', ai: 'badge-ai' };
|
||||
const BADGE_LABEL: Record<string, string> = { linter: 'Linter', custom: '自定义', ai: 'AI' };
|
||||
|
||||
function badgeHtml(source: string): string {
|
||||
return `<span class="item-badge ${BADGE_CLASS[source] || 'badge-linter'}">${BADGE_LABEL[source] || source}</span>`;
|
||||
let label: string;
|
||||
switch (source) {
|
||||
case 'linter': label = t('report.sourceLinter'); break;
|
||||
case 'custom': label = t('report.sourceCustom'); break;
|
||||
case 'ai': label = t('report.sourceAI'); break;
|
||||
default: label = source;
|
||||
}
|
||||
return `<span class="item-badge ${BADGE_CLASS[source] || 'badge-linter'}">${label}</span>`;
|
||||
}
|
||||
|
||||
function severityClass(severity: string): string {
|
||||
@@ -38,6 +45,7 @@ export class ReviewPanel {
|
||||
public static currentPanel: ReviewPanel | undefined;
|
||||
private readonly panel: vscode.WebviewPanel;
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
private currentReport: MergedReport | null = null;
|
||||
|
||||
private constructor(
|
||||
private readonly extensionUri: vscode.Uri,
|
||||
@@ -45,7 +53,7 @@ export class ReviewPanel {
|
||||
) {
|
||||
this.panel = vscode.window.createWebviewPanel(
|
||||
'codeReviewer.reviewPanel',
|
||||
'净码特工 · 代码审查报告',
|
||||
t('report.panelTitle'),
|
||||
column,
|
||||
{
|
||||
enableScripts: true,
|
||||
@@ -61,6 +69,15 @@ export class ReviewPanel {
|
||||
null,
|
||||
this.disposables
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
onLanguageChange(() => {
|
||||
this.panel.title = t('report.panelTitle');
|
||||
if (this.currentReport) {
|
||||
this.panel.webview.html = this.buildHtml(this.currentReport);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
static createOrShow(extensionUri: vscode.Uri, column?: vscode.ViewColumn): ReviewPanel {
|
||||
@@ -74,6 +91,7 @@ export class ReviewPanel {
|
||||
}
|
||||
|
||||
update(report: MergedReport): void {
|
||||
this.currentReport = report;
|
||||
this.panel.webview.html = this.buildHtml(report);
|
||||
}
|
||||
|
||||
@@ -96,13 +114,13 @@ export class ReviewPanel {
|
||||
const totalInfos = linterInfos + customInfos + aiInfos;
|
||||
|
||||
const errorBox = report.errors.length > 0
|
||||
? `<div class="errors-box"><div class="errors-box-title">✖ 执行错误</div>${report.errors.map(e => `<div class="errors-box-item">${esc(e)}</div>`).join('')}</div>`
|
||||
? `<div class="errors-box"><div class="errors-box-title">✖ ${t('report.executionErrors')}</div>${report.errors.map(e => `<div class="errors-box-item">${esc(e)}</div>`).join('')}</div>`
|
||||
: '';
|
||||
|
||||
const banner = report.errors.length > 0
|
||||
? '<div class="banner banner-error">⚠ AI 审查未完成,报告仅包含部分结果</div>'
|
||||
? `<div class="banner banner-error">⚠ ${t('report.degradedBanner')}</div>`
|
||||
: report.degraded
|
||||
? '<div class="banner banner-warning">⚠ 部分 AI 功能不可用,报告已降级</div>'
|
||||
? `<div class="banner banner-warning">⚠ ${t('report.degradedBanner')}</div>`
|
||||
: '';
|
||||
|
||||
const fixableLinterSet = new Set(report.fixableLinterIndices);
|
||||
@@ -116,11 +134,11 @@ export class ReviewPanel {
|
||||
return pts.join(' ');
|
||||
};
|
||||
|
||||
const linterToolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : '静态分析';
|
||||
const linterToolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : t('report.sourceLinter');
|
||||
|
||||
const customFilterInfo = report.customRuleFilterInfo;
|
||||
const customFilterLabel = customFilterInfo
|
||||
? `(注入 ${customFilterInfo.injected}/${customFilterInfo.totalActive} 条)`
|
||||
? t('report.injectedCount', { 0: customFilterInfo.injected, 1: customFilterInfo.totalActive })
|
||||
: '';
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
@@ -128,7 +146,7 @@ export class ReviewPanel {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>代码审查报告</title>
|
||||
<title>${t('report.title')}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); color: var(--vscode-foreground); background: var(--vscode-editor-background); }
|
||||
@@ -223,7 +241,7 @@ export class ReviewPanel {
|
||||
|
||||
<div class="header">
|
||||
<div>
|
||||
<h2>${SVG_HEADER_ICON} 净码特工 · 代码审查报告</h2>
|
||||
<h2>${SVG_HEADER_ICON} ${t('report.panelTitle')}</h2>
|
||||
<div class="meta">${esc(fileName)} · ${esc(report.language)} · ${(report.duration / 1000).toFixed(1)}s</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,16 +250,16 @@ ${banner}
|
||||
${errorBox}
|
||||
|
||||
<div class="summary">
|
||||
<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">${totalErrors}</div><div class="label">错误</div></div>
|
||||
<div class="stat-card stat-warning"><div class="num">${totalWarnings}</div><div class="label">警告</div></div>
|
||||
<div class="stat-card stat-info"><div class="num">${totalInfos}</div><div class="label">建议</div></div>
|
||||
<div class="stat-card stat-total"><div class="num">${total}</div><div class="label">${t('report.totalIssues')}</div></div>
|
||||
<div class="stat-card stat-error"><div class="num">${totalErrors}</div><div class="label">${t('report.errors')}</div></div>
|
||||
<div class="stat-card stat-warning"><div class="num">${totalWarnings}</div><div class="label">${t('report.warnings')}</div></div>
|
||||
<div class="stat-card stat-info"><div class="num">${totalInfos}</div><div class="label">${t('report.info')}</div></div>
|
||||
</div>
|
||||
|
||||
<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)} <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>
|
||||
<button class="tab active" data-tab="linter" onclick="switchTab('linter')">🔧 ${report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : t('report.sourceLinter')} ${tabCount(linterErrors, linterWarnings, linterInfos)}</button>
|
||||
<button class="tab" data-tab="custom" onclick="switchTab('custom')">📋 ${t('report.sourceCustom')} ${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')">🤖 ${t('report.sourceAI')} ${tabCount(aiErrors, aiWarnings, aiInfos)}</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="tab-linter">
|
||||
@@ -255,8 +273,8 @@ ${errorBox}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="send('rerun')">🔄 重新审查</button>
|
||||
<button class="btn" onclick="send('export')">📄 导出报告</button>
|
||||
<button class="btn btn-primary" onclick="send('rerun')">🔄 ${t('report.rerun')}</button>
|
||||
<button class="btn" onclick="send('export')">📄 ${t('report.export')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -283,35 +301,35 @@ ${errorBox}
|
||||
|
||||
private buildLinterList(report: MergedReport, fixableSet: Set<number>): string {
|
||||
if (report.linterDiagnostics.length === 0) {
|
||||
return '<div class="empty">未发现任何问题</div>';
|
||||
return `<div class="empty">${t('report.noIssues')}</div>`;
|
||||
}
|
||||
const toolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : '静态分析';
|
||||
const toolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : t('report.sourceLinter');
|
||||
const hasFixable = fixableSet.size > 0;
|
||||
return `<div class="section-header"><span class="section-header-title">${esc(toolName)} · ${report.linterCount} 个问题</span>${hasFixable ? '<button class="btn" onclick="send(\'fixAll\')">全部修复</button>' : ''}</div>`
|
||||
return `<div class="section-header"><span class="section-header-title">${esc(toolName)} · ${t('report.issuesCount', { 0: report.linterCount })}</span>${hasFixable ? `<button class="btn" onclick="send('fixAll')">${t('report.fixAll')}</button>` : ''}</div>`
|
||||
+ report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i), undefined, false)).join('');
|
||||
}
|
||||
|
||||
private buildCustomList(report: MergedReport, fixableSet: Set<number>): string {
|
||||
const filterInfo = report.customRuleFilterInfo;
|
||||
if (filterInfo?.skippedRequestA) {
|
||||
return '<div class="empty">当前文件语言无匹配的自定义规则,已跳过规则评估</div>';
|
||||
return `<div class="empty">${t('report.skipCustomRules')}</div>`;
|
||||
}
|
||||
if (report.customRuleDiagnostics.length === 0) {
|
||||
return '<div class="empty">未发现规则违规</div>';
|
||||
return `<div class="empty">${t('report.noRuleViolations')}</div>`;
|
||||
}
|
||||
const hasFixable = fixableSet.size > 0;
|
||||
const filterLabel = filterInfo
|
||||
? `(注入 ${filterInfo.injected}/${filterInfo.totalActive} 条规则)`
|
||||
? t('report.injectedRules', { 0: filterInfo.injected, 1: 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>`
|
||||
return `<div class="section-header"><span class="section-header-title">${t('report.sourceCustom')} · ${t('report.issuesCount', { 0: report.customRuleCount })}${filterLabel}</span>${hasFixable ? `<button class="btn" onclick="send('fixAll')">${t('report.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('');
|
||||
}
|
||||
|
||||
private buildAIList(report: MergedReport): string {
|
||||
if (report.aiFindings.length === 0) {
|
||||
return '<div class="empty">无 AI 审查建议</div>';
|
||||
return `<div class="empty">${t('report.noAIFindings')}</div>`;
|
||||
}
|
||||
const parts: string[] = ['<div class="section-header"><span class="section-header-title">AI 审查建议 · ' + report.aiCount + ' 条</span><button class="btn" onclick="send(\'fixAll\')">全部修复</button></div>'];
|
||||
const parts: string[] = [`<div class="section-header"><span class="section-header-title">${t('report.sourceAI')} · ${t('report.itemsCount', { 0: report.aiCount })}</span><button class="btn" onclick="send('fixAll')">${t('report.fixAll')}</button></div>`];
|
||||
for (const f of report.aiFindings) {
|
||||
const details: string[] = [];
|
||||
details.push(`<div class="detail-text">${esc(f.description)}</div>`);
|
||||
@@ -351,7 +369,7 @@ ${errorBox}
|
||||
parts.push(`<span class="item-message">${esc(message)}</span>`);
|
||||
parts.push(`<span class="item-line" onclick="event.stopPropagation();send('navigate', ${line}, '${esc(ruleId)}', '${source}')">L${lineNum}</span>`);
|
||||
if (fixable) {
|
||||
parts.push(`<button class="item-fix" onclick="event.stopPropagation(); this.disabled=true; this.textContent='⏳...';send('fix', ${line}, '${esc(ruleId)}', '${source}')">🔧 修复</button>`);
|
||||
parts.push(`<button class="item-fix" onclick="event.stopPropagation(); this.disabled=true; this.textContent='⏳...';send('fix', ${line}, '${esc(ruleId)}', '${source}')">🔧 ${t('report.fixAll')}</button>`);
|
||||
}
|
||||
parts.push('</div>');
|
||||
|
||||
|
||||
+15
-14
@@ -1,4 +1,5 @@
|
||||
import { MergedReport } from '../merger/merger';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
function severityEmoji(severity: string): string {
|
||||
switch (severity) {
|
||||
@@ -16,21 +17,21 @@ function formatLine(line: number): string {
|
||||
export function reportToMarkdown(report: MergedReport): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('# 代码审查报告');
|
||||
lines.push(`# ${t('report.title')}`);
|
||||
lines.push('');
|
||||
lines.push(`**文件:** \`${report.filePath}\``);
|
||||
lines.push(`**语言:** ${report.language}`);
|
||||
lines.push(`**耗时:** ${(report.duration / 1000).toFixed(1)}s`);
|
||||
lines.push(`**${t('report.file')}:** \`${report.filePath}\``);
|
||||
lines.push(`**${t('report.language')}:** ${report.language}`);
|
||||
lines.push(`**${t('report.duration')}:** ${(report.duration / 1000).toFixed(1)}s`);
|
||||
if (report.adapterNames.length > 0) {
|
||||
lines.push(`**分析工具:** ${report.adapterNames.join(', ')}`);
|
||||
lines.push(`**${t('report.tools')}:** ${report.adapterNames.join(', ')}`);
|
||||
}
|
||||
if (report.degraded) {
|
||||
lines.push('');
|
||||
lines.push('> ⚠️ 部分 AI 功能不可用,报告已降级');
|
||||
lines.push(`> ⚠️ ${t('report.degradedBanner')}`);
|
||||
}
|
||||
if (report.errors.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('## 错误');
|
||||
lines.push(`## ${t('report.errors')}`);
|
||||
for (const err of report.errors) {
|
||||
lines.push(`- ${err}`);
|
||||
}
|
||||
@@ -49,24 +50,24 @@ export function reportToMarkdown(report: MergedReport): string {
|
||||
+ report.aiFindings.filter(f => f.severity === 'warning').length;
|
||||
const infos = total - errors - warnings;
|
||||
|
||||
lines.push(`**总计:** ${total} | **错误:** ${errors} | **警告:** ${warnings} | **建议:** ${infos}`);
|
||||
lines.push(t('report.totalSummary', { 0: String(total), 1: String(errors), 2: String(warnings), 3: String(infos) }));
|
||||
lines.push('');
|
||||
|
||||
if (report.linterDiagnostics.length > 0) {
|
||||
lines.push(`## 🔧 静态分析 · ${report.linterCount} 个问题`);
|
||||
lines.push(t('report.staticSection', { 0: String(report.linterCount) }));
|
||||
lines.push('');
|
||||
for (const diag of report.linterDiagnostics) {
|
||||
lines.push(`- ${severityEmoji(diag.severity)} \`${diag.ruleId}\` ${formatLine(diag.range.start.line)}`);
|
||||
lines.push(` ${diag.message}`);
|
||||
if (diag.suggestion) {
|
||||
lines.push(` 建议: ${diag.suggestion}`);
|
||||
lines.push(` ${t('report.suggestion')}: ${diag.suggestion}`);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (report.customRuleDiagnostics.length > 0) {
|
||||
lines.push(`## 📋 自定义规则 · ${report.customRuleCount} 个问题`);
|
||||
lines.push(t('report.customSection', { 0: String(report.customRuleCount) }));
|
||||
lines.push('');
|
||||
for (const diag of report.customRuleDiagnostics) {
|
||||
lines.push(`- ${severityEmoji(diag.severity)} \`${diag.ruleId}\` ${formatLine(diag.range.start.line)}`);
|
||||
@@ -76,14 +77,14 @@ export function reportToMarkdown(report: MergedReport): string {
|
||||
}
|
||||
|
||||
if (report.aiFindings.length > 0) {
|
||||
lines.push(`## 🤖 AI 审查 · ${report.aiCount} 条建议`);
|
||||
lines.push(t('report.aiSection', { 0: String(report.aiCount) }));
|
||||
lines.push('');
|
||||
for (const finding of report.aiFindings) {
|
||||
lines.push(`- ${severityEmoji(finding.severity)} [AI] [${finding.category}] \`${finding.ruleId}\` ${formatLine(finding.line)}`);
|
||||
lines.push(` **${finding.title}**`);
|
||||
lines.push(` ${finding.description}`);
|
||||
if (finding.suggestion) {
|
||||
lines.push(` 建议: ${finding.suggestion}`);
|
||||
lines.push(` ${t('report.suggestion')}: ${finding.suggestion}`);
|
||||
}
|
||||
if (finding.codeDiff) {
|
||||
lines.push(' ```diff');
|
||||
@@ -95,7 +96,7 @@ export function reportToMarkdown(report: MergedReport): string {
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
lines.push('✅ 未发现问题');
|
||||
lines.push(`✅ ${t('report.noProblems')}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
|
||||
+15
-1
@@ -8,14 +8,28 @@
|
||||
vscode.postMessage({ type: type, value: value });
|
||||
}
|
||||
|
||||
function showRuleNameError(show) {
|
||||
var input = document.getElementById('newRuleInput');
|
||||
var error = document.getElementById('ruleNameError');
|
||||
if (!input || !error) { return; }
|
||||
input.classList.toggle('input-error', show);
|
||||
error.classList.toggle('show', show);
|
||||
}
|
||||
|
||||
function addRule() {
|
||||
var input = document.getElementById('newRuleInput');
|
||||
var name = input.value.trim();
|
||||
if (!name) { return; }
|
||||
if (!name) { showRuleNameError(true); return; }
|
||||
showRuleNameError(false);
|
||||
vscode.postMessage({ type: 'addRule', name: name });
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
var ruleInput = document.getElementById('newRuleInput');
|
||||
if (ruleInput) {
|
||||
ruleInput.addEventListener('input', function () { showRuleNameError(false); });
|
||||
}
|
||||
|
||||
function deleteFile(fileName) {
|
||||
vscode.postMessage({ type: 'deleteFile', fileName: fileName });
|
||||
}
|
||||
|
||||
+79
-55
@@ -13,18 +13,25 @@ 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';
|
||||
import { t, onLanguageChange, Language } from '../i18n/messages';
|
||||
|
||||
const languageLabels: Record<string, string> = {
|
||||
'zh-CN': '中文(简体)',
|
||||
'en': 'English',
|
||||
'ja': '日本語',
|
||||
};
|
||||
function getLanguageLabel(lang: string): string {
|
||||
switch (lang) {
|
||||
case 'zh-CN': return t('lang.zhCN');
|
||||
case 'en': return t('lang.en');
|
||||
case 'ja': return t('lang.ja');
|
||||
default: return lang;
|
||||
}
|
||||
}
|
||||
|
||||
export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
private _view?: vscode.WebviewView;
|
||||
public connectionTested = false;
|
||||
public connectionSuccess = false;
|
||||
private importService = new ImportService();
|
||||
private _providers: Record<string, { name: string; models: string[] }> = {};
|
||||
private _config: { provider: string; model: string; outputLanguage: string; baseUrl: string } = { provider: '', model: '', outputLanguage: 'zh-CN', baseUrl: '' };
|
||||
private _scriptUri: vscode.Uri | null = null;
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
this.importService.registerConverter(new YamlConverter());
|
||||
@@ -53,8 +60,19 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
vscode.Uri.joinPath(this.context.extensionUri, 'out', 'webview', 'setupView.js')
|
||||
);
|
||||
const aiConfig = getAIConfig();
|
||||
this._providers = providers;
|
||||
this._config = aiConfig;
|
||||
this._scriptUri = scriptUri;
|
||||
webviewView.webview.html = this.getHtml(providers, aiConfig, scriptUri);
|
||||
|
||||
const langDisposable = onLanguageChange(() => {
|
||||
const newConfig = getAIConfig();
|
||||
this._config = newConfig;
|
||||
if (this._view && this._scriptUri) {
|
||||
this._view.webview.html = this.getHtml(this._providers, newConfig, this._scriptUri);
|
||||
}
|
||||
});
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(async (msg) => {
|
||||
switch (msg.type) {
|
||||
case 'ready':
|
||||
@@ -128,7 +146,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
baseUrl: baseUrlConfigured ? config.baseUrl : '',
|
||||
baseUrlConfigured,
|
||||
language: config.outputLanguage,
|
||||
languageLabel: languageLabels[config.outputLanguage] || config.outputLanguage,
|
||||
languageLabel: getLanguageLabel(config.outputLanguage),
|
||||
apiKeyConfigured,
|
||||
},
|
||||
providers: getAllProviderMeta(),
|
||||
@@ -141,12 +159,12 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
private async testConnection(): Promise<void> {
|
||||
const apiKey = await getApiKey(this.context);
|
||||
if (!apiKey) {
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: false, message: '请先设置 API Key' });
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: false, message: t('setup.setApiKeyFirst') });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBaseUrlConfigured()) {
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: false, message: '请先设置 Base URL' });
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: false, message: t('setup.setBaseUrlFirst') });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,12 +180,12 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
this.connectionTested = true;
|
||||
this.connectionSuccess = true;
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: true, message: '✓ 连接成功' });
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: true, message: t('setup.testSuccess') });
|
||||
} catch (err) {
|
||||
this.connectionTested = true;
|
||||
this.connectionSuccess = false;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: false, message: `✗ 连接失败: ${message}` });
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: false, message: t('setup.testFail', { 0: message }) });
|
||||
}
|
||||
|
||||
await this.pushConfig();
|
||||
@@ -191,7 +209,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
const result = await vscode.window.showOpenDialog({
|
||||
canSelectMany: false,
|
||||
openLabel: '选择规则文件',
|
||||
openLabel: t('setup.selectRuleFile'),
|
||||
filters: { '规则文件': ['yaml', 'yml', 'md', 'txt', 'xlsx', 'xls', 'docx', 'pptx'] },
|
||||
});
|
||||
if (!result || result.length === 0) { return; }
|
||||
@@ -208,32 +226,33 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
const yamlPath = path.join(rulesDir, yamlFileName);
|
||||
|
||||
if (fs.existsSync(yamlPath)) {
|
||||
vscode.window.showErrorMessage(`文件 ${yamlFileName} 已存在`);
|
||||
vscode.window.showErrorMessage(t('setup.fileExists', { 0: yamlFileName }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ext === '.yaml' || ext === '.yml') {
|
||||
fs.copyFileSync(srcPath, yamlPath);
|
||||
vscode.window.showInformationMessage(`规则文件已导入: ${yamlFileName}`);
|
||||
vscode.window.showInformationMessage(t('setup.importSuccess', { 0: yamlFileName }));
|
||||
} else {
|
||||
try {
|
||||
const conversion = await this.importService.convert(srcPath, this.context);
|
||||
const conversion = await vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: t('setup.importing'),
|
||||
}, () => this.importService.convert(srcPath, this.context));
|
||||
|
||||
const decision = await showImportPreview(conversion);
|
||||
if (!decision || !decision.confirmed) {
|
||||
vscode.window.showInformationMessage('导入已取消');
|
||||
vscode.window.showInformationMessage(t('setup.importCancelled'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.importService.applyConversion(conversion, decision, yamlPath);
|
||||
vscode.window.showInformationMessage(
|
||||
`规则已导入: ${yamlFileName}(${conversion.rules.length} 条,` +
|
||||
`${conversion.exactCount} 条完全重复已注释,` +
|
||||
`${conversion.overlapCount} 条部分重叠已标注)`
|
||||
t('setup.importDedupResult', { 0: yamlFileName, 1: String(conversion.rules.length), 2: String(conversion.exactCount), 3: String(conversion.overlapCount) })
|
||||
);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
vscode.window.showErrorMessage(`规则导入失败: ${msg}`);
|
||||
vscode.window.showErrorMessage(t('setup.importFail', { 0: msg }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,6 +392,10 @@ select {
|
||||
}
|
||||
input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vscode-descriptionForeground)); }
|
||||
.field-hint { font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 4px; }
|
||||
.input-error { border-color: #f48771 !important; }
|
||||
.input-error:focus { border-color: #f48771 !important; }
|
||||
.error-hint { font-size: 11px; color: #f48771; margin-top: 4px; display: none; }
|
||||
.error-hint.show { display: block; }
|
||||
|
||||
/* Input group */
|
||||
.input-group { display: flex; gap: 4px; }
|
||||
@@ -469,31 +492,31 @@ 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>
|
||||
净码特工 · 代码审查 · 设置
|
||||
${t('setup.header')}
|
||||
</div>
|
||||
|
||||
<!-- 1. 快速开始 -->
|
||||
<div class="section">
|
||||
<div class="section-title">快速开始</div>
|
||||
<div class="section-title">${t('setup.quickStart')}</div>
|
||||
<div class="getting-started">
|
||||
<div class="gs-title">
|
||||
<span class="gs-title-dot"></span>
|
||||
三步启用代码审核
|
||||
${t('setup.gettingStarted')}
|
||||
</div>
|
||||
<div class="gs-steps">
|
||||
<div class="gs-step" data-step="1">
|
||||
<span class="gs-step-num">1</span>
|
||||
<span>安装插件后,<b>配置 AI 模型</b>及 API Key,激活智能审核能力</span>
|
||||
<span>${t('setup.step1')}</span>
|
||||
</div>
|
||||
<div class="gs-step" data-step="2">
|
||||
<span class="gs-step-num">2</span>
|
||||
<span>启用 <b>自定义规则</b>,补充团队特有的编码规范</span>
|
||||
<span>${t('setup.step2')}</span>
|
||||
</div>
|
||||
<div class="gs-step" data-step="3">
|
||||
<span class="gs-step-num">3</span>
|
||||
<div class="gs-step-body">
|
||||
<span><b>保存并测试连接</b>,验证配置无误后即可触发审核</span>
|
||||
<span class="gs-step-hint">按 <b>Ctrl + Shift + R</b> 快捷键触发审核,结果实时显示在 <b>审核结果报告</b>页面中</span>
|
||||
<span>${t('setup.step3')}</span>
|
||||
<span class="gs-step-hint">${t('setup.step3Hint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,27 +525,27 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
|
||||
<!-- 2. 审核引擎 -->
|
||||
<div class="section">
|
||||
<div class="section-title">审核引擎</div>
|
||||
<div class="section-title">${t('setup.engineSection')}</div>
|
||||
<div class="engines">
|
||||
<div class="engine-tab">
|
||||
<span class="engine-dot dot-purple"></span>
|
||||
<div>
|
||||
<div class="engine-label">共通规则</div>
|
||||
<div class="engine-desc">Linter 静态分析</div>
|
||||
<div class="engine-label">${t('setup.commonRules')}</div>
|
||||
<div class="engine-desc">${t('setup.linterStatic')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="engine-tab">
|
||||
<span class="engine-dot dot-amber"></span>
|
||||
<div>
|
||||
<div class="engine-label">自定义规则</div>
|
||||
<div class="engine-desc">团队编码规范</div>
|
||||
<div class="engine-label">${t('setup.customRules')}</div>
|
||||
<div class="engine-desc">${t('setup.teamCoding')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="engine-tab">
|
||||
<span class="engine-dot dot-green"></span>
|
||||
<div>
|
||||
<div class="engine-label">AI 审核</div>
|
||||
<div class="engine-desc">深度代码审查</div>
|
||||
<div class="engine-label">${t('setup.aiReview')}</div>
|
||||
<div class="engine-desc">${t('setup.deepReview')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -530,11 +553,11 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
|
||||
<!-- 3. AI 模型配置 -->
|
||||
<div class="section">
|
||||
<div class="section-title">AI 模型配置</div>
|
||||
<div class="section-title">${t('setup.aiConfig')}</div>
|
||||
<div class="card">
|
||||
<div class="card-row">
|
||||
<span class="card-label">模型提供商</span>
|
||||
<span class="badge" id="providerBadge">未配置</span>
|
||||
<span class="card-label">${t('setup.provider')}</span>
|
||||
<span class="badge" id="providerBadge">${t('setup.notConfigured')}</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<select id="providerSelect">${Object.entries(providers).map(([id, meta]) =>
|
||||
@@ -542,12 +565,12 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
).join('\n ')}</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">模型名称</label>
|
||||
<label class="field-label">${t('setup.model')}</label>
|
||||
<select id="modelSelect">${(providers[config.provider]?.models ?? []).map(m =>
|
||||
`<option value="${m}"${config.model === m ? ' selected' : ''}>${m}</option>`
|
||||
).join('\n ')}</select>
|
||||
</div>
|
||||
<div class="field-hint">建议使用支持结构化输出的模型。</div>
|
||||
<div class="field-hint">${t('setup.modelHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -556,56 +579,57 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
<div class="section-title">API Key</div>
|
||||
<div class="card">
|
||||
<div class="card-row">
|
||||
<span class="card-label">API Key</span>
|
||||
<span class="badge" id="apiKeyBadge">未配置</span>
|
||||
<span class="card-label">${t('setup.apiKey')}</span>
|
||||
<span class="badge" id="apiKeyBadge">${t('setup.notConfigured')}</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<input type="password" id="apiKeyInput" placeholder="sk-..." onchange="postMsg('setApiKey', this.value)">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Base URL</label>
|
||||
<label class="field-label">${t('setup.baseUrl')}</label>
|
||||
<input type="text" id="baseUrlInput" placeholder="https://api.deepseek.com/v1" onchange="postMsg('setBaseUrl', this.value)">
|
||||
</div>
|
||||
<div class="field-hint">Key 仅存储在本地 VS Code 安全存储中。</div>
|
||||
<div class="field-hint">${t('setup.keyStorageHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 输出语言 -->
|
||||
<div class="section">
|
||||
<div class="section-title">输出语言</div>
|
||||
<div class="section-title">${t('setup.outputLang')}</div>
|
||||
<div class="field">
|
||||
<label class="field-label">AI 审查结果输出语言</label>
|
||||
<label class="field-label">${t('setup.outputLangHint')}</label>
|
||||
<select id="languageSelect" onchange="postMsg('setLanguage', this.value)">
|
||||
<option value="zh-CN"${config.outputLanguage === 'zh-CN' ? ' selected' : ''}>中文(简体)</option>
|
||||
<option value="en"${config.outputLanguage === 'en' ? ' selected' : ''}>English</option>
|
||||
<option value="ja"${config.outputLanguage === 'ja' ? ' selected' : ''}>日本語</option>
|
||||
<option value="zh-CN"${config.outputLanguage === 'zh-CN' ? ' selected' : ''}>${t('lang.zhCN')}</option>
|
||||
<option value="en"${config.outputLanguage === 'en' ? ' selected' : ''}>${t('lang.en')}</option>
|
||||
<option value="ja"${config.outputLanguage === 'ja' ? ' selected' : ''}>${t('lang.ja')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. 自定义规则 -->
|
||||
<div class="section">
|
||||
<div class="section-title">自定义规则</div>
|
||||
<div class="section-title">${t('setup.customRulesSection')}</div>
|
||||
<div class="card">
|
||||
<div class="card-row">
|
||||
<span class="card-label">规则列表</span>
|
||||
<span class="badge" style="background:rgba(139,148,158,0.12);color:#8b949e;" id="ruleCountBadge">0 条</span>
|
||||
<span class="card-label">${t('setup.ruleList')}</span>
|
||||
<span class="badge" style="background:rgba(139,148,158,0.12);color:#8b949e;" id="ruleCountBadge">${t('setup.ruleCount', { 0: '0' })}</span>
|
||||
</div>
|
||||
<div id="ruleList"></div>
|
||||
<div class="field" style="margin-top:8px;">
|
||||
<div class="input-group">
|
||||
<input type="text" id="newRuleInput" placeholder="输入规则名称...">
|
||||
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="addRule()">+ 添加</button>
|
||||
<input type="text" id="newRuleInput" placeholder="${t('setup.ruleNamePlaceholder')}">
|
||||
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;" onclick="addRule()">${t('setup.add')}</button>
|
||||
</div>
|
||||
<div class="field-hint">建议使用英文名称,无需输入 .yaml 后缀(如 security-rules)</div>
|
||||
<div class="error-hint" id="ruleNameError">${t('setup.ruleNameRequired')}</div>
|
||||
<div class="field-hint">${t('setup.ruleNameHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="postMsg('reset')">重置</button>
|
||||
<button class="btn btn-primary" id="btnTest" onclick="postMsg('saveAndTest')">保存并测试连接</button>
|
||||
<button class="btn" onclick="postMsg('reset')">${t('setup.reset')}</button>
|
||||
<button class="btn btn-primary" id="btnTest" onclick="postMsg('saveAndTest')">${t('setup.saveAndTest')}</button>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
|
||||
Reference in New Issue
Block a user