Files
2026Technology-Competition/docs/superpowers/specs/implementation-steps/16-phase5.2-setup-panel.md
T
范智鹏 a734cdf009 refactor: 品牌重命名 + maxTokens 支持 + 设置面板简化
- CodeGuard → Code Purifier / 净码特工(displayName、命令、配置标题)
- 新增 ai.maxTokens 配置项,所有 Provider 及 fixer 传入 maxTokens
- AI 引擎增强:repairJsonEscapes + JSON 解析 fallback + 详细错误信息
- 设置面板规则管理改为文件级(list/delete .yaml),addRule 改为 AI 从 Markdown 生成 YAML
- yaml-parser 简化:移除 config.yaml 的 enable/disable 过滤逻辑
- 审查报告面板:errorBanner 优先显示具体错误、lint 诊断显示 suggestion、移除 translatedDiagnostics 独立渲染
- merger 中 translatedDiagnostics 覆盖原始 lint 诊断 message/suggestion
- HTML linter 配置项、测试用例重写、typescript-eslint 移入 dependencies
2026-07-20 20:24:40 +08:00

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.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 中注册视图

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


验收

  • 侧边栏显示 净码特工 设置视图
  • 快速开始三步引导可见
  • API Key 可输入并保存到 SecretStorage
  • 提供商/模型可切换
  • 自定义规则列表展示并支持开关
  • 保存并测试连接按钮功能正常
  • npm run compile 通过
  • npm run lint 通过