Files
2026Technology-Competition/docs/superpowers/specs/2026-07-28-provider-registry-dynamic-design.md
T
范智鹏 effcf30802 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 依赖及测试用例
2026-07-28 22:57:15 +08:00

28 KiB
Raw Blame History

AI 供应商与模型动态化设计书

将写死的供应商注册表外置为 JSON 配置文件,模型下拉框改为可编辑输入框,并修复 package.json 不一致问题。

  • 项目: vscode-code-reviewer (Code Purifier)
  • 分支: vscode-code-reviewer
  • 日期: 2026-07-28
  • 版本: 1.1.0 → 1.2.0

目录


01 问题分析

当前 src/ai/factory.ts 中的 registry 是一个纯静态对象,硬编码了 8 个供应商及其模型列表。这种写法在大模型快速迭代的环境下存在三类问题。

1.1 模型列表过期

factory.ts 中的模型列表是编码时手写的快照,一旦供应商发布新模型或下线旧模型,插件无法感知。用户只能使用列表中预置的模型名,即使供应商 API 已经支持新模型,也必须等插件发版后才能选用。

1.2 默认值不一致

位置 内容 问题
package.json 第 92-96 行 ai.provider enum 仅有 deepseekopenai 工厂注册了 8 个供应商,但配置 schema 只声明了 2 个
package.json 第 100 行 默认模型 deepseek-chat factory.ts 中 DeepSeek 的模型列表为 deepseek-v4-prodeepseek-v4-flash,不包含 deepseek-chat

用户首次安装后,默认模型在下拉框中找不到对应项,populateModelOptions() 会回退到列表第一个模型,导致实际使用的模型与配置中记录的不一致。

1.3 无法使用列表外模型

setupView.ts 中模型选择是 <select> 元素,只能从预置列表中选择。如果用户想使用新发布的模型、供应商列表中不存在的 OpenAI 兼容服务(如本地 Ollama、vLLM 部署),无法手动输入模型名。


02 设计目标与范围

优化项清单

# 优化项 方案 涉及文件 变更类型
1 模型选择改为可编辑输入框 A views/setupView.tsviews/setupView.js 修改 UI + 逻辑
2 供应商注册表外置为 JSON B 新增 providers.jsonai/registry.ts 新增文件
3 工厂函数改为动态加载 B ai/factory.ts 重构
4 支持用户自定义供应商覆盖 B ai/registry.ts 新增逻辑
5 修复 package.json 不一致 附加 package.json 修改 schema

零破坏性原则

所有变更不改变 AI 审查引擎的消费侧接口。engine.ts 调用 createProvider()getAIModel() 的方式不变,runAIReview() 的逻辑分支不受影响。现有用户的配置(provider、model、baseUrl、apiKey)在升级后自动保留,无需重新配置。


03 方案 A:模型输入框可编辑化

现状

setupView.ts 第 889-891 行渲染模型选择器:

<select id="modelSelect">${(providers[config.provider]?.models ?? []).map(m =>
  `<option value="${m}"${config.model === m ? ' selected' : ''}>${m}</option>`
).join('\n          ')}</select>

setupView.js 第 88-102 行的 populateModelOptions()<select> 填充 <option>

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];
  }
}

改造方案

<select> 替换为 <input type="text" list="..."> + <datalist>datalist 提供预置建议列表,同时允许用户手动输入任意模型名。

