12 KiB
12 KiB
Step 16 — Phase 5.2: 设置面板
依赖: Step 15
参考设计: §13
目标
实现侧边栏设置面板 codeReviewer.setupView:快速引导、AI 配置、API Key、输出语言、自定义规则管理。
新建文件
| # | 文件 | 说明 |
|---|---|---|
| 1 | src/views/setupView.ts |
SetupViewProvider implements vscode.TreeDataProvider |
面板参考
UI 预览文件: docs/superpowers/specs/setup-panel-preview.html
src/views/setupView.ts
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { getAIConfig, setApiKey, getApiKey, isApiKeyConfigured } from '../config';
import { createProvider } from '../ai/factory';
import { loadActiveRules } from '../rules/yaml-parser';
import { CustomRule } from '../rules/yaml-parser';
type SetupItemType = 'section' | 'step' | 'providerGroup' | 'provider' | 'model' | 'apiKey' | 'language' | 'rule' | 'ruleAdd' | 'action';
class SetupItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly itemType: SetupItemType,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly command?: vscode.Command,
public readonly iconPath?: vscode.ThemeIcon,
public readonly description?: string,
public readonly contextValue?: string,
) {
super(label, collapsibleState);
}
}
export class SetupViewProvider implements vscode.TreeDataProvider<SetupItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<SetupItem | undefined>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
private customRules: CustomRule[] = [];
private apiKeyConfigured = false;
private connectionTested = false;
private connectionSuccess = false;
constructor(private context: vscode.ExtensionContext) {
this.refresh();
}
async refresh(): Promise<void> {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';
this.customRules = loadActiveRules(workspaceRoot);
this.apiKeyConfigured = await isApiKeyConfigured(this.context);
this._onDidChangeTreeData.fire(undefined);
}
getTreeItem(element: SetupItem): vscode.TreeItem {
return element;
}
async getChildren(element?: SetupItem): Promise<SetupItem[]> {
if (!element) {
return this.getRootItems();
}
switch (element.itemType) {
case 'providerGroup': return this.getProviderItems();
case 'apiKey': return this.getApiKeyItems();
case 'language': return this.getLanguageItems();
default: return [];
}
}
private getRootItems(): SetupItem[] {
const items: SetupItem[] = [];
const step1Done = this.apiKeyConfigured;
const step2Done = this.customRules.some(r => r.id);
const step3Done = this.connectionTested && this.connectionSuccess;
items.push(new SetupItem(
'快速开始',
'section',
vscode.TreeItemCollapsibleState.Expanded,
undefined,
undefined,
undefined,
'section'
));
items.push(new SetupItem(
step1Done ? '① 完成 AI 模型配置' : '① 配置 AI 模型及 API Key',
'step',
vscode.TreeItemCollapsibleState.None,
step1Done ? undefined : {
command: 'codeReviewer.focusApiKey',
title: '配置 API Key',
},
step1Done ? new vscode.ThemeIcon('pass-filled', new vscode.ThemeColor('charts.purple')) : undefined
));
items.push(new SetupItem(
step2Done ? '② 完成规则启用' : '② 启用自定义规则',
'step',
vscode.TreeItemCollapsibleState.None,
undefined,
step2Done ? new vscode.ThemeIcon('pass-filled', new vscode.ThemeColor('charts.purple')) : undefined
));
items.push(new SetupItem(
step3Done ? '③ 完成连接测试' : '③ 保存并测试连接',
'step',
vscode.TreeItemCollapsibleState.None,
step3Done ? undefined : {
command: 'codeReviewer.saveAndTest',
title: '测试连接',
},
step3Done ? new vscode.ThemeIcon('pass-filled', new vscode.ThemeColor('charts.purple')) : undefined
));
items.push(new SetupItem(
'审核引擎',
'section',
vscode.TreeItemCollapsibleState.Collapsed
));
items.push(new SetupItem(
'AI 模型配置',
'providerGroup',
vscode.TreeItemCollapsibleState.Collapsed
));
items.push(new SetupItem(
'API Key',
'apiKey',
vscode.TreeItemCollapsibleState.Collapsed
));
items.push(new SetupItem(
'输出语言',
'language',
vscode.TreeItemCollapsibleState.Collapsed
));
items.push(new SetupItem(
`自定义规则 [${this.customRules.length} 条]`,
'section',
vscode.TreeItemCollapsibleState.Expanded
));
for (const rule of this.customRules) {
items.push(new SetupItem(
rule.id,
'rule',
vscode.TreeItemCollapsibleState.None,
{
command: 'codeReviewer.toggleRule',
title: '切换规则',
arguments: [rule.id],
},
undefined,
rule.severity,
'rule'
));
}
items.push(new SetupItem(
'输入规则名称... [+ 添加]',
'ruleAdd',
vscode.TreeItemCollapsibleState.None,
{
command: 'codeReviewer.addCustomRule',
title: '添加规则',
}
));
const connectionLabel = this.connectionTested
? (this.connectionSuccess ? '✓ 已连接' : '✗ 重试')
: '保存并测试连接';
items.push(new SetupItem(
connectionLabel,
'action',
vscode.TreeItemCollapsibleState.None,
{
command: 'codeReviewer.saveAndTest',
title: '测试连接',
}
));
return items;
}
private getProviderItems(): SetupItem[] {
const config = getAIConfig();
return [
new SetupItem(`提供商: ${config.provider}`,
'provider',
vscode.TreeItemCollapsibleState.None,
{
command: 'codeReviewer.selectProvider',
title: '选择提供商',
}
),
new SetupItem(`模型: ${config.model}`,
'model',
vscode.TreeItemCollapsibleState.None,
{
command: 'codeReviewer.selectModel',
title: '选择模型',
}
),
];
}
private getApiKeyItems(): SetupItem[] {
return [
new SetupItem(
'设置 API Key...',
'apiKey',
vscode.TreeItemCollapsibleState.None,
{
command: 'codeReviewer.setApiKey',
title: '设置 API Key',
}
),
];
}
private getLanguageItems(): SetupItem[] {
const config = getAIConfig();
return [
new SetupItem(
`当前: ${config.outputLanguage === 'zh-CN' ? '中文(简体)' : config.outputLanguage}`,
'language',
vscode.TreeItemCollapsibleState.None,
{
command: 'codeReviewer.selectLanguage',
title: '选择输出语言',
}
),
];
}
}
额外命令(在 commands.ts 中注册)
// 设置面板交互命令
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.setApiKey', async () => {
const key = await vscode.window.showInputBox({
prompt: '请输入 API Key',
password: true,
placeHolder: 'sk-...',
});
if (key) {
await setApiKey(context, key);
setupProvider?.refresh();
vscode.window.showInformationMessage('API Key 已保存');
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.selectProvider', async () => {
const config = vscode.workspace.getConfiguration('vscode-code-reviewer');
const current = config.get<string>('ai.provider', 'deepseek');
const selected = await vscode.window.showQuickPick(['deepseek', 'openai'], {
placeHolder: '选择模型提供商',
});
if (selected) {
await config.update('ai.provider', selected, vscode.ConfigurationTarget.Global);
setupProvider?.refresh();
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.selectModel', async () => {
const config = vscode.workspace.getConfiguration('vscode-code-reviewer');
const current = config.get<string>('ai.model', '');
const selected = await vscode.window.showInputBox({
prompt: '输入模型名称',
value: current,
placeHolder: 'deepseek-chat',
});
if (selected) {
await config.update('ai.model', selected, vscode.ConfigurationTarget.Global);
setupProvider?.refresh();
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.selectLanguage', async () => {
const config = vscode.workspace.getConfiguration('vscode-code-reviewer');
const current = config.get<string>('ai.outputLanguage', 'zh-CN');
const selected = await vscode.window.showQuickPick(
[
{ label: '中文(简体)', value: 'zh-CN' },
{ label: 'English', value: 'en' },
{ label: '日本語', value: 'ja' },
],
{ placeHolder: '选择输出语言' }
);
if (selected) {
await config.update('ai.outputLanguage', selected.value, vscode.ConfigurationTarget.Global);
setupProvider?.refresh();
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.saveAndTest', async () => {
const apiKey = await getApiKey(context);
if (!apiKey) {
vscode.window.showWarningMessage('请先设置 API Key');
return;
}
const config = getAIConfig();
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: '测试连接...',
cancellable: false,
}, async () => {
try {
const provider = createProvider(config.provider, apiKey, config.endpoint);
await provider.chat('回复 ok', 'ping', {
model: config.model,
temperature: 0,
timeoutMs: 15000,
});
connectionTested = true;
connectionSuccess = true;
setupProvider?.refresh();
vscode.window.showInformationMessage('✓ 连接成功', { modal: false });
} catch (err) {
connectionTested = true;
connectionSuccess = false;
setupProvider?.refresh();
const message = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(`✗ 连接失败: ${message}`, { modal: false });
}
});
})
);
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.toggleRule', async (ruleId: string) => {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) { return; }
const configPath = path.join(workspaceRoot, '.code-review', 'config.yaml');
if (!fs.existsSync(configPath)) { return; }
let content = fs.readFileSync(configPath, 'utf-8');
const enabledPattern = new RegExp(`^(\\s*${ruleId}\\s*:\\s*\\n\\s*enabled\\s*:\\s*)(true|false)`, 'm');
const match = enabledPattern.exec(content);
if (match) {
const newValue = match[2] === 'true' ? 'false' : 'true';
content = content.replace(enabledPattern, `$1${newValue}`);
} else {
content += `\n ${ruleId}:\n enabled: false\n`;
}
fs.writeFileSync(configPath, content, 'utf-8');
setupProvider?.refresh();
})
);
在 extension.ts 中注册视图
import { SetupViewProvider } from './views/setupView';
let setupProvider: SetupViewProvider;
export function activate(context: vscode.ExtensionContext) {
setupProvider = new SetupViewProvider(context);
vscode.window.registerTreeDataProvider('codeReviewer.setupView', setupProvider);
// ... 其余
}
注意: 需要
setupProvider作为模块级变量暴露给commands.ts。
验收
- 侧边栏显示 CodeGuard 设置视图
- 快速开始三步引导可见
- API Key 可输入并保存到 SecretStorage
- 提供商/模型可切换
- 自定义规则列表展示并支持开关
- 保存并测试连接按钮功能正常
npm run compile通过npm run lint通过