- 设置面板:三步引导 / AI 配置 / API Key / 规则管理 / 连接测试 - Provider 重构:统一 OpenAICompatibleProvider 基类,新增 Gemini/Claude/混元/智谱等 - 审查面板 Webview:三 Tab、统计卡片、问题列表、postMessage 通信 - esbuild 构建脚本 + 生产打包 + PMD 下载 - 保存文件自动静态分析(500ms debounce)
422 lines
12 KiB
Markdown
422 lines
12 KiB
Markdown
# 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`
|
|
|
|
```typescript
|
|
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` 中注册)
|
|
|
|
```typescript
|
|
// 设置面板交互命令
|
|
|
|
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.baseUrl);
|
|
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` 中注册视图
|
|
|
|
```typescript
|
|
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` 通过
|