HTML 变更(setupView.ts getHtml()

<div class="field">
  <label class="field-label">${t('setup.model')}</label>
  <input type="text" id="modelInput" list="modelOptions"
    value="${config.model || ''}"
    placeholder="${t('setup.modelPlaceholder')}"
    onchange="postMsg('setModel', this.value)">
  <datalist id="modelOptions">
    ${(providers[config.provider]?.models ?? []).map(m =>
      `<option value="${m}">`
    ).join('\n          ')}
  </datalist>
</div>

JS 变更(setupView.js populateModelOptions()

function populateModelOptions(providerId, currentModel) {
  var datalist = document.getElementById('modelOptions');
  var input = document.getElementById('modelInput');
  if (!datalist || !input) { return; }

  var models = (PROVIDERS[providerId] && PROVIDERS[providerId].models) || [];

  // 刷新 datalist 选项
  datalist.innerHTML = '';
  for (var i = 0; i < models.length; i++) {
    var opt = document.createElement('option');
    opt.value = models[i];
    datalist.appendChild(opt);
  }

  // 保留用户已输入的模型名,仅当为空时填入第一个建议
  if (!input.value && models.length > 0) {
    input.value = models[0];
    postMsg('setModel', models[0]);
  }
}

事件绑定变更

setupView.js 第 109-111 行原来监听 modelSelectchange 事件,改为监听 modelInput

document.getElementById('modelInput').addEventListener('change', function () {
  postMsg('setModel', this.value.trim());
});

initConfig 消息处理变更

setupView.js 第 143-147 行原来调用 populateModelOptions(c.provider, c.model) 并设置 select.value。改为直接设置 input.value

if (c.provider && PROVIDERS[c.provider]) {
  populateModelOptions(c.provider, c.model);
  var mi = document.getElementById('modelInput');
  if (mi && c.model) { mi.value = c.model; }
  var ps = document.getElementById('providerSelect');
  if (ps) { ps.value = c.provider; }
}

新增国际化键

zh-CN en ja
setup.modelPlaceholder 输入或选择模型名 Enter or select model name モデル名を入力または選択

04 方案 B:供应商注册表外置化

整体架构

插件内置 providers.json(随 VSIX 打包)
         ↓
registry.ts 加载并解析
         ↓
用户工作区 .code-review/providers.json(可选覆盖)
         ↓
合并后的 ProviderConfig[] 供 factory.ts 和 setupView.ts 消费

providers.json 格式

{
  "providers": [
    {
      "id": "deepseek",
      "name": "DeepSeek",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "https://api.deepseek.com/v1",
      "models": ["deepseek-chat", "deepseek-reasoner"]
    },
    {
      "id": "openai",
      "name": "OpenAI",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "https://api.openai.com/v1",
      "models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"]
    },
    {
      "id": "gemini",
      "name": "Google Gemini",
      "protocol": "gemini",
      "defaultBaseUrl": "https://generativelanguage.googleapis.com/v1",
      "models": ["gemini-2.0-flash", "gemini-1.5-pro"]
    },
    {
      "id": "claude",
      "name": "Anthropic Claude",
      "protocol": "claude",
      "defaultBaseUrl": "https://api.anthropic.com/v1",
      "models": ["claude-sonnet-4-20250514", "claude-opus-4-20250514"]
    },
    {
      "id": "hunyuan",
      "name": "腾讯混元",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "https://api.hunyuan.cloud.tencent.com/v1",
      "models": ["hunyuan-pro"]
    },
    {
      "id": "zhipu",
      "name": "智谱AI",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "https://open.bigmodel.cn/api/paas/v4",
      "models": ["glm-4-plus", "glm-4-flash"]
    },
    {
      "id": "moonshot",
      "name": "月之暗面",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "https://api.moonshot.cn/v1",
      "models": ["moonshot-v1-8k", "moonshot-v1-32k"]
    },
    {
      "id": "tongyi",
      "name": "阿里通义",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
      "models": ["qwen-plus", "qwen-turbo"]
    }
  ]
}

字段说明

字段 类型 必填 说明
id string 供应商唯一标识,用于配置存储和工厂查找
name string 显示名称,出现在侧边栏下拉框
protocol string 协议类型,决定实例化哪个 Provider 类。可选值:openai-compatiblegeminiclaude
defaultBaseUrl string 该供应商的默认 API 端点
models string[] 预置模型列表,作为 datalist 建议项。用户可输入列表外的模型名

用户覆盖机制

用户可在工作区根目录的 .code-review/providers.json 中放置同名文件,格式与内置文件一致。加载器按以下规则合并:

情况 合并结果
用户文件包含内置中已有的供应商(id 相同) 用户配置完整覆盖该供应商的所有字段
用户文件包含内置中没有的供应商 追加为新供应商
用户文件缺少内置中的某供应商 保留内置的该供应商

示例:用户想添加本地 Ollama 服务,在工作区放置 .code-review/providers.json

{
  "providers": [
    {
      "id": "ollama",
      "name": "Ollama (本地)",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "http://localhost:11434/v1",
      "models": ["llama3.1", "qwen2.5"]
    },
    {
      "id": "deepseek",
      "name": "DeepSeek (自定义代理)",
      "protocol": "openai-compatible",
      "defaultBaseUrl": "https://my-proxy.example.com/v1",
      "models": ["deepseek-chat", "deepseek-coder"]
    }
  ]
}

合并后:ollama 作为新供应商追加,deepseek 的 name/baseUrl/models 被用户配置完整覆盖,其余 6 个内置供应商保留。


05 修复 package.json 不一致

ai.provider 配置项

去掉 enum 限制,改为自由 string,因为供应商列表现在是动态的:

"vscode-code-reviewer.ai.provider": {
  "type": "string",
  "default": "deepseek",
  "description": "AI 模型提供商"
}

ai.model 配置项

去掉 enum 限制(如果有),保持为自由 string:

"vscode-code-reviewer.ai.model": {
  "type": "string",
  "default": "deepseek-chat",
  "description": "AI 模型名称"
}

默认模型保持 deepseek-chat,与内置 providers.json 中 DeepSeek 的第一个模型对齐。用户升级后如果之前未手动改过模型,会自动匹配到列表中的建议项。


06 数据结构设计

ProviderConfig 接口

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[];
}

