# 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 { private _onDidChangeTreeData = new vscode.EventEmitter(); 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 { 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 { 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('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('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('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` 通过