feat: 适配器 i18n + Provider 动态注册 + SetupView 重构 + jars 资源
- 适配器 i18n 接入(eslint/pmd/sql-lint/stylelint) - Provider 动态注册机制(registry.ts + providers.json + factory 重构) - SetupView 全面重构(setupView.ts 新增 600+ 行) - i18n 消息扩展(messages.ts +210 行) - 规则导入流程优化(import-service / prompt-builder) - 新增 PMD jars 依赖及测试用例
This commit is contained in:
+40
-3
@@ -1,8 +1,45 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ESLint } from 'eslint';
|
||||
import js from '@eslint/js';
|
||||
import ts from 'typescript-eslint';
|
||||
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
|
||||
import { getEslintConfigPath } from '../config';
|
||||
|
||||
const PROJECT_CONFIG_FILES = [
|
||||
'.eslintrc.js',
|
||||
'.eslintrc.json',
|
||||
'.eslintrc.yaml',
|
||||
'.eslintrc.yml',
|
||||
'.eslintrc',
|
||||
'eslint.config.js',
|
||||
'eslint.config.mjs',
|
||||
];
|
||||
|
||||
function findProjectConfig(dir: string): string | null {
|
||||
for (const name of PROJECT_CONFIG_FILES) {
|
||||
const p = path.join(dir, name);
|
||||
if (fs.existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveEslintConfig(workingDir: string): { configFile?: string; overrideConfig?: any[] } {
|
||||
const globalPath = getEslintConfigPath();
|
||||
if (globalPath && globalPath.trim() !== '') {
|
||||
return { configFile: globalPath };
|
||||
}
|
||||
|
||||
const projectConfig = findProjectConfig(workingDir);
|
||||
if (projectConfig) {
|
||||
return { configFile: projectConfig };
|
||||
}
|
||||
|
||||
return { overrideConfig: ESLintAdapter.getDefaultConfig() };
|
||||
}
|
||||
|
||||
export class ESLintAdapter implements LinterAdapter {
|
||||
id = 'eslint';
|
||||
@@ -10,7 +47,7 @@ export class ESLintAdapter implements LinterAdapter {
|
||||
|
||||
private static defaultConfig: any[] | null = null;
|
||||
|
||||
private static getDefaultConfig(): any[] {
|
||||
public static getDefaultConfig(): any[] {
|
||||
if (!ESLintAdapter.defaultConfig) {
|
||||
ESLintAdapter.defaultConfig = [
|
||||
js.configs.recommended,
|
||||
@@ -26,10 +63,10 @@ export class ESLintAdapter implements LinterAdapter {
|
||||
|
||||
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
|
||||
try {
|
||||
const resolved = resolveEslintConfig(workingDir);
|
||||
const engine = new ESLint({
|
||||
cwd: workingDir,
|
||||
overrideConfigFile: true,
|
||||
overrideConfig: ESLintAdapter.getDefaultConfig(),
|
||||
...resolved,
|
||||
});
|
||||
const ext = document.languageId === 'typescript' ? 'ts' : 'js';
|
||||
const isVirtual = document.uri.scheme === 'untitled';
|
||||
|
||||
+21
-3
@@ -3,16 +3,26 @@ 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';
|
||||
import { getPMDJarPath, getPMDRulesetPath } from '../config';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
export class PmdAdapter implements LinterAdapter {
|
||||
id = 'pmd';
|
||||
supportedLanguages = ['java'];
|
||||
private pmdDir: string | null = null;
|
||||
private jarPathChecked = false;
|
||||
|
||||
private resolvePmdDir(): string {
|
||||
if (this.pmdDir) { return this.pmdDir; }
|
||||
|
||||
const jarPath = getPMDJarPath();
|
||||
if (jarPath && jarPath.trim() !== '') {
|
||||
if (existsSync(path.join(jarPath, 'PmdRunner.class'))) {
|
||||
this.pmdDir = jarPath;
|
||||
return this.pmdDir;
|
||||
}
|
||||
}
|
||||
|
||||
const candidates: string[] = [];
|
||||
try {
|
||||
const ext = vscode.extensions.getExtension?.('vscode-code-reviewer');
|
||||
@@ -43,8 +53,16 @@ export class PmdAdapter implements LinterAdapter {
|
||||
|
||||
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
|
||||
try {
|
||||
const ruleset = getPMDRulesetPath()
|
||||
|| path.join(this.getPmdRunnerClasspath(), 'pmd-java-ruleset.xml');
|
||||
const globalRuleset = getPMDRulesetPath();
|
||||
let ruleset: string;
|
||||
if (globalRuleset && globalRuleset.trim() !== '') {
|
||||
ruleset = globalRuleset;
|
||||
} else {
|
||||
const projectRuleset = path.join(workingDir, 'ruleset.xml');
|
||||
ruleset = existsSync(projectRuleset)
|
||||
? projectRuleset
|
||||
: path.join(this.getPmdRunnerClasspath(), 'pmd-java-ruleset.xml');
|
||||
}
|
||||
const classpath = `${this.getPmdLibClasspath()};${this.getPmdRunnerClasspath()}`;
|
||||
|
||||
const isVirtual = document.uri.scheme === 'untitled';
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter';
|
||||
import { getSqlLintConfigFile } from '../config';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
const DIALECT_MAP: Record<string, string> = {
|
||||
@@ -22,9 +25,14 @@ interface SqlFluffResult {
|
||||
violations: SqlFluffViolation[];
|
||||
}
|
||||
|
||||
function runSqlfluff(dialect: string, code: string, cwd: string): Promise<string> {
|
||||
function runSqlfluff(dialect: string, code: string, cwd: string, configPath?: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('sqlfluff', ['lint', '--dialect', dialect, '--format', 'json', '-'], {
|
||||
const args = ['lint', '--dialect', dialect, '--format', 'json'];
|
||||
if (configPath) {
|
||||
args.push('--config', configPath);
|
||||
}
|
||||
args.push('-');
|
||||
const child = spawn('sqlfluff', args, {
|
||||
cwd,
|
||||
timeout: 30000,
|
||||
});
|
||||
@@ -68,8 +76,20 @@ export class SqlLintAdapter implements LinterAdapter {
|
||||
const languageId = document.languageId;
|
||||
const dialect = DIALECT_MAP[languageId] || 'ansi';
|
||||
|
||||
let configPath: string | undefined;
|
||||
|
||||
const globalConfig = getSqlLintConfigFile();
|
||||
if (globalConfig && globalConfig.trim() !== '') {
|
||||
configPath = globalConfig;
|
||||
} else {
|
||||
const projectConfig = path.join(workingDir, '.sqlfluff');
|
||||
if (fs.existsSync(projectConfig)) {
|
||||
configPath = projectConfig;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const stdout = await runSqlfluff(dialect, document.getText(), workingDir);
|
||||
const stdout = await runSqlfluff(dialect, document.getText(), workingDir, configPath);
|
||||
const results: SqlFluffResult[] = JSON.parse(stdout);
|
||||
const diagnostics: LinterDiagnostic[] = [];
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
|
||||
import { getStylelintConfigPath } from '../config';
|
||||
|
||||
const CONFIG_FILE_NAMES = [
|
||||
'.stylelintrc',
|
||||
@@ -36,6 +37,7 @@ interface LinterOptions {
|
||||
codeFilename?: string;
|
||||
cwd?: string;
|
||||
config?: Record<string, unknown>;
|
||||
configFile?: string;
|
||||
}
|
||||
|
||||
interface LinterResult {
|
||||
@@ -88,7 +90,10 @@ export class StylelintAdapter implements LinterAdapter {
|
||||
cwd: workingDir,
|
||||
};
|
||||
|
||||
if (!hasExternalConfig(workingDir)) {
|
||||
const globalPath = getStylelintConfigPath();
|
||||
if (globalPath && globalPath.trim() !== '') {
|
||||
lintOptions.configFile = globalPath;
|
||||
} else if (!hasExternalConfig(workingDir)) {
|
||||
lintOptions.config = DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -192,7 +192,7 @@ export async function runAIReview(
|
||||
|
||||
let provider: AIProvider;
|
||||
try {
|
||||
provider = createProvider(providerId, apiKey, baseUrl);
|
||||
provider = createProvider(providerId, apiKey, baseUrl, context.extensionUri);
|
||||
} catch (err) {
|
||||
return {
|
||||
customRuleResults: [],
|
||||
|
||||
+40
-107
@@ -1,118 +1,51 @@
|
||||
import { AIProvider } from './providers/base';
|
||||
import * as vscode from 'vscode';
|
||||
import type { AIProvider } from './providers/base';
|
||||
import type { ProviderMeta, ProviderProtocol } from './types';
|
||||
import { OpenAICompatibleProvider } from './providers/openai-compatible';
|
||||
import { GeminiProvider } from './providers/gemini';
|
||||
import { ClaudeProvider } from './providers/claude';
|
||||
import {
|
||||
getProviderById,
|
||||
getAllProviderMeta as getAllProviderMetaFromRegistry,
|
||||
invalidateProviderCache,
|
||||
} from './registry';
|
||||
|
||||
interface ProviderInfo {
|
||||
cls: new (apiKey: string, baseUrl: string) => AIProvider;
|
||||
defaultBaseUrl: string;
|
||||
models: string[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
const registry: Record<string, ProviderInfo> = {
|
||||
deepseek: {
|
||||
cls: class extends OpenAICompatibleProvider {
|
||||
constructor(apiKey: string, baseUrl: string) {
|
||||
super(apiKey, baseUrl, 'deepseek', 'DeepSeek');
|
||||
}
|
||||
},
|
||||
defaultBaseUrl: 'https://api.deepseek.com/v1',
|
||||
models: ['deepseek-v4-pro', 'deepseek-v4-flash'],
|
||||
name: 'DeepSeek',
|
||||
},
|
||||
openai: {
|
||||
cls: class extends OpenAICompatibleProvider {
|
||||
constructor(apiKey: string, baseUrl: string) {
|
||||
super(apiKey, baseUrl, 'openai', 'OpenAI');
|
||||
}
|
||||
},
|
||||
defaultBaseUrl: 'https://api.openai.com/v1',
|
||||
models: ['GPT-5.6 Sol', 'GPT-5.6 Terra', 'GPT-5.6 Luna', 'GPT-5.5', 'GPT-5.4'],
|
||||
name: 'OpenAI',
|
||||
},
|
||||
gemini: {
|
||||
cls: GeminiProvider,
|
||||
defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1',
|
||||
models: ['Gemini 3.1 Pro', 'Gemini 3.1 Flash', 'Gemini 3.1 Flash-Lite', 'Gemini 3 Pro', 'Gemini 3 Flash'],
|
||||
name: 'Google Gemini',
|
||||
},
|
||||
claude: {
|
||||
cls: ClaudeProvider,
|
||||
defaultBaseUrl: 'https://api.anthropic.com/v1',
|
||||
models: ['Claude Fable 5', 'Claude Mythos 5', 'Claude Opus 4.8', 'Claude Sonnet 4.6'],
|
||||
name: 'Anthropic Claude',
|
||||
},
|
||||
hunyuan: {
|
||||
cls: class extends OpenAICompatibleProvider {
|
||||
constructor(apiKey: string, baseUrl: string) {
|
||||
super(apiKey, baseUrl, 'hunyuan', '腾讯混元');
|
||||
}
|
||||
},
|
||||
defaultBaseUrl: 'https://api.hunyuan.cloud.tencent.com/v1',
|
||||
models: ['Hy3'],
|
||||
name: '腾讯混元',
|
||||
},
|
||||
zhipu: {
|
||||
cls: class extends OpenAICompatibleProvider {
|
||||
constructor(apiKey: string, baseUrl: string) {
|
||||
super(apiKey, baseUrl, 'zhipu', '智谱AI');
|
||||
}
|
||||
},
|
||||
defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
models: ['GLM-5.2', 'GLM-5.1', 'GLM-5'],
|
||||
name: '智谱AI',
|
||||
},
|
||||
moonshot: {
|
||||
cls: class extends OpenAICompatibleProvider {
|
||||
constructor(apiKey: string, baseUrl: string) {
|
||||
super(apiKey, baseUrl, 'moonshot', '月之暗面');
|
||||
}
|
||||
},
|
||||
defaultBaseUrl: 'https://api.moonshot.cn/v1',
|
||||
models: ['Kimi K2.7 Code', 'Kimi K2.6', 'Kimi K2.5'],
|
||||
name: '月之暗面',
|
||||
},
|
||||
tongyi: {
|
||||
cls: class extends OpenAICompatibleProvider {
|
||||
constructor(apiKey: string, baseUrl: string) {
|
||||
super(apiKey, baseUrl, 'tongyi', '阿里通义');
|
||||
}
|
||||
},
|
||||
defaultBaseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
models: ['Qwen3-2507', 'Qwen3.6-35B-A3B', 'Qwen3.5-397B-A17B'],
|
||||
name: '阿里通义',
|
||||
},
|
||||
const PROTOCOL_MAP: Record<ProviderProtocol, new (...args: any[]) => AIProvider> = {
|
||||
'openai-compatible': OpenAICompatibleProvider,
|
||||
gemini: GeminiProvider,
|
||||
claude: ClaudeProvider,
|
||||
};
|
||||
|
||||
export function createProvider(providerId: string, apiKey: string, baseUrl: string): AIProvider {
|
||||
const info = registry[providerId];
|
||||
if (!info) {
|
||||
export function createProvider(
|
||||
providerId: string,
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
extensionUri: vscode.Uri
|
||||
): AIProvider {
|
||||
const config = getProviderById(extensionUri, providerId);
|
||||
if (!config) {
|
||||
throw new Error(`未知的 Provider: ${providerId}`);
|
||||
}
|
||||
return new info.cls(apiKey, baseUrl);
|
||||
}
|
||||
|
||||
export function getProviderIds(): string[] {
|
||||
return Object.keys(registry);
|
||||
}
|
||||
|
||||
export function getProviderInfo(providerId: string): ProviderInfo | undefined {
|
||||
return registry[providerId];
|
||||
}
|
||||
|
||||
export function getProviderDefaultBaseUrl(providerId: string): string {
|
||||
return registry[providerId]?.defaultBaseUrl ?? '';
|
||||
}
|
||||
|
||||
export function getProviderModels(providerId: string): string[] {
|
||||
return registry[providerId]?.models ?? [];
|
||||
}
|
||||
|
||||
export function getAllProviderMeta(): Record<string, { name: string; models: string[] }> {
|
||||
const result: Record<string, { name: string; models: string[] }> = {};
|
||||
for (const [id, info] of Object.entries(registry)) {
|
||||
result[id] = { name: info.name, models: info.models };
|
||||
const Cls = PROTOCOL_MAP[config.protocol];
|
||||
if (!Cls) {
|
||||
throw new Error(`未知的协议类型: ${config.protocol}`);
|
||||
}
|
||||
return result;
|
||||
|
||||
if (config.protocol === 'openai-compatible') {
|
||||
return new Cls(apiKey, baseUrl, config.id, config.name);
|
||||
}
|
||||
return new Cls(apiKey, baseUrl);
|
||||
}
|
||||
|
||||
export function getProviderModels(extensionUri: vscode.Uri, providerId: string): string[] {
|
||||
return getProviderById(extensionUri, providerId)?.models ?? [];
|
||||
}
|
||||
|
||||
export function getAllProviderMeta(
|
||||
extensionUri: vscode.Uri
|
||||
): Record<string, ProviderMeta> {
|
||||
return getAllProviderMetaFromRegistry(extensionUri);
|
||||
}
|
||||
|
||||
export { invalidateProviderCache };
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface ChatOptions {
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
timeoutMs: number;
|
||||
seed?: number;
|
||||
}
|
||||
|
||||
export abstract class AIProvider {
|
||||
|
||||
@@ -14,7 +14,7 @@ export class OpenAICompatibleProvider extends AIProvider {
|
||||
async chat(systemPrompt: string, userPrompt: string, options: ChatOptions): Promise<string> {
|
||||
const url = `${this.baseUrl}/chat/completions`;
|
||||
|
||||
const body = JSON.stringify({
|
||||
const bodyObj: Record<string, unknown> = {
|
||||
model: options.model,
|
||||
max_tokens: options.maxTokens,
|
||||
temperature: options.temperature,
|
||||
@@ -22,7 +22,13 @@ export class OpenAICompatibleProvider extends AIProvider {
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt },
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
if (options.seed !== undefined) {
|
||||
bodyObj.seed = options.seed;
|
||||
}
|
||||
|
||||
const body = JSON.stringify(bodyObj);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { ProviderConfig, ProvidersFile, ProviderMeta } from './types';
|
||||
|
||||
let cachedProviders: ProviderConfig[] | null = null;
|
||||
|
||||
function loadBuiltinProviders(extensionUri: vscode.Uri): ProviderConfig[] {
|
||||
const filePath = vscode.Uri.joinPath(extensionUri, 'providers.json').fsPath;
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||
const data = JSON.parse(raw) as ProvidersFile;
|
||||
return data.providers ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadUserProviders(): ProviderConfig[] {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) { return []; }
|
||||
|
||||
const filePath = path.join(workspaceRoot, '.code-review', 'providers.json');
|
||||
if (!fs.existsSync(filePath)) { return []; }
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||
const data = JSON.parse(raw) as ProvidersFile;
|
||||
return data.providers ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function mergeProviders(
|
||||
builtin: ProviderConfig[],
|
||||
user: ProviderConfig[]
|
||||
): ProviderConfig[] {
|
||||
const map = new Map<string, ProviderConfig>();
|
||||
|
||||
for (const p of builtin) {
|
||||
map.set(p.id, p);
|
||||
}
|
||||
|
||||
for (const p of user) {
|
||||
map.set(p.id, p);
|
||||
}
|
||||
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
export function getProviders(extensionUri?: vscode.Uri): ProviderConfig[] {
|
||||
if (cachedProviders) { return cachedProviders; }
|
||||
|
||||
if (!extensionUri) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const builtin = loadBuiltinProviders(extensionUri);
|
||||
const user = loadUserProviders();
|
||||
cachedProviders = mergeProviders(builtin, user);
|
||||
return cachedProviders;
|
||||
}
|
||||
|
||||
export function invalidateProviderCache(): void {
|
||||
cachedProviders = null;
|
||||
}
|
||||
|
||||
export function getProviderById(
|
||||
extensionUri: vscode.Uri,
|
||||
id: string
|
||||
): ProviderConfig | undefined {
|
||||
return getProviders(extensionUri).find(p => p.id === id);
|
||||
}
|
||||
|
||||
export function getAllProviderMeta(
|
||||
extensionUri: vscode.Uri
|
||||
): Record<string, ProviderMeta> {
|
||||
const result: Record<string, ProviderMeta> = {};
|
||||
for (const p of getProviders(extensionUri)) {
|
||||
result[p.id] = {
|
||||
name: p.name,
|
||||
models: p.models,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export type ProviderProtocol = 'openai-compatible' | 'gemini' | 'claude';
|
||||
|
||||
export interface ProviderConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
protocol: ProviderProtocol;
|
||||
defaultBaseUrl: string;
|
||||
models: string[];
|
||||
}
|
||||
|
||||
export interface ProvidersFile {
|
||||
providers: ProviderConfig[];
|
||||
}
|
||||
|
||||
export interface ProviderMeta {
|
||||
name: string;
|
||||
models: string[];
|
||||
}
|
||||
@@ -21,3 +21,23 @@ export function getPMDJspRulesetPath(): string {
|
||||
export function getSqlLintConfigFile(): string {
|
||||
return vscode.workspace.getConfiguration(ROOT).get<string>('sql-lint.configFile', '');
|
||||
}
|
||||
|
||||
export function getEslintConfigPath(): string {
|
||||
return vscode.workspace.getConfiguration(ROOT).get<string>('linters.eslintConfigPath', '');
|
||||
}
|
||||
|
||||
export function getStylelintConfigPath(): string {
|
||||
return vscode.workspace.getConfiguration(ROOT).get<string>('linters.stylelintConfigPath', '');
|
||||
}
|
||||
|
||||
export function isAdapterEnabled(adapterId: string): boolean {
|
||||
return vscode.workspace.getConfiguration(ROOT).get<boolean>(`linter.${adapterId}.enabled`, true);
|
||||
}
|
||||
|
||||
export async function setAdapterEnabled(adapterId: string, enabled: boolean): Promise<void> {
|
||||
await vscode.workspace.getConfiguration(ROOT).update(
|
||||
`linter.${adapterId}.enabled`,
|
||||
enabled,
|
||||
vscode.ConfigurationTarget.Global
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,6 +212,11 @@ const messages: Record<string, Record<Language, string>> = {
|
||||
en: 'Press <b>Ctrl + Shift + R</b> to trigger review, results appear in the <b>Review Report</b> panel',
|
||||
ja: '<b>Ctrl + Shift + R</b> でレビューを実行、結果は<b>レビューレポート</b>に表示',
|
||||
},
|
||||
'setup.aiConnectionConfig': {
|
||||
'zh-CN': 'AI 连接配置',
|
||||
en: 'AI Connection Config',
|
||||
ja: 'AI接続設定',
|
||||
},
|
||||
'setup.engineSection': {
|
||||
'zh-CN': '审核引擎',
|
||||
en: 'Review Engines',
|
||||
@@ -267,6 +272,11 @@ const messages: Record<string, Record<Language, string>> = {
|
||||
en: 'Model Name',
|
||||
ja: 'モデル名',
|
||||
},
|
||||
'setup.modelPlaceholder': {
|
||||
'zh-CN': '输入或选择模型名',
|
||||
en: 'Enter or select model name',
|
||||
ja: 'モデル名を入力または選択',
|
||||
},
|
||||
'setup.modelHint': {
|
||||
'zh-CN': '建议使用支持结构化输出的模型。',
|
||||
en: 'Use a model that supports structured output.',
|
||||
@@ -342,6 +352,206 @@ const messages: Record<string, Record<Language, string>> = {
|
||||
en: 'Save & Test Connection',
|
||||
ja: '保存して接続テスト',
|
||||
},
|
||||
'setup.configured': {
|
||||
'zh-CN': '已配置',
|
||||
en: 'Configured',
|
||||
ja: '設定済み',
|
||||
},
|
||||
'setup.connected': {
|
||||
'zh-CN': '✓ 已连接',
|
||||
en: '✓ Connected',
|
||||
ja: '✓ 接続済み',
|
||||
},
|
||||
'setup.retry': {
|
||||
'zh-CN': '✗ 重试',
|
||||
en: '✗ Retry',
|
||||
ja: '✗ 再試行',
|
||||
},
|
||||
'setup.ruleCountFormat': {
|
||||
'zh-CN': '{0} 个文件',
|
||||
en: '{0} file(s)',
|
||||
ja: '{0} ファイル',
|
||||
},
|
||||
'setup.noRuleFiles': {
|
||||
'zh-CN': '暂无规则文件',
|
||||
en: 'No rule files',
|
||||
ja: 'ルールファイルなし',
|
||||
},
|
||||
'setup.adapter.subtitle': {
|
||||
'zh-CN': '静态分析适配器',
|
||||
en: 'Static Analysis Adapters',
|
||||
ja: '静的解析アダプター',
|
||||
},
|
||||
'setup.adapter.modeBuiltin': {
|
||||
'zh-CN': '内置规则',
|
||||
en: 'Built-in Rules',
|
||||
ja: '組み込みルール',
|
||||
},
|
||||
'setup.adapter.modeProject': {
|
||||
'zh-CN': '项目配置',
|
||||
en: 'Project Config',
|
||||
ja: 'プロジェクト設定',
|
||||
},
|
||||
'setup.adapter.modeGlobal': {
|
||||
'zh-CN': '全局配置',
|
||||
en: 'Global Config',
|
||||
ja: 'グローバル設定',
|
||||
},
|
||||
'setup.adapter.modeLegend': {
|
||||
'zh-CN': '插件按 内置<全局<项目的优先级自动选择配置来源',
|
||||
en: 'Auto-selects config by priority: Built-in < Global < Project',
|
||||
ja: '優先順位に従って自動選択: 組み込み < グローバル < プロジェクト',
|
||||
},
|
||||
'setup.adapter.configYes': {
|
||||
'zh-CN': '已配置',
|
||||
en: 'Configured',
|
||||
ja: '設定済み',
|
||||
},
|
||||
'setup.adapter.configNo': {
|
||||
'zh-CN': '未配置',
|
||||
en: 'Not configured',
|
||||
ja: '未設定',
|
||||
},
|
||||
'setup.adapter.langLabel': {
|
||||
'zh-CN': '可审查的语言:',
|
||||
en: 'Languages:',
|
||||
ja: '対応言語:',
|
||||
},
|
||||
'setup.adapter.tooltipTab': {
|
||||
'zh-CN': '点击展开/收起静态分析适配器',
|
||||
en: 'Click to expand/collapse static analysis adapters',
|
||||
ja: 'クリックで静的解析アダプターを展開/折りたたむ',
|
||||
},
|
||||
'setup.adapter.btnCreateConfig': {
|
||||
'zh-CN': '创建项目配置',
|
||||
en: 'Create Project Config',
|
||||
ja: 'プロジェクト設定を作成',
|
||||
},
|
||||
'setup.adapter.btnEditGlobal': {
|
||||
'zh-CN': '修改全局设置',
|
||||
en: 'Modify Global Settings',
|
||||
ja: 'グローバル設定を変更',
|
||||
},
|
||||
'setup.adapter.tooltipCreate': {
|
||||
'zh-CN': '在项目根目录创建 {0}',
|
||||
en: 'Create {0} in project root',
|
||||
ja: 'プロジェクトルートに {0} を作成',
|
||||
},
|
||||
'setup.adapter.tooltipEdit': {
|
||||
'zh-CN': '修改 VS Code 设置中的全局参数',
|
||||
en: 'Modify global parameters in VS Code settings',
|
||||
ja: 'VS Code設定のグローバルパラメータを変更',
|
||||
},
|
||||
'setup.adapter.toggleEnable': {
|
||||
'zh-CN': '启用 {0} 适配器',
|
||||
en: 'Enable {0} adapter',
|
||||
ja: '{0} アダプターを有効化',
|
||||
},
|
||||
'setup.adapter.toggleDisable': {
|
||||
'zh-CN': '禁用 {0} 适配器',
|
||||
en: 'Disable {0} adapter',
|
||||
ja: '{0} アダプターを無効化',
|
||||
},
|
||||
'setup.template.pmdBestPractices': {
|
||||
'zh-CN': 'Java 最佳实践(如:避免空 catch、关闭流等)',
|
||||
en: 'Java best practices (avoid empty catch, close streams, etc.)',
|
||||
ja: 'Javaベストプラクティス(空のcatch回避、ストリームクローズ等)',
|
||||
},
|
||||
'setup.template.pmdCodeStyle': {
|
||||
'zh-CN': 'Java 代码风格(如:命名规范、花括号位置等)',
|
||||
en: 'Java code style (naming conventions, brace placement, etc.)',
|
||||
ja: 'Javaコードスタイル(命名規則、ブレース位置等)',
|
||||
},
|
||||
'setup.template.sqlfluffDialect': {
|
||||
'zh-CN': '数据库方言:postgres / mysql / bigquery / snowflake 等',
|
||||
en: 'Database dialect: postgres / mysql / bigquery / snowflake etc.',
|
||||
ja: 'データベース方言:postgres / mysql / bigquery / snowflake など',
|
||||
},
|
||||
'setup.template.sqlfluffRules': {
|
||||
'zh-CN': 'all = 启用全部规则,也可指定规则名逗号分隔',
|
||||
en: 'all = enable all rules, or specify rule names separated by commas',
|
||||
ja: 'all = すべてのルールを有効、ルール名をカンマ区切りで指定可',
|
||||
},
|
||||
'setup.template.eslintComment1': {
|
||||
'zh-CN': '未使用的变量 → 警告',
|
||||
en: 'Unused variables → warning',
|
||||
ja: '未使用変数 → 警告',
|
||||
},
|
||||
'setup.template.eslintComment2': {
|
||||
'zh-CN': '允许使用 console',
|
||||
en: 'Allow console',
|
||||
ja: 'consoleを許可',
|
||||
},
|
||||
'setup.template.eslintComment3': {
|
||||
'zh-CN': '强制分号',
|
||||
en: 'Enforce semicolons',
|
||||
ja: 'セミコロンを強制',
|
||||
},
|
||||
'setup.template.stylelintComment1': {
|
||||
'zh-CN': '缩进 2 空格',
|
||||
en: 'Indentation: 2 spaces',
|
||||
ja: 'インデント: 2スペース',
|
||||
},
|
||||
'setup.template.stylelintComment2': {
|
||||
'zh-CN': '禁止空规则',
|
||||
en: 'No empty rules',
|
||||
ja: '空ルールを禁止',
|
||||
},
|
||||
'setup.adapter.pmdLanguages': {
|
||||
'zh-CN': 'Java(含 JSP 中的 Java 代码,例如<% ... %>)',
|
||||
en: 'Java (including Java code in JSP, e.g. <% ... %>)',
|
||||
ja: 'Java(JSP内のJavaコードを含む、例: <% ... %>)',
|
||||
},
|
||||
'setup.adapter.pmdGuide': {
|
||||
'zh-CN': '需要 Java 运行环境;项目根目录创建 ruleset.xml 或在设置中配置 pmd.rulesetPath',
|
||||
en: 'Requires Java runtime; create ruleset.xml in project root or set pmd.rulesetPath in settings',
|
||||
ja: 'Java実行環境が必要。プロジェクトルートにruleset.xmlを作成するか、設定でpmd.rulesetPathを設定してください',
|
||||
},
|
||||
'setup.adapter.sqlLanguages': {
|
||||
'zh-CN': 'SQL',
|
||||
en: 'SQL',
|
||||
ja: 'SQL',
|
||||
},
|
||||
'setup.adapter.sqlGuide': {
|
||||
'zh-CN': '需要 Python 环境和 sqlfluff;运行 pip install sqlfluff,项目根目录创建 .sqlfluff',
|
||||
en: 'Requires Python and sqlfluff; run pip install sqlfluff, create .sqlfluff in project root',
|
||||
ja: 'Python環境とsqlfluffが必要。pip install sqlfluff を実行し、プロジェクトルートに.sqlfluffを作成してください',
|
||||
},
|
||||
'setup.adapter.eslintLanguages': {
|
||||
'zh-CN': 'JS, TS, JSX, TSX(含 JSP 中的 JavaScript 代码,例如<script>)',
|
||||
en: 'JS, TS, JSX, TSX (including JavaScript in JSP, e.g. <script>)',
|
||||
ja: 'JS, TS, JSX, TSX(JSP内のJavaScriptコードを含む、例: <script>)',
|
||||
},
|
||||
'setup.adapter.eslintGuide': {
|
||||
'zh-CN': '项目根目录创建 .eslintrc.js 或在 VS Code 设置中配置 eslintConfigPath',
|
||||
en: 'Create .eslintrc.js in project root or set eslintConfigPath in VS Code settings',
|
||||
ja: 'プロジェクトルートに.eslintrc.jsを作成するか、VS Code設定でeslintConfigPathを設定してください',
|
||||
},
|
||||
'setup.adapter.stylelintLanguages': {
|
||||
'zh-CN': 'CSS, SCSS, Less(含 JSP 中的 CSS 代码,例如<style>)',
|
||||
en: 'CSS, SCSS, Less (including CSS in JSP, e.g. <style>)',
|
||||
ja: 'CSS, SCSS, Less(JSP内のCSSコードを含む、例: <style>)',
|
||||
},
|
||||
'setup.adapter.stylelintGuide': {
|
||||
'zh-CN': '项目根目录创建 .stylelintrc 或在 VS Code 设置中配置 stylelintConfigPath',
|
||||
en: 'Create .stylelintrc in project root or set stylelintConfigPath in VS Code settings',
|
||||
ja: 'プロジェクトルートに.stylelintrcを作成するか、VS Code設定でstylelintConfigPathを設定してください',
|
||||
},
|
||||
'setup.aiReviewStatusCapability': {
|
||||
'zh-CN': '审查能力',
|
||||
en: 'Capability',
|
||||
ja: 'レビュー機能',
|
||||
},
|
||||
'setup.aiReviewCapability': {
|
||||
'zh-CN': '修改建议 · 自定义规则审查 · 深度代码审查',
|
||||
en: 'Fix Suggestions · Custom Rules Review · Deep Code Review',
|
||||
ja: '修正提案 · カスタムルールレビュー · 詳細コードレビュー',
|
||||
},
|
||||
'setup.notConnected': {
|
||||
'zh-CN': '未连接',
|
||||
en: 'Not connected',
|
||||
ja: '未接続',
|
||||
},
|
||||
'importPreview.title': {
|
||||
'zh-CN': '规则导入预览',
|
||||
en: 'Rule Import Preview',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import type { LinterAdapter, LinterDiagnostic } from '../types';
|
||||
import { getLinterForLanguage } from '../config';
|
||||
import { getLinterForLanguage, isAdapterEnabled } from '../config';
|
||||
import { ESLintAdapter } from '../adapters/eslint';
|
||||
import { PmdAdapter } from '../adapters/pmd';
|
||||
import { StylelintAdapter } from '../adapters/stylelint';
|
||||
@@ -59,6 +59,15 @@ export class Orchestrator {
|
||||
};
|
||||
}
|
||||
|
||||
if (!isAdapterEnabled(adapter.id)) {
|
||||
return {
|
||||
diagnostics: [],
|
||||
errors: [],
|
||||
adapterIds: [],
|
||||
duration: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await adapter.check(document, workingDir);
|
||||
const errors: string[] = [];
|
||||
if (result.status !== 'ok') {
|
||||
|
||||
@@ -43,6 +43,10 @@ const p: Record<Lang, PromptStrings> = {
|
||||
'- 混合形式(段落 + 列表 + 表格组合)',
|
||||
'',
|
||||
'不得因输入格式非标准而拒绝转换。应主动从松散描述中提取规则语义。',
|
||||
'',
|
||||
'- 按输入文档的段落或列表项顺序处理,保持原文顺序输出',
|
||||
'- 不要合并或拆分原文中已是独立条目的规则',
|
||||
'- 一个段落包含多条规则时才拆分,单条规则不要拆成多条',
|
||||
],
|
||||
nonRuleFilterTitle: '## 非规则内容过滤',
|
||||
nonRuleFilterLines: [
|
||||
@@ -58,12 +62,17 @@ const p: Record<Lang, PromptStrings> = {
|
||||
' **必须**基于规则描述内容自动生成语义化的 id',
|
||||
' 即使输入中无显式 id 标识,也必须根据 description/message 的语义推断出合适的 id',
|
||||
' 多条规则之间 id 不得重复',
|
||||
' id 只能使用 description/message 中已有的英文单词或短语,转换为 kebab-case',
|
||||
' 不要自行创造原文中没有的英文词汇',
|
||||
' 如果输入全中文,从语义提取核心关键词翻译为简短英文(2-4 个词)',
|
||||
'- severity: 严重级别(error / warning / info)',
|
||||
' **必须**输出。按规则语义推断:',
|
||||
' error: 会导致 bug / 安全问题 / 数据损坏',
|
||||
' warning: 潜在问题 / 不良实践',
|
||||
' info: 风格 / 可读性建议',
|
||||
' 即使输入中无显式严重级别,也必须根据规则后果的严重程度推断',
|
||||
' 如果无法从输入中确定严重级别,默认填 warning',
|
||||
' 只有明确涉及安全、数据泄露、崩溃风险时才填 error',
|
||||
'- description: 规则简短描述',
|
||||
' **必须**输出。若输入中不明显,从 message 的内容反向推导出简短描述',
|
||||
'- message: 违反时的提示消息',
|
||||
@@ -135,7 +144,7 @@ const p: Record<Lang, PromptStrings> = {
|
||||
'',
|
||||
'仅输出 YAML,不要额外说明。',
|
||||
],
|
||||
finalInstruction: '',
|
||||
finalInstruction: '只输出 YAML 内容,不要输出 markdown 代码块标记,不要输出解释性文字',
|
||||
dedupHeader: '## 已知规则清单(用于重复检测)',
|
||||
dedupLinterLabel: (name, count) => `### ${name} (${count} 条)`,
|
||||
dedupCustomLabel: (count) => `### 已导入的自定义规则 (${count} 条)`,
|
||||
@@ -154,6 +163,10 @@ const p: Record<Lang, PromptStrings> = {
|
||||
'- Mixed forms (paragraphs + lists + tables)',
|
||||
'',
|
||||
'Do not reject conversion due to non-standard input format. Actively extract rule semantics from loose descriptions.',
|
||||
'',
|
||||
'- Process in the order of the input document paragraphs or list items, preserving original order',
|
||||
'- Do not merge or split entries that are already independent rules in the original text',
|
||||
'- Only split when a single paragraph contains multiple rules; do not split a single rule into multiple',
|
||||
],
|
||||
nonRuleFilterTitle: '## Non-Rule Content Filtering',
|
||||
nonRuleFilterLines: [
|
||||
@@ -169,12 +182,17 @@ const p: Record<Lang, PromptStrings> = {
|
||||
' **Must** generate a semantic id based on the rule description content',
|
||||
' Even if no explicit id is present in the input, infer a suitable id from the description/message semantics',
|
||||
' IDs must not be duplicated across rules',
|
||||
' id must use only English words or phrases already present in description/message, converted to kebab-case',
|
||||
' Do not invent English words not found in the original text',
|
||||
' If input is entirely in Chinese, extract core semantic keywords and translate to short English (2-4 words)',
|
||||
'- severity: Severity level (error / warning / info)',
|
||||
' **Must** output. Infer based on rule semantics:',
|
||||
' error: causes bugs / security issues / data corruption',
|
||||
' warning: potential issues / bad practices',
|
||||
' info: style / readability suggestions',
|
||||
' Even if no explicit severity is given, infer from the rule\'s impact',
|
||||
' If severity cannot be determined from input, default to warning',
|
||||
' Only use error when the rule clearly involves security, data leakage, or crash risk',
|
||||
'- description: Short rule description',
|
||||
' **Must** output. If not obvious from input, derive from message content',
|
||||
'- message: Violation message',
|
||||
@@ -246,7 +264,7 @@ const p: Record<Lang, PromptStrings> = {
|
||||
'',
|
||||
'Output YAML only, no extra explanation.',
|
||||
],
|
||||
finalInstruction: 'All descriptions and messages must be written in English.',
|
||||
finalInstruction: 'All descriptions and messages must be written in English.\nOutput YAML only, no markdown code fences, no explanatory text',
|
||||
dedupHeader: '## Known Rules (for duplicate detection)',
|
||||
dedupLinterLabel: (name, count) => `### ${name} (${count} rules)`,
|
||||
dedupCustomLabel: (count) => `### Imported custom rules (${count} rules)`,
|
||||
@@ -262,9 +280,13 @@ const p: Record<Lang, PromptStrings> = {
|
||||
'- 自然言語の段落(1つ以上の段落でルールを記述)',
|
||||
'- 順不同リスト(各ルールが1行または1段落)',
|
||||
'- テーブル(列名は固定されていません。意味から推測してください)',
|
||||
'- 混合形式(段落 + リスト + テーブルの組み合わせ)',
|
||||
'- 混合形式(段落 + リスト + テーブルの組み込み)',
|
||||
'',
|
||||
'非標準的な入力形式であっても変換を拒否してはいけません。緩やかな記述からルールの意味を積極的に抽出してください。',
|
||||
'',
|
||||
'- 入力ドキュメントの段落またはリスト項目の順序で処理し、原文の順序を保持する',
|
||||
'- 原文ですでに独立したエントリであるルールを結合または分割しない',
|
||||
'- 単一の段落に複数のルールが含まれる場合のみ分割し、単一ルールを複数に分割しない',
|
||||
],
|
||||
nonRuleFilterTitle: '## 非ルールコンテンツのフィルタリング',
|
||||
nonRuleFilterLines: [
|
||||
@@ -280,12 +302,17 @@ const p: Record<Lang, PromptStrings> = {
|
||||
' **必須** ルール説明内容に基づいて意味的なidを自動生成する',
|
||||
' 入力に明示的なidがない場合でも、description/messageの意味から適切なidを推測する',
|
||||
' 複数ルール間でidが重複してはいけない',
|
||||
' idはdescription/messageにすでに存在する英単語またはフレーズのみを使用し、kebab-caseに変換する',
|
||||
' 原文にない英単語を独自に作成しない',
|
||||
' 入力がすべて日本語の場合は、セマンティクスから核心キーワードを抽出し、短い英語(2〜4語)に翻訳する',
|
||||
'- severity: 重大度(error / warning / info)',
|
||||
' **必須**で出力。ルールの意味に従って推測:',
|
||||
' error: バグ/セキュリティ問題/データ破損を引き起こす',
|
||||
' warning: 潜在的な問題/悪い慣行',
|
||||
' info: スタイル/可読性の提案',
|
||||
' 入力に明示的な重大度がない場合でも、ルールの影響の重大さから推測する',
|
||||
' 入力から重大度を判断できない場合は、デフォルトでwarningとする',
|
||||
' セキュリティ、データ漏洩、クラッシュリスクに明確に関連する場合のみerrorとする',
|
||||
'- description: ルールの簡単な説明',
|
||||
' **必須**で出力。入力で不明確な場合、messageの内容から逆算して短い説明を導出',
|
||||
'- message: 違反時のメッセージ',
|
||||
@@ -357,7 +384,7 @@ const p: Record<Lang, PromptStrings> = {
|
||||
'',
|
||||
'YAMLのみを出力し、追加説明は不要です。',
|
||||
],
|
||||
finalInstruction: 'すべてのdescriptionとmessageは日本語で出力してください。',
|
||||
finalInstruction: 'すべてのdescriptionとmessageは日本語で出力してください。\nYAML のみ出力、マークダウンコードブロックなし、説明テキストなし',
|
||||
dedupHeader: '## 既知ルール一覧(重複検出用)',
|
||||
dedupLinterLabel: (name, count) => `### ${name}(${count} 件)`,
|
||||
dedupCustomLabel: (count) => `### インポート済みカスタムルール(${count} 件)`,
|
||||
|
||||
@@ -218,6 +218,30 @@ function buildFinalYamlFromRaw(
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
function normalizeRuleIds(rules: ImportableRule[]): ImportableRule[] {
|
||||
const seen = new Set<string>();
|
||||
return rules.map((rule, idx) => {
|
||||
let id = rule.id
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
if (!id) {
|
||||
id = `rule-${idx + 1}`;
|
||||
}
|
||||
|
||||
if (seen.has(id)) {
|
||||
let suffix = 2;
|
||||
while (seen.has(`${id}-${suffix}`)) { suffix++; }
|
||||
id = `${id}-${suffix}`;
|
||||
}
|
||||
seen.add(id);
|
||||
|
||||
return { ...rule, id };
|
||||
});
|
||||
}
|
||||
|
||||
export class ImportService {
|
||||
private converters: Map<string, RuleConverter> = new Map();
|
||||
|
||||
@@ -245,7 +269,8 @@ export class ImportService {
|
||||
throw new Error(t('import.conversionFailed'));
|
||||
}
|
||||
|
||||
const rules = parseImportableYaml(yamlContent);
|
||||
const parsedRules = parseImportableYaml(yamlContent);
|
||||
const rules = normalizeRuleIds(parsedRules);
|
||||
const exactCount = rules.filter(r => r.duplicateLevel === 'exact').length;
|
||||
const overlapCount = rules.filter(r => r.duplicateLevel === 'overlap').length;
|
||||
|
||||
@@ -354,7 +379,7 @@ export async function convertContentWithAI(
|
||||
}
|
||||
|
||||
const config = getAIConfig();
|
||||
const provider = createProvider(config.provider, apiKey, config.baseUrl);
|
||||
const provider = createProvider(config.provider, apiKey, config.baseUrl, context.extensionUri);
|
||||
|
||||
const prompt = systemPrompt ?? buildFallbackPrompt();
|
||||
|
||||
@@ -362,9 +387,10 @@ export async function convertContentWithAI(
|
||||
try {
|
||||
yamlOutput = await provider.chat(prompt, content, {
|
||||
model: config.model,
|
||||
temperature: 0.1,
|
||||
maxTokens: 4096,
|
||||
temperature: 0,
|
||||
maxTokens: 8192,
|
||||
timeoutMs: getAITimeout() * 1000,
|
||||
seed: 42,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import { ESLintAdapter } from '../adapters/eslint';
|
||||
import { StylelintAdapter } from '../adapters/stylelint';
|
||||
|
||||
suite('Adapter Tests', () => {
|
||||
test('ESLintAdapter has correct id and languages', () => {
|
||||
const adapter = new ESLintAdapter();
|
||||
assert.strictEqual(adapter.id, 'eslint');
|
||||
assert.deepStrictEqual(adapter.supportedLanguages, ['javascript', 'typescript']);
|
||||
});
|
||||
|
||||
test('StylelintAdapter has correct id and languages', () => {
|
||||
const adapter = new StylelintAdapter();
|
||||
assert.strictEqual(adapter.id, 'stylelint');
|
||||
assert.deepStrictEqual(adapter.supportedLanguages, ['css']);
|
||||
});
|
||||
|
||||
test('ESLintAdapter check returns AdapterResult structure', async () => {
|
||||
const adapter = new ESLintAdapter();
|
||||
if (!adapter.isAvailable()) { return; }
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument({
|
||||
content: 'const x = 1;\nconsole.log(x);\n',
|
||||
language: 'javascript',
|
||||
});
|
||||
|
||||
const result = await adapter.check(doc, __dirname);
|
||||
assert.ok(result.status === 'ok' || result.status === 'tool-unavailable');
|
||||
assert.ok(Array.isArray(result.diagnostics));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as assert from 'assert';
|
||||
import { getAIConfig, getAIProvider, getAIModel, getAIBaseUrl, getAITemperature, getAITimeout, getAIMaxTokens, getAIOutputLanguage } from '../config/ai';
|
||||
import { getLinterForLanguage, getPMDJarPath, getPMDRulesetPath } from '../config/linter';
|
||||
import { getContextLines } from '../config/fixer';
|
||||
|
||||
suite('Config Tests', () => {
|
||||
test('getAIConfig returns default values', () => {
|
||||
const config = getAIConfig();
|
||||
assert.strictEqual(config.provider, 'deepseek');
|
||||
assert.strictEqual(config.model, 'deepseek-chat');
|
||||
assert.strictEqual(config.baseUrl, 'https://api.deepseek.com/v1');
|
||||
assert.strictEqual(config.outputLanguage, 'zh-CN');
|
||||
});
|
||||
|
||||
test('AI individual getters return defaults', () => {
|
||||
assert.strictEqual(getAIProvider(), 'deepseek');
|
||||
assert.strictEqual(getAIModel(), 'deepseek-chat');
|
||||
assert.strictEqual(getAIBaseUrl(), 'https://api.deepseek.com/v1');
|
||||
assert.strictEqual(getAITemperature(), 0.2);
|
||||
assert.strictEqual(getAITimeout(), 300);
|
||||
assert.strictEqual(getAIMaxTokens(), 8192);
|
||||
assert.strictEqual(getAIOutputLanguage(), 'zh-CN');
|
||||
});
|
||||
|
||||
test('getLinterForLanguage returns configured linter', () => {
|
||||
assert.strictEqual(getLinterForLanguage('javascript'), 'eslint');
|
||||
assert.strictEqual(getLinterForLanguage('java'), 'pmd');
|
||||
});
|
||||
|
||||
test('getContextLines returns default', () => {
|
||||
assert.strictEqual(getContextLines(), 5);
|
||||
});
|
||||
});
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
public class Sample {
|
||||
public void test() {
|
||||
String password = "admin123";
|
||||
System.out.println("debug");
|
||||
System.out.println("debug");
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
.hello { color: black; background: #FFF; }
|
||||
#test { margin: 0px; }
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
function test() {
|
||||
var unused = 1;
|
||||
console.log('debug');
|
||||
return "hello world";
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import * as assert from 'assert';
|
||||
import { buildFinalYaml, parseImportableYaml } from '../rules/import-service';
|
||||
import type { ImportableRule, PreviewDecision } from '../rules/import-types';
|
||||
|
||||
function makeRules(data: Array<Partial<ImportableRule>>): ImportableRule[] {
|
||||
return data.map(d => ({
|
||||
id: d.id ?? 'test-rule',
|
||||
severity: d.severity ?? 'warning',
|
||||
description: d.description ?? 'test description',
|
||||
message: d.message ?? 'test message',
|
||||
languages: d.languages,
|
||||
excludeLanguages: d.excludeLanguages,
|
||||
duplicateOf: d.duplicateOf,
|
||||
duplicateLevel: d.duplicateLevel,
|
||||
duplicateReason: d.duplicateReason,
|
||||
}));
|
||||
}
|
||||
|
||||
function defaultDecision(rules: ImportableRule[]): PreviewDecision {
|
||||
const keepRule: Record<string, boolean> = {};
|
||||
for (const rule of rules) {
|
||||
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
|
||||
}
|
||||
return { keepRule, confirmed: true };
|
||||
}
|
||||
|
||||
function makeYaml(rules: Array<{ id: string; fields: Record<string, string> }>): string {
|
||||
return rules.map(r => {
|
||||
const lines = [`- id: ${r.id}`];
|
||||
for (const [key, value] of Object.entries(r.fields)) {
|
||||
lines.push(` ${key}: ${value}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}).join('\n\n') + '\n';
|
||||
}
|
||||
|
||||
suite('Import Dedup Tests', () => {
|
||||
|
||||
test('无重复规则→全部保留,无注释行', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'rule-a', duplicateLevel: 'none' },
|
||||
{ id: 'rule-b' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'rule-a', fields: { severity: 'warning', description: 'desc a', message: 'msg a', duplicateLevel: 'none' } },
|
||||
{ id: 'rule-b', fields: { severity: 'info', description: 'desc b', message: 'msg b' } },
|
||||
]);
|
||||
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
||||
assert.ok(!result.includes('# [DUPLICATE'), 'Should have no duplicate headers');
|
||||
assert.ok(result.includes('id: rule-a'));
|
||||
assert.ok(result.includes('id: rule-b'));
|
||||
assert.ok(!result.includes('duplicateLevel:'), 'duplicateLevel should be stripped');
|
||||
});
|
||||
|
||||
test('全部 exact→全部注释', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'rule-a', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' },
|
||||
{ id: 'rule-b', duplicateOf: 'eslint/no-debugger', duplicateLevel: 'exact' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'rule-a', fields: { severity: 'warning', description: 'desc a', message: 'msg a', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
||||
{ id: 'rule-b', fields: { severity: 'error', description: 'desc b', message: 'msg b', duplicateOf: 'eslint/no-debugger', duplicateLevel: 'exact' } },
|
||||
]);
|
||||
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
||||
assert.ok(!result.match(/^[^#]*- id:/m), 'No non-commented rule lines');
|
||||
assert.ok(result.includes('[DUPLICATE: exact]'));
|
||||
assert.ok(result.includes('# 如需启用'));
|
||||
});
|
||||
|
||||
test('全部 overlap→全部保留,无注释行', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'rule-a', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求 logger' },
|
||||
{ id: 'rule-b', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap', duplicateReason: '更窄范围' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'rule-a', fields: { severity: 'warning', description: 'desc a', message: 'msg a', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求 logger' } },
|
||||
{ id: 'rule-b', fields: { severity: 'warning', description: 'desc b', message: 'msg b', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap', duplicateReason: '更窄范围' } },
|
||||
]);
|
||||
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
||||
assert.ok(!result.includes('# [DUPLICATE'), 'Overlap rules should not be commented by default');
|
||||
assert.ok(result.includes('id: rule-a'));
|
||||
assert.ok(result.includes('id: rule-b'));
|
||||
});
|
||||
|
||||
test('混合三档→exact注释,overlap和none保留', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'exact-rule', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' },
|
||||
{ id: 'overlap-rule', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap' },
|
||||
{ id: 'none-rule', duplicateLevel: 'none' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'exact-rule', fields: { severity: 'warning', description: 'd1', message: 'm1', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
||||
{ id: 'overlap-rule', fields: { severity: 'warning', description: 'd2', message: 'm2', duplicateOf: 'eslint/no-unused', duplicateLevel: 'overlap' } },
|
||||
{ id: 'none-rule', fields: { severity: 'info', description: 'd3', message: 'm3', duplicateLevel: 'none' } },
|
||||
]);
|
||||
const result = buildFinalYaml(yaml, rules, defaultDecision(rules));
|
||||
assert.ok(result.includes('[DUPLICATE: exact]'));
|
||||
assert.ok(!result.match(/^-\s+id:\s+exact-rule/m), 'exact rule should be commented');
|
||||
assert.ok(result.match(/^-\s+id:\s+overlap-rule/m), 'overlap rule should be active');
|
||||
assert.ok(result.match(/^-\s+id:\s+none-rule/m), 'none rule should be active');
|
||||
});
|
||||
|
||||
test('用户恢复 exact 规则→取消注释', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'restored', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'restored', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { restored: true }, confirmed: true };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(!result.includes('[DUPLICATE'), 'Restored rule should have no duplicate annotation');
|
||||
assert.ok(result.match(/^-\s+id:\s+restored/m), 'Restored rule should be active');
|
||||
});
|
||||
|
||||
test('用户注释 overlap 规则→加 # 前缀和注释头', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'commented', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'commented', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '额外要求' } },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { commented: false }, confirmed: true };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(result.includes('[DUPLICATE: overlap]'));
|
||||
assert.ok(result.includes('重叠原因:额外要求'));
|
||||
assert.ok(!result.match(/^-\s+id:\s+commented/m), 'Commented rule should have # prefix');
|
||||
});
|
||||
|
||||
test('用户注释 none 规则→加 # 前缀', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'comment-none', duplicateLevel: 'none' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'comment-none', fields: { severity: 'warning', description: 'd', message: 'm', duplicateLevel: 'none' } },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { 'comment-none': false }, confirmed: true };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(!result.match(/^-\s+id:\s+comment-none/m), 'Should be commented');
|
||||
assert.ok(result.includes('# - id: comment-none'));
|
||||
});
|
||||
|
||||
test('保留规则→duplicateLevel/duplicateOf/duplicateReason 行被移除', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'kept', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: 'reason' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'kept', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: 'reason' } },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { kept: true }, confirmed: true };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(!result.includes('duplicateOf:'));
|
||||
assert.ok(!result.includes('duplicateLevel:'));
|
||||
assert.ok(!result.includes('duplicateReason:'));
|
||||
assert.ok(result.includes('id: kept'));
|
||||
});
|
||||
|
||||
test('注释规则含 duplicateReason→注释头包含重叠原因', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'r1', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '检测目标相同但额外要求 logger' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'r1', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'overlap', duplicateReason: '检测目标相同但额外要求 logger' } },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { r1: false }, confirmed: true };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(result.includes('重叠原因:检测目标相同但额外要求 logger'));
|
||||
});
|
||||
|
||||
test('编辑 description 后确认→YAML 使用新值', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'edit-desc', duplicateLevel: 'none' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'edit-desc', fields: { severity: 'warning', description: '旧描述', message: '旧消息' } },
|
||||
]);
|
||||
const editedRules = makeRules([
|
||||
{ id: 'edit-desc', severity: 'warning', description: '新描述', message: '新消息' },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { 'edit-desc': true }, confirmed: true, editedRules };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(result.includes('description: 新描述'));
|
||||
assert.ok(!result.includes('旧描述'));
|
||||
assert.ok(result.includes('message: 新消息'));
|
||||
});
|
||||
|
||||
test('编辑 severity 后确认→YAML 使用新 severity', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'edit-sev', duplicateLevel: 'none' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'edit-sev', fields: { severity: 'warning', description: 'd', message: 'm' } },
|
||||
]);
|
||||
const editedRules = makeRules([
|
||||
{ id: 'edit-sev', severity: 'error', description: 'd', message: 'm' },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { 'edit-sev': true }, confirmed: true, editedRules };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(result.includes('severity: error'));
|
||||
assert.ok(!result.includes('severity: warning'));
|
||||
});
|
||||
|
||||
test('编辑 languages 后确认→YAML 含新 languages', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'edit-lang', duplicateLevel: 'none', languages: ['java'] },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'edit-lang', fields: { severity: 'warning', description: 'd', message: 'm', languages: '[java]' } },
|
||||
]);
|
||||
const editedRules = makeRules([
|
||||
{ id: 'edit-lang', severity: 'warning', description: 'd', message: 'm', languages: ['javascript', 'typescript'] },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { 'edit-lang': true }, confirmed: true, editedRules };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(result.includes('languages: [javascript, typescript]'));
|
||||
assert.ok(!result.includes('languages: [java]'), 'old java language should be gone');
|
||||
assert.ok(!result.includes(' [java]'), 'standalone java tag should be gone');
|
||||
});
|
||||
|
||||
test('编辑后切换为注释→注释内容为编辑后的值', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'edit-comment', duplicateLevel: 'none' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'edit-comment', fields: { severity: 'warning', description: '原描述', message: '原消息' } },
|
||||
]);
|
||||
const editedRules = makeRules([
|
||||
{ id: 'edit-comment', severity: 'error', description: '新描述', message: '新消息' },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { 'edit-comment': false }, confirmed: true, editedRules };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(result.includes('# severity: error'), 'should have commented severity: error');
|
||||
assert.ok(result.includes('# description: 新描述'));
|
||||
assert.ok(result.includes('# message: 新消息'));
|
||||
assert.ok(result.includes('[手动注释]'));
|
||||
});
|
||||
|
||||
test('无编辑场景→回退到原始 yamlContent 处理', () => {
|
||||
const rules = makeRules([
|
||||
{ id: 'fallback', duplicateLevel: 'exact', duplicateOf: 'eslint/no-console' },
|
||||
]);
|
||||
const yaml = makeYaml([
|
||||
{ id: 'fallback', fields: { severity: 'warning', description: 'd', message: 'm', duplicateOf: 'eslint/no-console', duplicateLevel: 'exact' } },
|
||||
]);
|
||||
const decision: PreviewDecision = { keepRule: { fallback: false }, confirmed: true };
|
||||
const result = buildFinalYaml(yaml, rules, decision);
|
||||
assert.ok(result.includes('[DUPLICATE: exact]'));
|
||||
assert.ok(!result.match(/^-\s+id:\s+fallback/m));
|
||||
assert.ok(result.includes('duplicateOf:'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('parseImportableYaml Fallback Tests', () => {
|
||||
|
||||
test('severity 缺失→降级为 warning', () => {
|
||||
const yaml = `- id: test-rule
|
||||
description: test desc
|
||||
message: test msg`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 1);
|
||||
assert.strictEqual(rules[0].severity, 'warning');
|
||||
});
|
||||
|
||||
test('severity 非法值→降级为 warning', () => {
|
||||
const yaml = `- id: test-rule
|
||||
severity: critical
|
||||
description: test desc
|
||||
message: test msg`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 1);
|
||||
assert.strictEqual(rules[0].severity, 'warning');
|
||||
});
|
||||
|
||||
test('id 缺失→生成 rule-N', () => {
|
||||
const yaml = `- severity: warning
|
||||
description: test desc
|
||||
message: test msg`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 1);
|
||||
assert.strictEqual(rules[0].id, 'rule-1');
|
||||
});
|
||||
|
||||
test('多条 id 缺失→rule-1, rule-2...', () => {
|
||||
const yaml = `- severity: warning
|
||||
description: desc a
|
||||
message: msg a
|
||||
- severity: info
|
||||
description: desc b
|
||||
message: msg b`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 2);
|
||||
assert.strictEqual(rules[0].id, 'rule-1');
|
||||
assert.strictEqual(rules[1].id, 'rule-2');
|
||||
});
|
||||
|
||||
test('description 缺失、message 存在→互填', () => {
|
||||
const yaml = `- id: test-rule
|
||||
severity: error
|
||||
message: test msg`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 1);
|
||||
assert.strictEqual(rules[0].description, 'test msg');
|
||||
assert.strictEqual(rules[0].message, 'test msg');
|
||||
});
|
||||
|
||||
test('message 缺失、description 存在→互填', () => {
|
||||
const yaml = `- id: test-rule
|
||||
severity: error
|
||||
description: test desc`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 1);
|
||||
assert.strictEqual(rules[0].description, 'test desc');
|
||||
assert.strictEqual(rules[0].message, 'test desc');
|
||||
});
|
||||
|
||||
test('description 与 message 同时缺失→丢弃', () => {
|
||||
const yaml = `- id: test-rule
|
||||
severity: error
|
||||
- id: test-rule2
|
||||
severity: warning
|
||||
description: test desc
|
||||
message: test msg`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 1);
|
||||
assert.strictEqual(rules[0].id, 'test-rule2');
|
||||
});
|
||||
|
||||
test('正常完整输入→无回归', () => {
|
||||
const yaml = `- id: no-console-log
|
||||
severity: error
|
||||
description: 禁止使用 console.log
|
||||
message: 请使用 logger 替代
|
||||
- id: no-unused-vars
|
||||
severity: warning
|
||||
description: 禁止未使用变量
|
||||
message: 删除或注释未使用变量
|
||||
duplicateOf: eslint/no-unused-vars
|
||||
duplicateLevel: exact`;
|
||||
const rules = parseImportableYaml(yaml);
|
||||
assert.strictEqual(rules.length, 2);
|
||||
assert.strictEqual(rules[0].severity, 'error');
|
||||
assert.strictEqual(rules[0].description, '禁止使用 console.log');
|
||||
assert.strictEqual(rules[1].duplicateLevel, 'exact');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
public class Buggy {
|
||||
public void test() {
|
||||
String password = "admin123";
|
||||
String name = "test";
|
||||
int x = 1;
|
||||
int y = 2;
|
||||
int z = x + y;
|
||||
System.out.println("debug");
|
||||
System.out.println("done");
|
||||
}
|
||||
|
||||
public void duplicate() {
|
||||
String password = "secret";
|
||||
System.out.println(password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.hello { color: #FFFFFF; background: black; }
|
||||
#test { margin: 0px; }
|
||||
.foo { font-size: 12px; }
|
||||
@@ -0,0 +1,11 @@
|
||||
var x = 1;
|
||||
var y = 2;
|
||||
var x = 3;
|
||||
|
||||
function test() {
|
||||
var unused = 'hello';
|
||||
console.log('debug');
|
||||
return "world";
|
||||
}
|
||||
|
||||
test();
|
||||
@@ -0,0 +1,42 @@
|
||||
<%@ page language="java" contentType="text/html" %>
|
||||
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
|
||||
<html>
|
||||
<head>
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
.btn { color: #fff; background: blue; }
|
||||
.btn { font-size: 14px; }
|
||||
body { color: #fff; }
|
||||
body { margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%
|
||||
String password = "admin123";
|
||||
String name = "test";
|
||||
int x = 1;
|
||||
%>
|
||||
|
||||
<p>${message} ${message}</p>
|
||||
|
||||
<c:if test="${not empty user}">
|
||||
<p>Welcome, ${user.name}</p>
|
||||
</c:if>
|
||||
|
||||
<c:forEach items="${list}" var="item">
|
||||
<span>${item}</span>
|
||||
</c:forEach>
|
||||
|
||||
<c:out value="${xssInput}" />
|
||||
|
||||
<c:forEach begin="1" end="5">
|
||||
<span>hello</span>
|
||||
</c:forEach>
|
||||
|
||||
<script>
|
||||
var msg = "hello";
|
||||
var msg = "world";
|
||||
console.log(msg);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
SELECT name FORM users;
|
||||
SELECT * FORM products;
|
||||
INSERT INTO customers VALUES (1, 'test');
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as assert from 'assert';
|
||||
import { mergeResults, MergedReport } from '../merger/merger';
|
||||
import { CustomRuleResult, AIFinding } from '../ai/schema';
|
||||
import { LinterDiagnostic } from '../types';
|
||||
|
||||
suite('Merger Tests', () => {
|
||||
test('mergeResults counts correctly', () => {
|
||||
const staticDiags: LinterDiagnostic[] = [
|
||||
{ severity: 'error', ruleId: 'eslint:no-unused', message: 'x is unused', range: new (require('vscode').Range)(0, 0, 0, 1) },
|
||||
];
|
||||
const customResults: CustomRuleResult[] = [
|
||||
{ ruleId: 'custom:no-console', line: 5, severity: 'warning', message: 'avoid console.log' },
|
||||
];
|
||||
const aiFindings: AIFinding[] = [
|
||||
{ ruleId: 'hardcoded-secret', severity: 'error', category: 'security', title: 'Hardcoded', description: 'Found secret', suggestion: 'Use env', line: 3 },
|
||||
];
|
||||
|
||||
const report = mergeResults({
|
||||
staticDiagnostics: staticDiags,
|
||||
customRuleResults: customResults,
|
||||
translatedDiagnostics: [],
|
||||
aiFindings,
|
||||
errors: [],
|
||||
degraded: false,
|
||||
startTime: Date.now(),
|
||||
filePath: '/test/sample.js',
|
||||
language: 'javascript',
|
||||
adapterIds: ['eslint'],
|
||||
});
|
||||
|
||||
assert.strictEqual(report.linterCount, 1);
|
||||
assert.strictEqual(report.customRuleCount, 1);
|
||||
assert.strictEqual(report.aiCount, 1);
|
||||
assert.strictEqual(report.degraded, false);
|
||||
assert.strictEqual(report.language, 'javascript');
|
||||
});
|
||||
|
||||
test('mergeResults marks degraded when AI fails', () => {
|
||||
const report = mergeResults({
|
||||
staticDiagnostics: [],
|
||||
customRuleResults: [],
|
||||
translatedDiagnostics: [],
|
||||
aiFindings: [],
|
||||
errors: ['AI 请求超时'],
|
||||
degraded: true,
|
||||
startTime: Date.now(),
|
||||
filePath: '/test/sample.js',
|
||||
language: 'javascript',
|
||||
adapterIds: ['eslint'],
|
||||
});
|
||||
|
||||
assert.strictEqual(report.degraded, true);
|
||||
assert.strictEqual(report.errors.length, 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as assert from 'assert';
|
||||
import { t, setLanguage, getLanguage, getMessageKeys, Language } from '../i18n/messages';
|
||||
|
||||
suite('I18n Tests', () => {
|
||||
test('all message keys have values for all three languages', () => {
|
||||
const languages: Language[] = ['zh-CN', 'en', 'ja'];
|
||||
const keys = getMessageKeys();
|
||||
|
||||
for (const lang of languages) {
|
||||
setLanguage(lang);
|
||||
for (const key of keys) {
|
||||
const result = t(key);
|
||||
assert.ok(result, `Key "${key}" is empty for language "${lang}"`);
|
||||
assert.notStrictEqual(result, key, `Key "${key}" has no translation for language "${lang}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('default language is zh-CN', () => {
|
||||
setLanguage('zh-CN');
|
||||
assert.strictEqual(getLanguage(), 'zh-CN');
|
||||
});
|
||||
|
||||
test('setLanguage changes current language', () => {
|
||||
setLanguage('en');
|
||||
assert.strictEqual(getLanguage(), 'en');
|
||||
setLanguage('ja');
|
||||
assert.strictEqual(getLanguage(), 'ja');
|
||||
setLanguage('zh-CN');
|
||||
});
|
||||
|
||||
test('t() falls back to key when key does not exist', () => {
|
||||
setLanguage('zh-CN');
|
||||
const result = t('nonexistent.key');
|
||||
assert.strictEqual(result, 'nonexistent.key');
|
||||
});
|
||||
|
||||
test('t() returns zh-CN string in zh-CN language', () => {
|
||||
setLanguage('zh-CN');
|
||||
assert.strictEqual(t('review.noEditor'), '请先打开一个文件');
|
||||
});
|
||||
|
||||
test('t() returns English string in en language', () => {
|
||||
setLanguage('en');
|
||||
assert.strictEqual(t('review.noEditor'), 'Please open a file first');
|
||||
});
|
||||
|
||||
test('t() returns Japanese string in ja language', () => {
|
||||
setLanguage('ja');
|
||||
assert.strictEqual(t('review.noEditor'), '最初にファイルを開いてください');
|
||||
});
|
||||
|
||||
test('t() with template variables', () => {
|
||||
setLanguage('en');
|
||||
assert.strictEqual(
|
||||
t('export.saved', { 0: '/home/user/report.md' }),
|
||||
'Report saved to /home/user/report.md'
|
||||
);
|
||||
});
|
||||
|
||||
test('t() with multiple template variables', () => {
|
||||
setLanguage('zh-CN');
|
||||
assert.strictEqual(
|
||||
t('report.totalSummary', { 0: '10', 1: '3', 2: '5', 3: '2' }),
|
||||
'总计: 10 | 错误: 3 | 警告: 5 | 建议: 2'
|
||||
);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
setLanguage('zh-CN');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { ESLintAdapter } from '../adapters/eslint';
|
||||
import { mergeResults } from '../merger/merger';
|
||||
|
||||
suite('Pipeline Tests', () => {
|
||||
test('Full pipeline: linter check + merge', async () => {
|
||||
const adapter = new ESLintAdapter();
|
||||
if (!adapter.isAvailable()) { return; }
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument({
|
||||
content: 'var x = 1;\nvar y = 2;\n',
|
||||
language: 'javascript',
|
||||
});
|
||||
|
||||
const staticResult = await adapter.check(doc, __dirname);
|
||||
assert.ok(staticResult.status === 'ok');
|
||||
|
||||
const report = mergeResults({
|
||||
staticDiagnostics: staticResult.diagnostics,
|
||||
customRuleResults: [],
|
||||
translatedDiagnostics: [],
|
||||
aiFindings: [],
|
||||
errors: [],
|
||||
degraded: false,
|
||||
startTime: Date.now(),
|
||||
filePath: 'virtual-doc',
|
||||
language: 'javascript',
|
||||
adapterIds: ['eslint'],
|
||||
});
|
||||
|
||||
assert.ok(typeof report.duration === 'number');
|
||||
assert.ok(typeof report.linterCount === 'number');
|
||||
assert.strictEqual(report.language, 'javascript');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import * as assert from 'assert';
|
||||
import { filterForDocument, filterAndSummarize } from '../rules/rule-filter';
|
||||
import type { CustomRule } from '../types';
|
||||
|
||||
function mockDoc(languageId: string, fileName: string): { languageId: string; fileName: string } {
|
||||
return { languageId, fileName };
|
||||
}
|
||||
|
||||
const baseRules: CustomRule[] = [
|
||||
{ id: 'java-rule', severity: 'error', description: '', message: '', languages: ['java'] },
|
||||
{ id: 'js-rule', severity: 'error', description: '', message: '', languages: ['javascript'] },
|
||||
{ id: 'ts-rule', severity: 'error', description: '', message: '', languages: ['typescript'] },
|
||||
{ id: 'css-rule', severity: 'error', description: '', message: '', languages: ['css'] },
|
||||
{ id: 'universal', severity: 'warning', description: '', message: '' },
|
||||
{ id: 'no-css', severity: 'info', description: '', message: '', excludeLanguages: ['css'] },
|
||||
{ id: 'empty-langs', severity: 'info', description: '', message: '', languages: [] },
|
||||
];
|
||||
|
||||
suite('Rule Filter Tests', () => {
|
||||
|
||||
test('白名单命中', () => {
|
||||
const doc = mockDoc('java', '/test/Foo.java');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('java-rule'));
|
||||
assert.ok(ids.includes('universal'));
|
||||
});
|
||||
|
||||
test('白名单未命中', () => {
|
||||
const doc = mockDoc('java', '/test/Foo.java');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(!ids.includes('js-rule'));
|
||||
assert.ok(!ids.includes('css-rule'));
|
||||
});
|
||||
|
||||
test('白名单为空→全语言保留', () => {
|
||||
const doc = mockDoc('css', '/test/test.css');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('universal'));
|
||||
assert.ok(ids.includes('empty-langs'));
|
||||
});
|
||||
|
||||
test('白名单缺失→全语言保留', () => {
|
||||
const doc = mockDoc('css', '/test/test.css');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('universal'));
|
||||
});
|
||||
|
||||
test('黑名单命中→剔除', () => {
|
||||
const doc = mockDoc('css', '/test/test.css');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(!ids.includes('no-css'));
|
||||
});
|
||||
|
||||
test('黑名单未命中→保留', () => {
|
||||
const doc = mockDoc('java', '/test/Foo.java');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('no-css'));
|
||||
});
|
||||
|
||||
test('黑名单为空→保留', () => {
|
||||
const rules: CustomRule[] = [
|
||||
{ id: 'r1', severity: 'info', description: '', message: '', excludeLanguages: [] },
|
||||
];
|
||||
const doc = mockDoc('css', '/test/test.css');
|
||||
const result = filterForDocument(rules, doc as any);
|
||||
assert.strictEqual(result.length, 1);
|
||||
});
|
||||
|
||||
test('白名单+黑名单交集→黑名单胜出剔除', () => {
|
||||
const rules: CustomRule[] = [
|
||||
{ id: 'r1', severity: 'info', description: '', message: '', languages: ['java'], excludeLanguages: ['java'] },
|
||||
];
|
||||
const doc = mockDoc('java', '/test/Foo.java');
|
||||
const result = filterForDocument(rules, doc as any);
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('typescriptreact 别名→命中 typescript 规则', () => {
|
||||
const doc = mockDoc('typescriptreact', '/test/App.tsx');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('ts-rule'));
|
||||
});
|
||||
|
||||
test('plsql 同组→命中 sql 规则', () => {
|
||||
const rules: CustomRule[] = [
|
||||
{ id: 'sql-rule', severity: 'error', description: '', message: '', languages: ['sql'] },
|
||||
{ id: 'plsql-rule', severity: 'error', description: '', message: '', languages: ['plsql'] },
|
||||
];
|
||||
const doc = mockDoc('plsql', '/test/test.plsql');
|
||||
const result = filterForDocument(rules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('sql-rule'));
|
||||
assert.ok(ids.includes('plsql-rule'));
|
||||
});
|
||||
|
||||
test('JSP 并集→保留 java 规则', () => {
|
||||
const doc = mockDoc('html', '/test/test.jsp');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('java-rule'));
|
||||
assert.ok(ids.includes('js-rule'));
|
||||
assert.ok(ids.includes('ts-rule'));
|
||||
assert.ok(ids.includes('css-rule'));
|
||||
});
|
||||
|
||||
test('JSP 并集→保留 css 规则', () => {
|
||||
const doc = mockDoc('html', '/test/test.jspx');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(ids.includes('css-rule'));
|
||||
});
|
||||
|
||||
test('普通HTML不触发JSP→剔除java规则', () => {
|
||||
const doc = mockDoc('html', '/test/index.html');
|
||||
const result = filterForDocument(baseRules, doc as any);
|
||||
const ids = result.map(r => r.id);
|
||||
assert.ok(!ids.includes('java-rule'));
|
||||
});
|
||||
|
||||
test('全部过滤→skippedRequestA=true', () => {
|
||||
const rules: CustomRule[] = [
|
||||
{ id: 'java-rule', severity: 'error', description: '', message: '', languages: ['java'] },
|
||||
];
|
||||
const doc = mockDoc('css', '/test/test.css');
|
||||
const result = filterAndSummarize(rules, doc as any);
|
||||
assert.strictEqual(result.relevant.length, 0);
|
||||
assert.strictEqual(result.filteredOut.length, 1);
|
||||
assert.strictEqual(result.skippedRequestA, true);
|
||||
});
|
||||
|
||||
test('部分过滤→skippedRequestA=false', () => {
|
||||
const doc = mockDoc('java', '/test/Foo.java');
|
||||
const result = filterAndSummarize(baseRules, doc as any);
|
||||
assert.ok(result.relevant.length > 0);
|
||||
assert.ok(result.filteredOut.length > 0);
|
||||
assert.strictEqual(result.skippedRequestA, false);
|
||||
});
|
||||
});
|
||||
+170
-29
@@ -36,6 +36,7 @@
|
||||
|
||||
var _step1Done = false;
|
||||
var _step2Done = false;
|
||||
var _i18n = {};
|
||||
|
||||
function updateSteps(step3Done) {
|
||||
var steps = [_step1Done, _step2Done, step3Done];
|
||||
@@ -51,25 +52,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCommonRules() {
|
||||
var body = document.getElementById('commonRulesBody');
|
||||
var arrow = document.getElementById('commonRulesArrow');
|
||||
if (!body || !arrow) { return; }
|
||||
var isHidden = body.style.display === 'none';
|
||||
body.style.display = isHidden ? 'block' : 'none';
|
||||
arrow.classList.toggle('expanded', isHidden);
|
||||
}
|
||||
|
||||
function toggleCustomRules() {
|
||||
var body = document.getElementById('customRulesBody');
|
||||
var arrow = document.getElementById('customRulesArrow');
|
||||
if (!body || !arrow) { return; }
|
||||
var isHidden = body.style.display === 'none';
|
||||
body.style.display = isHidden ? 'block' : 'none';
|
||||
arrow.classList.toggle('expanded', isHidden);
|
||||
}
|
||||
|
||||
function toggleAIReview() {
|
||||
var body = document.getElementById('aiReviewBody');
|
||||
var arrow = document.getElementById('aiReviewArrow');
|
||||
if (!body || !arrow) { return; }
|
||||
var isHidden = body.style.display === 'none';
|
||||
body.style.display = isHidden ? 'block' : 'none';
|
||||
arrow.classList.toggle('expanded', isHidden);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = text;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function populateModelOptions(providerId, selectModel) {
|
||||
var modelSelect = document.getElementById('modelSelect');
|
||||
if (!modelSelect) { return; }
|
||||
var models = (PROVIDERS[providerId] && PROVIDERS[providerId].models) || [];
|
||||
modelSelect.innerHTML = '';
|
||||
for (var i = 0; i < models.length; i++) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = models[i];
|
||||
opt.textContent = models[i];
|
||||
modelSelect.appendChild(opt);
|
||||
}
|
||||
if (models.length > 0) {
|
||||
modelSelect.value = selectModel && models.indexOf(selectModel) !== -1 ? selectModel : models[0];
|
||||
function populateModelOptions(providerId, currentModel) {
|
||||
var input = document.getElementById('modelInput');
|
||||
if (!input) { return; }
|
||||
|
||||
if (!input.value) {
|
||||
var models = (PROVIDERS[providerId] && PROVIDERS[providerId].models) || [];
|
||||
if (models.length > 0) {
|
||||
input.value = models[0];
|
||||
postMsg('setModel', models[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +103,8 @@
|
||||
postMsg('setProvider', this.value);
|
||||
});
|
||||
|
||||
document.getElementById('modelSelect').addEventListener('change', function () {
|
||||
postMsg('setModel', this.value);
|
||||
document.getElementById('modelInput').addEventListener('change', function () {
|
||||
postMsg('setModel', this.value.trim());
|
||||
});
|
||||
|
||||
document.getElementById('baseUrlInput').addEventListener('change', function () {
|
||||
@@ -90,6 +115,7 @@
|
||||
var msg = event.data;
|
||||
|
||||
if (msg.type === 'initConfig') {
|
||||
_i18n = msg.i18n || {};
|
||||
var c = msg.config;
|
||||
document.getElementById('languageSelect').value = c.language || 'zh-CN';
|
||||
|
||||
@@ -113,50 +139,75 @@
|
||||
|
||||
if (c.provider && PROVIDERS[c.provider]) {
|
||||
populateModelOptions(c.provider, c.model);
|
||||
var mi = document.getElementById('modelInput');
|
||||
if (mi) { mi.value = c.model || ''; }
|
||||
var ps = document.getElementById('providerSelect');
|
||||
if (ps) { ps.value = c.provider; }
|
||||
}
|
||||
|
||||
var pb = document.getElementById('providerBadge');
|
||||
if (pb) {
|
||||
pb.textContent = c.provider && c.model ? '已配置' : '未配置';
|
||||
pb.textContent = c.provider && c.model ? msg.i18n.configured : msg.i18n.notConfigured;
|
||||
pb.className = 'badge ' + (c.provider && c.model ? 'badge-configured' : 'badge-unconfigured');
|
||||
}
|
||||
|
||||
var akb = document.getElementById('apiKeyBadge');
|
||||
akb.textContent = c.apiKeyConfigured && c.baseUrlConfigured ? '已配置' : '未配置';
|
||||
akb.textContent = c.apiKeyConfigured && c.baseUrlConfigured ? msg.i18n.configured : msg.i18n.notConfigured;
|
||||
akb.className = 'badge ' + (c.apiKeyConfigured && c.baseUrlConfigured ? 'badge-configured' : 'badge-unconfigured');
|
||||
|
||||
var connDone = msg.connectionTested && msg.connectionSuccess;
|
||||
var btnTest = document.getElementById('btnTest');
|
||||
if (connDone) {
|
||||
btnTest.innerHTML = '✓ 已连接';
|
||||
btnTest.innerHTML = msg.i18n.connected;
|
||||
} else if (msg.connectionTested && !msg.connectionSuccess) {
|
||||
btnTest.innerHTML = '✗ 重试';
|
||||
btnTest.innerHTML = msg.i18n.retry;
|
||||
} else {
|
||||
btnTest.innerHTML = '保存并测试连接';
|
||||
btnTest.innerHTML = msg.i18n.saveTest;
|
||||
}
|
||||
btnTest.disabled = false;
|
||||
|
||||
var ruleList = document.getElementById('ruleList');
|
||||
var countBadge = document.getElementById('ruleCountBadge');
|
||||
var ruleList = document.getElementById('ruleListInEngine');
|
||||
var customBadge = document.getElementById('customRuleCountBadge');
|
||||
if (customBadge) {
|
||||
customBadge.textContent = msg.ruleFiles ? String(msg.ruleFiles.length) : '0';
|
||||
}
|
||||
if (msg.ruleFiles && msg.ruleFiles.length > 0) {
|
||||
countBadge.textContent = msg.ruleFiles.length + ' 个文件';
|
||||
countBadge.className = 'badge badge-configured';
|
||||
ruleList.innerHTML = msg.ruleFiles.map(function (f) {
|
||||
return '<div class="rule-item">' +
|
||||
'<span class="rule-name">' + escapeHtml(f) + '</span>' +
|
||||
'<button class="rule-del" onclick="deleteFile(\'' + escapeHtml(f) + '\')">×</button></div>';
|
||||
}).join('');
|
||||
} else {
|
||||
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;">' + msg.i18n.noRuleFiles + '</div>';
|
||||
}
|
||||
|
||||
_step1Done = c.provider && c.model && c.apiKeyConfigured && c.baseUrlConfigured;
|
||||
_step2Done = msg.ruleFiles && msg.ruleFiles.length > 0;
|
||||
updateSteps(msg.connectionTested && msg.connectionSuccess);
|
||||
|
||||
var aiProviderDisplay = document.getElementById('aiProviderDisplay');
|
||||
var aiModelDisplay = document.getElementById('aiModelDisplay');
|
||||
var aiConnDisplay = document.getElementById('aiConnectionDisplay');
|
||||
var aiBadge = document.getElementById('aiReviewStatusBadge');
|
||||
if (aiProviderDisplay) {
|
||||
var p = PROVIDERS[c.provider];
|
||||
aiProviderDisplay.textContent = p ? p.name : c.provider;
|
||||
}
|
||||
if (aiModelDisplay) {
|
||||
aiModelDisplay.textContent = c.model || '';
|
||||
}
|
||||
if (aiConnDisplay) {
|
||||
var connected = msg.connectionTested && msg.connectionSuccess;
|
||||
aiConnDisplay.textContent = connected ? msg.i18n.connected : msg.i18n.notConnected;
|
||||
}
|
||||
if (aiBadge) {
|
||||
var connected = msg.connectionTested && msg.connectionSuccess;
|
||||
aiBadge.textContent = connected ? msg.i18n.connected : msg.i18n.notConnected;
|
||||
aiBadge.style.background = connected ? 'rgba(63,185,80,0.15)' : 'rgba(139,148,158,0.12)';
|
||||
aiBadge.style.color = connected ? '#3fb950' : 'var(--vscode-descriptionForeground)';
|
||||
}
|
||||
|
||||
renderAdapters(msg.adapterStatus);
|
||||
}
|
||||
|
||||
if (msg.type === 'testResult') {
|
||||
@@ -168,15 +219,105 @@
|
||||
var btnTest = document.getElementById('btnTest');
|
||||
btnTest.disabled = false;
|
||||
if (msg.success) {
|
||||
btnTest.innerHTML = '✓ 已连接';
|
||||
btnTest.innerHTML = _i18n.connected || '✓ Connected';
|
||||
} else {
|
||||
btnTest.innerHTML = '✗ 重试';
|
||||
btnTest.innerHTML = _i18n.retry || '✗ Retry';
|
||||
}
|
||||
|
||||
updateSteps(msg.success);
|
||||
}
|
||||
});
|
||||
|
||||
function renderAdapters(adapterStatus) {
|
||||
var container = document.getElementById('adapter-list');
|
||||
if (!container || !adapterStatus) { return; }
|
||||
|
||||
var i18n = _i18n;
|
||||
var modeBadgeMap = {
|
||||
builtin: { class: 'adapter-badge-info', text: i18n.modeBuiltin || '' },
|
||||
project: { class: 'adapter-badge-ok', text: i18n.modeProject || '' },
|
||||
global: { class: 'adapter-badge-warn', text: i18n.modeGlobal || '' },
|
||||
};
|
||||
|
||||
var html = adapterStatus.map(function(a) {
|
||||
var modeBadge = modeBadgeMap[a.configMode];
|
||||
var depBadge = a.dependencyStatus === 'none' ? '' :
|
||||
a.dependencyStatus === 'ready'
|
||||
? '<span class="adapter-badge adapter-badge-ok">' + a.dependencyLabel + ' ✓</span>'
|
||||
: '<span class="adapter-badge adapter-badge-error">' + a.dependencyLabel + ' ✗</span>';
|
||||
var configBadge = a.configured
|
||||
? '<span class="adapter-badge adapter-badge-ok">' + i18n.configYes + '</span>'
|
||||
: '<span class="adapter-badge adapter-badge-warn">' + i18n.configNo + '</span>';
|
||||
var toggleTooltip = a.enabled
|
||||
? i18n.toggleDisable.replace('{0}', a.name)
|
||||
: i18n.toggleEnable.replace('{0}', a.name);
|
||||
|
||||
return '<div class="adapter-card' + (a.enabled ? '' : ' disabled') + '">' +
|
||||
'<div class="adapter-card-header">' +
|
||||
'<span class="adapter-card-name">' + a.name + '</span>' +
|
||||
'<div class="adapter-toggle' + (a.enabled ? '' : ' off') + '" data-adapter-id="' + a.id + '" data-tooltip="' + toggleTooltip + '"></div>' +
|
||||
'</div>' +
|
||||
'<div class="adapter-badges">' +
|
||||
'<span class="adapter-badge ' + modeBadge.class + '">' + modeBadge.text + '</span>' +
|
||||
depBadge +
|
||||
configBadge +
|
||||
'</div>' +
|
||||
'<div class="adapter-languages"><span class="adapter-lang-label">' + i18n.langLabel + '</span>' + escapeHtml(a.languages) + '</div>' +
|
||||
'<div class="adapter-guide">' + a.guideText + '</div>' +
|
||||
'<div class="adapter-actions">' +
|
||||
'<button class="adapter-btn" data-action="openAdapterConfig" data-adapter-id="' + a.id + '" data-tooltip="' + i18n.tooltipCreate.replace('{0}', a.projectConfigFileName) + '">' + i18n.btnCreateConfig + '</button>' +
|
||||
'<button class="adapter-btn" data-action="openSettings" data-settings-target="' + a.settingsTarget + '" data-tooltip="' + i18n.tooltipEdit + '">' + i18n.btnEditGlobal + '</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
container.querySelectorAll('.adapter-toggle').forEach(function(el) {
|
||||
el.addEventListener('click', function() {
|
||||
var adapterId = el.dataset.adapterId;
|
||||
var isEnabled = !el.classList.contains('off');
|
||||
vscode.postMessage({
|
||||
type: 'toggleAdapter',
|
||||
adapterId: adapterId,
|
||||
enabled: !isEnabled,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.adapter-btn').forEach(function(el) {
|
||||
el.addEventListener('click', function() {
|
||||
var action = el.dataset.action;
|
||||
var adapterId = el.dataset.adapterId;
|
||||
var settingsTarget = el.dataset.settingsTarget;
|
||||
if (action === 'openAdapterConfig') {
|
||||
vscode.postMessage({ type: 'openAdapterConfig', adapterId: adapterId });
|
||||
} else if (action === 'openSettings') {
|
||||
vscode.postMessage({ type: 'openSettings', settingsTarget: settingsTarget });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var enabledCount = adapterStatus.filter(function(a) { return a.enabled; }).length;
|
||||
var badge = document.getElementById('adapterCountBadge');
|
||||
if (badge) { badge.textContent = enabledCount + '/4'; }
|
||||
}
|
||||
|
||||
var tab = document.getElementById('tabCommonRules');
|
||||
if (tab) {
|
||||
tab.addEventListener('click', toggleCommonRules);
|
||||
}
|
||||
|
||||
var customTab = document.getElementById('tabCustomRules');
|
||||
if (customTab) {
|
||||
customTab.addEventListener('click', toggleCustomRules);
|
||||
}
|
||||
|
||||
var aiTab = document.getElementById('tabAIReview');
|
||||
if (aiTab) {
|
||||
aiTab.addEventListener('click', toggleAIReview);
|
||||
}
|
||||
|
||||
vscode.postMessage({ type: 'ready' });
|
||||
|
||||
window.postMsg = postMsg;
|
||||
|
||||
+580
-72
@@ -1,9 +1,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { getAIProvider, getAIModel, getAIOutputLanguage, getAIConfig } from '../config/ai';
|
||||
import { getApiKey, setApiKey } from '../config/secret';
|
||||
import { createProvider, getAllProviderMeta, getProviderModels } from '../ai/factory';
|
||||
import { createProvider, getAllProviderMeta, getProviderModels, invalidateProviderCache } from '../ai/factory';
|
||||
import { listRuleFiles } from '../rules/yaml-parser';
|
||||
import { ImportService } from '../rules/import-service';
|
||||
import { showImportPreview } from '../rules/import-preview';
|
||||
@@ -14,6 +15,128 @@ import { ExcelConverter } from '../rules/converters/excel-converter';
|
||||
import { DocxConverter } from '../rules/converters/docx-converter';
|
||||
import { PptxConverter } from '../rules/converters/pptx-converter';
|
||||
import { t, onLanguageChange } from '../i18n/messages';
|
||||
import { getEslintConfigPath, getStylelintConfigPath, getPMDRulesetPath, getSqlLintConfigFile, isAdapterEnabled, setAdapterEnabled } from '../config/linter';
|
||||
|
||||
type ConfigMode = 'builtin' | 'project' | 'global';
|
||||
|
||||
type DependencyStatus = 'ready' | 'missing' | 'none';
|
||||
|
||||
interface AdapterConfigStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
configMode: ConfigMode;
|
||||
dependencyStatus: DependencyStatus;
|
||||
dependencyLabel?: string;
|
||||
configured: boolean;
|
||||
guideText: string;
|
||||
languages: string;
|
||||
projectConfigFileName: string;
|
||||
settingsTarget: string;
|
||||
}
|
||||
|
||||
function getPmdRulesetTemplate(): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||
<description>Custom PMD Ruleset</description>
|
||||
<!-- ${t('setup.template.pmdBestPractices')} -->
|
||||
<rule ref="category/java/bestpractices.xml" />
|
||||
<!-- ${t('setup.template.pmdCodeStyle')} -->
|
||||
<rule ref="category/java/codestyle.xml" />
|
||||
</ruleset>`;
|
||||
}
|
||||
|
||||
function getSqlfluffTemplate(): string {
|
||||
return `[sqlfluff]
|
||||
# ${t('setup.template.sqlfluffDialect')}
|
||||
dialect = postgres
|
||||
# ${t('setup.template.sqlfluffRules')}
|
||||
rules = all`;
|
||||
}
|
||||
|
||||
function getEslintTemplate(): string {
|
||||
return `module.exports = {
|
||||
root: true,
|
||||
env: { node: true, es2022: true },
|
||||
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
||||
rules: {
|
||||
'no-unused-vars': 'warn', // ${t('setup.template.eslintComment1')}
|
||||
'no-console': 'off', // ${t('setup.template.eslintComment2')}
|
||||
'semi': ['error', 'always'], // ${t('setup.template.eslintComment3')}
|
||||
},
|
||||
};`;
|
||||
}
|
||||
|
||||
function getStylelintTemplate(): string {
|
||||
return `module.exports = {
|
||||
extends: 'stylelint-config-standard',
|
||||
rules: {
|
||||
'indentation': 2, // ${t('setup.template.stylelintComment1')}
|
||||
'no-empty': true, // ${t('setup.template.stylelintComment2')}
|
||||
},
|
||||
};`;
|
||||
}
|
||||
|
||||
const ADAPTER_METADATA: Record<string, {
|
||||
name: string;
|
||||
projectConfigFileName: string;
|
||||
settingsTarget: string;
|
||||
hasExternalDependency: boolean;
|
||||
dependencyLabel?: string;
|
||||
configFileTemplate: () => string;
|
||||
i18nKey: string;
|
||||
}> = {
|
||||
pmd: {
|
||||
name: 'PMD',
|
||||
projectConfigFileName: 'ruleset.xml',
|
||||
settingsTarget: 'vscode-code-reviewer.pmd',
|
||||
hasExternalDependency: true,
|
||||
dependencyLabel: 'Java',
|
||||
configFileTemplate: getPmdRulesetTemplate,
|
||||
i18nKey: 'pmd',
|
||||
},
|
||||
'sql-lint': {
|
||||
name: 'SQL-Lint',
|
||||
projectConfigFileName: '.sqlfluff',
|
||||
settingsTarget: 'vscode-code-reviewer.sql-lint',
|
||||
hasExternalDependency: true,
|
||||
dependencyLabel: 'Python + sqlfluff',
|
||||
configFileTemplate: getSqlfluffTemplate,
|
||||
i18nKey: 'sql',
|
||||
},
|
||||
eslint: {
|
||||
name: 'ESLint',
|
||||
projectConfigFileName: '.eslintrc.js',
|
||||
settingsTarget: 'vscode-code-reviewer.linters',
|
||||
hasExternalDependency: false,
|
||||
configFileTemplate: getEslintTemplate,
|
||||
i18nKey: 'eslint',
|
||||
},
|
||||
stylelint: {
|
||||
name: 'Stylelint',
|
||||
projectConfigFileName: '.stylelintrc.js',
|
||||
settingsTarget: 'vscode-code-reviewer.linters',
|
||||
hasExternalDependency: false,
|
||||
configFileTemplate: getStylelintTemplate,
|
||||
i18nKey: 'stylelint',
|
||||
},
|
||||
};
|
||||
|
||||
const PROJECT_CONFIG_FILES: Record<string, string[]> = {
|
||||
eslint: ['.eslintrc.js', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml', '.eslintrc', 'eslint.config.js', 'eslint.config.mjs'],
|
||||
stylelint: ['.stylelintrc.js', '.stylelintrc.json', '.stylelintrc.yaml', '.stylelintrc.yml', '.stylelintrc', 'stylelint.config.js'],
|
||||
pmd: ['ruleset.xml'],
|
||||
'sql-lint': ['.sqlfluff'],
|
||||
};
|
||||
|
||||
const GLOBAL_CONFIG_GETTERS: Record<string, () => string> = {
|
||||
eslint: getEslintConfigPath,
|
||||
stylelint: getStylelintConfigPath,
|
||||
pmd: getPMDRulesetPath,
|
||||
'sql-lint': getSqlLintConfigFile,
|
||||
};
|
||||
|
||||
export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
private _view?: vscode.WebviewView;
|
||||
@@ -25,6 +148,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
private _scriptUri: vscode.Uri | null = null;
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
this.restoreConnectionState();
|
||||
this.importService.registerConverter(new YamlConverter());
|
||||
this.importService.registerConverter(new MdConverter());
|
||||
this.importService.registerConverter(new TxtConverter());
|
||||
@@ -33,6 +157,36 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
this.importService.registerConverter(new PptxConverter());
|
||||
}
|
||||
|
||||
private async getConfigFingerprint(): Promise<string> {
|
||||
const cfg = getAIConfig();
|
||||
const hasApiKey = await isApiKeyConfigured(this.context);
|
||||
const hasBaseUrl = isBaseUrlConfigured();
|
||||
return `${cfg.provider}|${cfg.model}|${hasApiKey}|${hasBaseUrl}`;
|
||||
}
|
||||
|
||||
private restoreConnectionState(): void {
|
||||
const saved = this.context.globalState.get<{ tested: boolean; success: boolean; fingerprint: string }>('connectionState');
|
||||
if (saved) {
|
||||
this.connectionTested = saved.tested;
|
||||
this.connectionSuccess = saved.success;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveConnectionState(): Promise<void> {
|
||||
const fingerprint = await this.getConfigFingerprint();
|
||||
await this.context.globalState.update('connectionState', {
|
||||
tested: this.connectionTested,
|
||||
success: this.connectionSuccess,
|
||||
fingerprint,
|
||||
});
|
||||
}
|
||||
|
||||
private async clearConnectionState(): Promise<void> {
|
||||
this.connectionTested = false;
|
||||
this.connectionSuccess = false;
|
||||
await this.context.globalState.update('connectionState', undefined);
|
||||
}
|
||||
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
_context: vscode.WebviewViewResolveContext,
|
||||
@@ -46,7 +200,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
};
|
||||
|
||||
const config = getAIConfig();
|
||||
const providers = getAllProviderMeta();
|
||||
const providers = getAllProviderMeta(this.context.extensionUri);
|
||||
const scriptUri = webviewView.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this.context.extensionUri, 'out', 'webview', 'setupView.js')
|
||||
);
|
||||
@@ -54,6 +208,13 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
this._providers = providers;
|
||||
this._config = aiConfig;
|
||||
this._scriptUri = scriptUri;
|
||||
this.getConfigFingerprint().then(fingerprint => {
|
||||
const saved = this.context.globalState.get<{ fingerprint: string }>('connectionState');
|
||||
if (saved && saved.fingerprint !== fingerprint) {
|
||||
this.clearConnectionState();
|
||||
this.pushConfig();
|
||||
}
|
||||
});
|
||||
webviewView.webview.html = this.getHtml(providers, aiConfig, scriptUri);
|
||||
|
||||
const langDisposable = onLanguageChange(() => {
|
||||
@@ -75,25 +236,29 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
break;
|
||||
case 'setApiKey':
|
||||
await setApiKey(this.context, msg.value);
|
||||
await this.clearConnectionState();
|
||||
await this.pushConfig();
|
||||
break;
|
||||
case 'setProvider': {
|
||||
const cfg = vscode.workspace.getConfiguration('vscode-code-reviewer');
|
||||
await cfg.update('ai.provider', msg.value, vscode.ConfigurationTarget.Global);
|
||||
const models = getProviderModels(msg.value);
|
||||
const models = getProviderModels(this.context.extensionUri, msg.value);
|
||||
if (models.length > 0) {
|
||||
await cfg.update('ai.model', models[0], vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
await this.clearConnectionState();
|
||||
await this.pushConfig();
|
||||
break;
|
||||
}
|
||||
case 'setModel':
|
||||
await vscode.workspace.getConfiguration('vscode-code-reviewer').update('ai.model', msg.value, vscode.ConfigurationTarget.Global);
|
||||
await this.clearConnectionState();
|
||||
await this.pushConfig();
|
||||
break;
|
||||
case 'setBaseUrl':
|
||||
await vscode.workspace.getConfiguration('vscode-code-reviewer')
|
||||
.update('ai.baseUrl', msg.value || undefined, vscode.ConfigurationTarget.Global);
|
||||
await this.clearConnectionState();
|
||||
await this.pushConfig();
|
||||
break;
|
||||
case 'setLanguage':
|
||||
@@ -115,8 +280,51 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
await this.resetConfig();
|
||||
await this.pushConfig();
|
||||
break;
|
||||
case 'openAdapterConfig':
|
||||
await this.handleAdapterConfig(msg.adapterId);
|
||||
await this.pushConfig();
|
||||
break;
|
||||
case 'openSettings':
|
||||
await vscode.commands.executeCommand(
|
||||
'workbench.action.openSettings',
|
||||
msg.settingsTarget
|
||||
);
|
||||
break;
|
||||
case 'toggleAdapter':
|
||||
await setAdapterEnabled(msg.adapterId, msg.enabled);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const configChangeDisposable = vscode.workspace.onDidChangeConfiguration((e) => {
|
||||
if (
|
||||
e.affectsConfiguration('vscode-code-reviewer.linter') ||
|
||||
e.affectsConfiguration('vscode-code-reviewer.linters') ||
|
||||
e.affectsConfiguration('vscode-code-reviewer.pmd') ||
|
||||
e.affectsConfiguration('vscode-code-reviewer.sql-lint')
|
||||
) {
|
||||
this.pushConfig();
|
||||
}
|
||||
});
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
||||
'**/.code-review/providers.json'
|
||||
);
|
||||
|
||||
watcher.onDidChange(() => {
|
||||
invalidateProviderCache();
|
||||
this.pushConfig();
|
||||
});
|
||||
|
||||
watcher.onDidCreate(() => {
|
||||
invalidateProviderCache();
|
||||
this.pushConfig();
|
||||
});
|
||||
|
||||
webviewView.onDidDispose(() => {
|
||||
configChangeDisposable.dispose();
|
||||
watcher.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private async pushConfig(): Promise<void> {
|
||||
@@ -139,10 +347,33 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
language: config.outputLanguage,
|
||||
apiKeyConfigured,
|
||||
},
|
||||
providers: getAllProviderMeta(),
|
||||
providers: getAllProviderMeta(this.context.extensionUri),
|
||||
ruleFiles,
|
||||
connectionTested: this.connectionTested,
|
||||
connectionSuccess: this.connectionSuccess,
|
||||
adapterStatus: this.collectAdapterStatus(),
|
||||
i18n: {
|
||||
configured: t('setup.configured'),
|
||||
notConfigured: t('setup.notConfigured'),
|
||||
connected: t('setup.connected'),
|
||||
notConnected: t('setup.notConnected'),
|
||||
retry: t('setup.retry'),
|
||||
saveTest: t('setup.saveAndTest'),
|
||||
ruleCountFormat: t('setup.ruleCountFormat'),
|
||||
noRuleFiles: t('setup.noRuleFiles'),
|
||||
modeBuiltin: t('setup.adapter.modeBuiltin'),
|
||||
modeProject: t('setup.adapter.modeProject'),
|
||||
modeGlobal: t('setup.adapter.modeGlobal'),
|
||||
configYes: t('setup.adapter.configYes'),
|
||||
configNo: t('setup.adapter.configNo'),
|
||||
langLabel: t('setup.adapter.langLabel'),
|
||||
btnCreateConfig: t('setup.adapter.btnCreateConfig'),
|
||||
btnEditGlobal: t('setup.adapter.btnEditGlobal'),
|
||||
tooltipCreate: t('setup.adapter.tooltipCreate'),
|
||||
tooltipEdit: t('setup.adapter.tooltipEdit'),
|
||||
toggleEnable: t('setup.adapter.toggleEnable'),
|
||||
toggleDisable: t('setup.adapter.toggleDisable'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,7 +392,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
const config = getAIConfig();
|
||||
|
||||
try {
|
||||
const provider = createProvider(config.provider, apiKey, config.baseUrl);
|
||||
const provider = createProvider(config.provider, apiKey, config.baseUrl, this.context.extensionUri);
|
||||
await provider.chat('回复 ok', 'ping', {
|
||||
model: config.model,
|
||||
temperature: 0,
|
||||
@@ -170,10 +401,12 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
this.connectionTested = true;
|
||||
this.connectionSuccess = true;
|
||||
await this.saveConnectionState();
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: true, message: t('setup.testSuccess') });
|
||||
} catch (err) {
|
||||
this.connectionTested = true;
|
||||
this.connectionSuccess = false;
|
||||
await this.saveConnectionState();
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this._view?.webview.postMessage({ type: 'testResult', success: false, message: t('setup.testFail', { 0: message }) });
|
||||
}
|
||||
@@ -253,8 +486,7 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {
|
||||
await config.update('ai.model', undefined, vscode.ConfigurationTarget.Global);
|
||||
await config.update('ai.outputLanguage', undefined, vscode.ConfigurationTarget.Global);
|
||||
await this.deleteApiKey();
|
||||
this.connectionTested = false;
|
||||
this.connectionSuccess = false;
|
||||
await this.clearConnectionState();
|
||||
}
|
||||
|
||||
private async deleteApiKey(): Promise<void> {
|
||||
@@ -290,6 +522,13 @@ body {
|
||||
padding: 10px 0 14px; font-size: 15px; font-weight: 600; color: var(--vscode-foreground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border); margin-bottom: 12px;
|
||||
}
|
||||
.panel-header-title {
|
||||
display: flex; align-items: center; gap: 8px; flex: 1;
|
||||
}
|
||||
.panel-header .lang-select {
|
||||
width: auto; min-width: 100px; padding: 2px 24px 2px 8px;
|
||||
font-size: 11px; min-height: 24px; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.section { margin-bottom: 16px; }
|
||||
@@ -472,17 +711,151 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
border-top-color: #fff; border-radius: 50%;
|
||||
animation: spin .6s linear infinite;
|
||||
}
|
||||
|
||||
/* Adapter cards */
|
||||
.adapter-card {
|
||||
background: var(--vscode-sideBar-background, var(--vscode-editor-background));
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.adapter-card.disabled { opacity: 0.45; }
|
||||
.adapter-card-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.adapter-card-name { font-size: 12px; font-weight: 600; color: var(--vscode-foreground); }
|
||||
.adapter-toggle {
|
||||
width: 30px; height: 16px; border-radius: 8px;
|
||||
background: #3fb950; position: relative; cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.adapter-toggle.off { background: #3c3c3c; }
|
||||
.adapter-toggle::after {
|
||||
content: ''; position: absolute; top: 2px; left: 2px;
|
||||
width: 12px; height: 12px; border-radius: 50%; background: #fff;
|
||||
transition: transform 0.2s;
|
||||
transform: translateX(14px);
|
||||
}
|
||||
.adapter-toggle.off::after { transform: translateX(0); background: #ccc; }
|
||||
.adapter-badges { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 5px; }
|
||||
.adapter-badge {
|
||||
display: inline-flex; align-items: center; padding: 1px 5px;
|
||||
border-radius: 3px; font-size: 9px; font-weight: 600;
|
||||
}
|
||||
.adapter-badge-info { background: rgba(139,92,246,0.15); color: #8b5cf6; }
|
||||
.adapter-badge-ok { background: rgba(63,185,80,0.15); color: #3fb950; }
|
||||
.adapter-badge-warn { background: rgba(210,153,34,0.15); color: #d29922; }
|
||||
.adapter-badge-error { background: rgba(248,81,73,0.15); color: #f48771; }
|
||||
.adapter-languages { font-size: 10px; color: var(--vscode-descriptionForeground); margin-bottom: 4px; }
|
||||
.adapter-lang-label { color: var(--vscode-descriptionForeground); }
|
||||
.adapter-guide { font-size: 10px; color: var(--vscode-descriptionForeground); line-height: 1.4; margin-bottom: 7px; }
|
||||
.adapter-actions { display: flex; gap: 5px; }
|
||||
.adapter-btn {
|
||||
padding: 2px 8px; border-radius: 3px; font-size: 10px;
|
||||
border: 1px solid var(--vscode-panel-border); background: transparent;
|
||||
color: var(--vscode-foreground); cursor: pointer;
|
||||
}
|
||||
.adapter-btn:hover { background: var(--vscode-panel-border); }
|
||||
|
||||
/* Clickable engine tab */
|
||||
.engine-tab-top {
|
||||
display: flex; justify-content: space-between; align-items: flex-start; width: 100%;
|
||||
}
|
||||
.engine-tab.clickable { cursor: pointer; user-select: none; position: relative; }
|
||||
.engine-tab.clickable:hover { border-color: var(--vscode-focusBorder); }
|
||||
[data-tooltip] { position: relative; }
|
||||
[data-tooltip]:hover::before {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute; bottom: calc(100% + 14px); left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--vscode-editorWidget-background, var(--vscode-editor-background));
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border));
|
||||
border-radius: 4px; padding: 4px 8px;
|
||||
font-size: 12px; white-space: nowrap;
|
||||
z-index: 200; pointer-events: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.adapter-toggle[data-tooltip]:hover::before {
|
||||
top: 50%; right: calc(100% + 8px);
|
||||
bottom: auto; left: auto; transform: translateY(-50%);
|
||||
}
|
||||
.adapter-btn[data-action="openAdapterConfig"][data-tooltip]:hover::before {
|
||||
top: 50%; left: calc(100% + 8px);
|
||||
bottom: auto; right: auto; transform: translateY(-50%);
|
||||
}
|
||||
.engine-indicator {
|
||||
font-size: 9px; color: var(--vscode-descriptionForeground);
|
||||
transition: transform .15s; flex-shrink: 0;
|
||||
}
|
||||
.engine-indicator.expanded { transform: rotate(90deg); }
|
||||
.engine-badge {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: 1px 6px; border-radius: 8px;
|
||||
background: rgba(139,92,246,0.15); color: #8b5cf6;
|
||||
font-size: 10px; font-weight: 600;
|
||||
white-space: nowrap; flex-shrink: 0;
|
||||
}
|
||||
.engine-tab-footer {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
justify-content: flex-end; width: 100%; margin-top: 6px;
|
||||
}
|
||||
.engine-common-rules-body {
|
||||
margin-top: 8px; padding: 0 2px;
|
||||
}
|
||||
.engine-custom-rules-body,
|
||||
.engine-ai-review-body {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.ai-review-status-grid {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.status-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
.status-label { color: var(--vscode-descriptionForeground); }
|
||||
.status-value { color: var(--vscode-foreground); font-weight: 500; }
|
||||
|
||||
.engine-subtitle {
|
||||
font-size: 11px; font-weight: 600;
|
||||
color: var(--vscode-descriptionForeground); margin-bottom: 8px;
|
||||
}
|
||||
.mode-legend {
|
||||
display: flex; flex-wrap: wrap; gap: 6px 12px;
|
||||
margin-bottom: 10px; padding: 6px 8px;
|
||||
background: var(--vscode-sideBar-background, var(--vscode-editor-background));
|
||||
border: 1px solid var(--vscode-panel-border); border-radius: 6px;
|
||||
font-size: 10px; color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.mode-legend-item { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.mode-legend-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||||
.mode-legend-desc { width: 100%; font-size: 10px; opacity: 0.75; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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 class="panel-header-title">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
<select class="lang-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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 1. 快速开始 -->
|
||||
@@ -513,37 +886,9 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 审核引擎 -->
|
||||
<!-- 2. AI 连接配置 -->
|
||||
<div class="section">
|
||||
<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">${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">${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">${t('setup.aiReview')}</div>
|
||||
<div class="engine-desc">${t('setup.deepReview')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. AI 模型配置 -->
|
||||
<div class="section">
|
||||
<div class="section-title">${t('setup.aiConfig')}</div>
|
||||
<div class="section-title">${t('setup.aiConnectionConfig')}</div>
|
||||
<div class="card">
|
||||
<div class="card-row">
|
||||
<span class="card-label">${t('setup.provider')}</span>
|
||||
@@ -556,18 +901,13 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
</div>
|
||||
<div class="field">
|
||||
<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>
|
||||
<input type="text" id="modelInput"
|
||||
value="${config.model || ''}"
|
||||
placeholder="${t('setup.modelPlaceholder')}"
|
||||
onchange="postMsg('setModel', this.value)">
|
||||
</div>
|
||||
<div class="field-hint">${t('setup.modelHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. API Key -->
|
||||
<div class="section">
|
||||
<div class="section-title">API Key</div>
|
||||
<div class="card">
|
||||
<div style="border-top:1px solid var(--vscode-panel-border);margin:10px -12px;"></div>
|
||||
<div class="card-row">
|
||||
<span class="card-label">${t('setup.apiKey')}</span>
|
||||
<span class="badge" id="apiKeyBadge">${t('setup.notConfigured')}</span>
|
||||
@@ -583,28 +923,65 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 输出语言 -->
|
||||
<!-- 3. 审核引擎 -->
|
||||
<div class="section">
|
||||
<div class="section-title">${t('setup.outputLang')}</div>
|
||||
<div class="field">
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. 自定义规则 -->
|
||||
<div class="section">
|
||||
<div class="section-title">${t('setup.customRulesSection')}</div>
|
||||
<div class="card">
|
||||
<div class="card-row">
|
||||
<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 class="section-title">${t('setup.engineSection')}</div>
|
||||
<div class="engines">
|
||||
<div class="engine-tab clickable" id="tabCommonRules" data-tooltip="${t('setup.adapter.tooltipTab')}">
|
||||
<div class="engine-tab-top">
|
||||
<span class="engine-dot dot-purple"></span>
|
||||
<span class="engine-badge" id="adapterCountBadge">0/4</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="engine-label">${t('setup.commonRules')}</div>
|
||||
<div class="engine-desc">${t('setup.linterStatic')}</div>
|
||||
</div>
|
||||
<div class="engine-tab-footer">
|
||||
<span class="engine-indicator" id="commonRulesArrow">▶</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ruleList"></div>
|
||||
<div class="engine-tab clickable" id="tabCustomRules" data-tooltip="${t('setup.adapter.tooltipTab')}">
|
||||
<div class="engine-tab-top">
|
||||
<span class="engine-dot dot-amber"></span>
|
||||
<span class="engine-badge" id="customRuleCountBadge" style="background:rgba(210,153,34,0.15);color:#d29922;">0</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="engine-label">${t('setup.customRules')}</div>
|
||||
<div class="engine-desc">${t('setup.teamCoding')}</div>
|
||||
</div>
|
||||
<div class="engine-tab-footer">
|
||||
<span class="engine-indicator" id="customRulesArrow">▶</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="engine-tab clickable" id="tabAIReview" data-tooltip="${t('setup.adapter.tooltipTab')}">
|
||||
<div class="engine-tab-top">
|
||||
<span class="engine-dot dot-green"></span>
|
||||
<span class="engine-badge" id="aiReviewStatusBadge" style="background:rgba(139,148,158,0.12);color:var(--vscode-descriptionForeground);">${t('setup.notConfigured')}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="engine-label">${t('setup.aiReview')}</div>
|
||||
<div class="engine-desc">${t('setup.deepReview')}</div>
|
||||
</div>
|
||||
<div class="engine-tab-footer">
|
||||
<span class="engine-indicator" id="aiReviewArrow">▶</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="engine-common-rules-body" id="commonRulesBody" style="display:none;">
|
||||
<div class="engine-subtitle">${t('setup.adapter.subtitle')}</div>
|
||||
<div class="mode-legend">
|
||||
<span class="mode-legend-item"><span class="mode-legend-dot" style="background:#8b5cf6;"></span>${t('setup.adapter.modeBuiltin')}</span>
|
||||
<span class="mode-legend-item"><span class="mode-legend-dot" style="background:#3fb950;"></span>${t('setup.adapter.modeProject')}</span>
|
||||
<span class="mode-legend-item"><span class="mode-legend-dot" style="background:#d29922;"></span>${t('setup.adapter.modeGlobal')}</span>
|
||||
<span class="mode-legend-desc">${t('setup.adapter.modeLegend')}</span>
|
||||
</div>
|
||||
<div id="adapter-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="engine-custom-rules-body" id="customRulesBody" style="display:none;">
|
||||
<div class="engine-subtitle">${t('setup.ruleList')}</div>
|
||||
<div id="ruleListInEngine"></div>
|
||||
<div class="field" style="margin-top:8px;">
|
||||
<div class="input-group">
|
||||
<input type="text" id="newRuleInput" placeholder="${t('setup.ruleNamePlaceholder')}">
|
||||
@@ -614,6 +991,27 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
<div class="field-hint">${t('setup.ruleNameHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="engine-ai-review-body" id="aiReviewBody" style="display:none;">
|
||||
<div class="ai-review-status-grid">
|
||||
<div class="status-row">
|
||||
<span class="status-label">${t('setup.provider')}</span>
|
||||
<span class="status-value" id="aiProviderDisplay"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">${t('setup.model')}</span>
|
||||
<span class="status-value" id="aiModelDisplay"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">连接状态</span>
|
||||
<span class="status-value" id="aiConnectionDisplay"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">${t('setup.aiReviewStatusCapability')}</span>
|
||||
<span class="status-value">${t('setup.aiReviewCapability')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
@@ -636,6 +1034,116 @@ input::placeholder { color: var(--vscode-input-placeholderForeground, var(--vsco
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
private detectConfigMode(adapterId: string): ConfigMode {
|
||||
const globalPath = GLOBAL_CONFIG_GETTERS[adapterId]?.();
|
||||
if (globalPath && globalPath.trim() !== '') {
|
||||
return 'global';
|
||||
}
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
const rootPath = workspaceFolders[0].uri.fsPath;
|
||||
const configFiles = PROJECT_CONFIG_FILES[adapterId] ?? [];
|
||||
for (const fileName of configFiles) {
|
||||
const filePath = path.join(rootPath, fileName);
|
||||
if (fs.existsSync(filePath)) {
|
||||
return 'project';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'builtin';
|
||||
}
|
||||
|
||||
private checkJavaReady(): boolean {
|
||||
try {
|
||||
const result = execSync('java -version 2>&1', {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
});
|
||||
return result.includes('version');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private checkPythonReady(): boolean {
|
||||
let pythonCmd = '';
|
||||
for (const cmd of ['python3', 'python']) {
|
||||
try {
|
||||
execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
||||
pythonCmd = cmd;
|
||||
break;
|
||||
} catch { continue; }
|
||||
}
|
||||
if (!pythonCmd) { return false; }
|
||||
|
||||
try {
|
||||
execSync('sqlfluff --version', { encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private collectAdapterStatus(): AdapterConfigStatus[] {
|
||||
const statuses: AdapterConfigStatus[] = [];
|
||||
|
||||
for (const [id, meta] of Object.entries(ADAPTER_METADATA)) {
|
||||
const configMode = this.detectConfigMode(id);
|
||||
const enabled = isAdapterEnabled(id);
|
||||
|
||||
let dependencyStatus: DependencyStatus = 'none';
|
||||
if (meta.hasExternalDependency) {
|
||||
if (id === 'pmd') {
|
||||
dependencyStatus = this.checkJavaReady() ? 'ready' : 'missing';
|
||||
} else if (id === 'sql-lint') {
|
||||
dependencyStatus = this.checkPythonReady() ? 'ready' : 'missing';
|
||||
}
|
||||
}
|
||||
|
||||
const configured = !meta.hasExternalDependency || configMode !== 'builtin' || dependencyStatus === 'ready';
|
||||
|
||||
statuses.push({
|
||||
id,
|
||||
name: meta.name,
|
||||
enabled,
|
||||
configMode,
|
||||
dependencyStatus,
|
||||
dependencyLabel: meta.dependencyLabel,
|
||||
configured,
|
||||
guideText: t(`setup.adapter.${meta.i18nKey}Guide`),
|
||||
languages: t(`setup.adapter.${meta.i18nKey}Languages`),
|
||||
projectConfigFileName: meta.projectConfigFileName,
|
||||
settingsTarget: meta.settingsTarget,
|
||||
});
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
|
||||
private async handleAdapterConfig(adapterId: string): Promise<void> {
|
||||
const meta = ADAPTER_METADATA[adapterId];
|
||||
if (!meta) { return; }
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
vscode.window.showWarningMessage('请先打开一个工作区文件夹');
|
||||
return;
|
||||
}
|
||||
|
||||
const rootPath = workspaceFolders[0].uri.fsPath;
|
||||
const filePath = path.join(rootPath, meta.projectConfigFileName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, meta.configFileTemplate(), 'utf-8');
|
||||
vscode.window.showInformationMessage(`配置文件已创建: ${meta.projectConfigFileName}`);
|
||||
}
|
||||
|
||||
const doc = await vscode.workspace.openTextDocument(filePath);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
}
|
||||
|
||||
async function isApiKeyConfigured(context: vscode.ExtensionContext): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user