ProviderMeta 接口(供 UI 消费)

export interface ProviderMeta {
  name: string;
  models: string[];
}

getAllProviderMeta() 的返回类型保持 Record<string, ProviderMeta>,与现有结构一致。


07 注册表加载器实现

新建文件:src/ai/registry.ts

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;

/** 读取插件内置的 providers.json */
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 [];
  }
}

/** 读取用户工作区的 .code-review/providers.json */
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 [];
  }
}

/** 合并内置与用户配置:用户配置按 id 覆盖内置 */
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());
}

/**
 * 获取合并后的供应商列表。
 * 首次调用时加载并缓存,后续调用直接返回缓存。
 * extensionUri 仅首次调用时需要传入。
 */
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;
}

/** 按 id 查找单个供应商 */
export function getProviderById(
  extensionUri: vscode.Uri,
  id: string
): ProviderConfig | undefined {
  return getProviders(extensionUri).find(p => p.id === id);
}

/** 获取所有供应商的 UI 元数据 */
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;
}

缓存失效策略

setupView.tsresolveWebviewView() 中注册文件系统监听器,当用户编辑 .code-review/providers.json 后自动刷新:

const watcher = vscode.workspace.createFileSystemWatcher(
  '**/.code-review/providers.json'
);

watcher.onDidChange(() => {
  invalidateProviderCache();
  this.pushConfig();
});

watcher.onDidCreate(() => {
  invalidateProviderCache();
  this.pushConfig();
});

webviewView.onDidDispose(() => {
  watcher.dispose();
});

08 工厂函数改造

现状

factory.ts 中的 registry 是静态对象,每个供应商的 cls 字段直接引用具体类。createProvider() 通过 new info.cls(apiKey, baseUrl) 实例化。

改造方案

factory.ts 不再持有静态注册表,改为从 registry.ts 动态加载。protocol 字段决定实例化哪个类。

改造后的 factory.ts

import * as vscode from 'vscode';
import type { AIProvider } from './providers/base';
import type { ProviderConfig, ProviderMeta, ProviderProtocol } from './types';
import { OpenAICompatibleProvider } from './providers/openai-compatible';
import { GeminiProvider } from './providers/gemini';
import { ClaudeProvider } from './providers/claude';
import {
  getProviders,
  getProviderById,
  getAllProviderMeta as getAllProviderMetaFromRegistry,
  invalidateProviderCache,
} from './registry';

const PROTOCOL_MAP: Record<ProviderProtocol,
  new (apiKey: string, baseUrl: string, id: string, name: string) => any
> = {
  'openai-compatible': OpenAICompatibleProvider,
  gemini: GeminiProvider,
  claude: ClaudeProvider,
};

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}`);
  }

  const Cls = PROTOCOL_MAP[config.protocol];
  if (!Cls) {
    throw new Error(`未知的协议类型: ${config.protocol}`);
  }

  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 };

GeminiProvider / ClaudeProvider 适配

现有的 GeminiProviderClaudeProvider 构造函数签名为 constructor(apiKey, baseUrl),不接收 idname。保持不变,在 createProvider() 中根据 protocol 分别调用不同的构造方式。

engine.ts 调用链变更

engine.ts 第 195 行调用 createProvider() 时需要传入 extensionUri

// 现状
provider = createProvider(providerId, apiKey, baseUrl);

// 改造后
provider = createProvider(providerId, apiKey, baseUrl, context.extensionUri);

runAIReview() 已经接收 context: vscode.ExtensionContext 参数(第 173 行),context.extensionUri 可直接获取。

testConnection() 调用链变更

setupView.ts 第 380 行的 testConnection() 同样需要传入 extensionUri

const provider = createProvider(config.provider, apiKey, config.baseUrl, this.context.extensionUri);

convertContentWithAI() 调用链变更

src/rules/import-service.ts 第 382 行的 convertContentWithAI() 也调用了 createProvider()。该函数已接收 context: vscode.ExtensionContext 参数(第 367 行),可直接使用 context.extensionUri

// 现状
const provider = createProvider(config.provider, apiKey, config.baseUrl);

// 改造后
const provider = createProvider(config.provider, apiKey, config.baseUrl, context.extensionUri);

09 侧边栏面板改造

setupView.ts 变更

resolveWebviewView() — 初始化时传入 extensionUri

第 203 行:

// 现状
const providers = getAllProviderMeta();

// 改造后
const providers = getAllProviderMeta(this.context.extensionUri);

pushConfig() — 推送供应商元数据

第 335 行:

// 现状
providers: getAllProviderMeta(),

// 改造后
providers: getAllProviderMeta(this.context.extensionUri),

setProvider 消息处理 — 传入 extensionUri

第 245 行:

// 现状
const models = getProviderModels(msg.value);

// 改造后
const models = getProviderModels(this.context.extensionUri, msg.value);

getHtml() — 供应商下拉框渲染

供应商下拉框保持 <select> 不变(供应商数量有限,且不允许用户手动输入供应商 id),但 providers 数据来源改为 getAllProviderMeta(this.context.extensionUri)

模型区域改为方案 A 中的 <input> + <datalist>

resolveWebviewView() — 新增 FileSystemWatcher

新增对 .code-review/providers.json 的文件系统监听,在 onDidDispose 中一并释放。

setupView.js 变更

modelSelect → modelInput

按方案 A 的改造方案,将所有 modelSelect 引用替换为 modelInputpopulateModelOptions() 改为填充 datalist

initConfig 消息处理 — 设置 modelInput 值

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; }
}

10 文件级变更规格

[NEW] providers.json(项目根目录)

插件内置的供应商配置文件,随 VSIX 打包。内容见 04 节的 JSON 示例。包含 8 个供应商的完整配置。

[NEW] src/ai/types.ts

提取 ProviderConfigProvidersFileProviderMetaProviderProtocol 类型定义。factory.tsregistry.ts 均从此文件导入。

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[];
}

[NEW] src/ai/registry.ts

注册表加载器,实现内置 JSON 读取、用户覆盖合并、缓存管理。完整实现见 07 节。

[MODIFY] src/ai/factory.ts

变更位置 变更内容
删除 静态 registry 对象及其全部内容
删除 ProviderInfo 接口定义
修改 createProvider() 签名新增 extensionUri 参数,改为从 registry 动态查找
修改 getProviderModels() 签名新增 extensionUri 参数
修改 getAllProviderMeta() 签名新增 extensionUri 参数,委托给 registry.ts
新增 PROTOCOL_MAP 常量,映射 protocol → Provider 类
新增 invalidateProviderCache re-export
删除 getProviderIds()getProviderInfo()getProviderDefaultBaseUrl()(改为通过 registry.ts 获取)

[MODIFY] src/ai/engine.ts

变更位置 变更内容
第 195 行 createProvider(providerId, apiKey, baseUrl)createProvider(providerId, apiKey, baseUrl, context.extensionUri)

[MODIFY] src/rules/import-service.ts

变更位置 变更内容
第 382 行 createProvider(config.provider, apiKey, config.baseUrl)createProvider(config.provider, apiKey, config.baseUrl, context.extensionUri)

[MODIFY] src/views/setupView.ts

变更位置 变更内容
第 203 行 getAllProviderMeta() 调用改为传入 this.context.extensionUri
第 245 行 getProviderModels(msg.value) 调用改为传入 this.context.extensionUri, msg.value
第 335 行 getAllProviderMeta() 调用改为传入 this.context.extensionUri
第 380 行 createProvider() 调用新增 this.context.extensionUri 参数
resolveWebviewView() 新增 FileSystemWatcher 监听 .code-review/providers.json 变更
第 889-891 行 模型区域 <select id="modelSelect"> 替换为 <input id="modelInput" list="modelOptions"> + <datalist id="modelOptions">

[MODIFY] src/views/setupView.js

变更位置 变更内容
第 88-102 行 populateModelOptions() 改为填充 <datalist> 而非 <select>,保留用户已输入的值
第 109-111 行 modelSelect change 事件 改为 modelInput change 事件
第 143-147 行 initConfig 消息处理 modelSelect.value 设置改为 modelInput.value 设置

[MODIFY] package.json

变更位置 变更内容
第 92-96 行 ai.provider 配置项 删除 enum 数组,改为自由 string
版本号 1.1.01.2.0

[MODIFY] src/i18n/messages.ts

新增 setup.modelPlaceholder 国际化键(三语言)。

.vscodeignore

确认 providers.json 不在忽略列表中,确保随 VSIX 打包。


11 迁移路径

11.1 内置 providers.json 中的模型名修正

当前 factory.ts 中的模型名(如 deepseek-v4-proGPT-5.6 SolClaude Fable 5)并非真实模型名。新的 providers.json 中应使用各供应商 API 实际接受的模型标识符:

供应商 当前(虚构) 修正为(真实)
DeepSeek deepseek-v4-pro, deepseek-v4-flash deepseek-chat, deepseek-reasoner
OpenAI GPT-5.6 Sol, GPT-5.6 Terra gpt-4o, gpt-4o-mini, gpt-4-turbo
Gemini Gemini 3.1 Pro gemini-2.0-flash, gemini-1.5-pro
Claude Claude Fable 5 claude-sonnet-4-20250514, claude-opus-4-20250514
混元 Hy3 hunyuan-pro
智谱 GLM-5.2 glm-4-plus, glm-4-flash
月之暗面 Kimi K2.7 Code moonshot-v1-8k, moonshot-v1-32k
通义 Qwen3-2507 qwen-plus, qwen-turbo

以上模型名基于截至 2026-07 各供应商 API 文档的公开模型标识符。实际发布前需再次验证各供应商 API 文档的最新模型列表。

11.2 用户配置兼容性

用户已有配置 升级后行为
ai.provider = deepseek 保留,providers.json 中存在该 id,正常工作
ai.model = deepseek-chat 保留,新 providers.json 中 DeepSeek 列表包含此项,datalist 中可见
ai.model = deepseek-v4-pro(旧列表中的值) 保留,modelInput 中显示该值,但不在 datalist 建议中。用户可继续使用或手动修改。API 是否接受取决于供应商
ai.baseUrl 有值 保留不变,切换供应商时不自动覆盖
API KeySecretStorage 保留不变
ai.outputLanguage 保留不变

11.3 向后兼容保证

createProvider() 的调用方(engine.tssetupView.ts)需要传入 extensionUri。这是签名变更,但均在插件内部调用,不影响用户侧 API。runAIReview() 的外部调用签名不变。


12 测试要点

方案 A:模型输入框

测试场景 操作步骤 期望结果
选择预置模型 点击模型输入框,从 datalist 建议中选择一个 输入框填入选中值,ai.model 配置更新
手动输入模型名 在模型输入框中输入 my-custom-model 并失焦 输入框保留输入值,ai.model 更新为 my-custom-model
切换供应商后保留手输模型 先手动输入模型名,再切换供应商 模型输入框清空并填入新供应商的第一个建议模型
空值处理 清空模型输入框并失焦 ai.model 更新为空字符串,AI 审查时报错提示需配置模型

方案 B:供应商注册表

测试场景 前置条件 期望结果
内置 JSON 加载 无用户覆盖文件 getProviders() 返回 8 个内置供应商
用户追加供应商 工作区有 .code-review/providers.json,包含 ollama 合并后返回 9 个供应商,ollama 出现在下拉框
用户覆盖内置供应商 用户文件中 deepseekdefaultBaseUrl 改为代理地址 合并后 DeepSeek 的 defaultBaseUrl 为用户配置的代理地址
用户文件格式错误 .code-review/providers.json 内容为非法 JSON 加载器静默返回空数组,不影响内置供应商加载
用户文件不存在 工作区无 .code-review/ 目录 仅返回内置 8 个供应商
文件监听刷新 编辑并保存 .code-review/providers.json 缓存失效,侧边栏自动刷新供应商列表
缓存命中 连续调用两次 getProviders() 第二次直接返回缓存,不重复读取文件系统

附加修复项

测试场景 操作步骤 期望结果
package.json 无 enum 限制 在 settings.json 中手动写入 ai.provider: "custom-provider" VS Code 不报 schema 校验错误,插件正常加载
连接测试使用 extensionUri 配置完 API Key 后点击"保存并测试" createProvider() 正确接收 extensionUri,测试请求发送到正确端点

回归测试

测试场景 期望结果
AI 代码审查(Ctrl+Shift+R 正常运行,使用配置的供应商和模型
选中代码审查 正常运行
审查结果面板导出 正常导出报告
自定义规则管理 规则文件导入、删除正常
静态分析适配器面板 4 张适配器卡片正常渲染,不受供应商改造影响
保存文件自动分析 500ms 防抖后触发静态分析

本设计书基于 vscode-code-reviewer 插件 v1.1.0 编写,覆盖方案 A(模型输入框可编辑化)、方案 B(供应商注册表外置化)及 package.json 修复的全部技术实现规格。实施时按第 10 节文件级变更规格逐文件执行,以第 06-09 节的数据结构和逻辑为实现依据。