- 适配器 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 依赖及测试用例
28 KiB
AI 供应商与模型动态化设计书
将写死的供应商注册表外置为 JSON 配置文件,模型下拉框改为可编辑输入框,并修复
package.json不一致问题。
- 项目: vscode-code-reviewer (Code Purifier)
- 分支: vscode-code-reviewer
- 日期: 2026-07-28
- 版本: 1.1.0 → 1.2.0
目录
- 01 问题分析
- 02 设计目标与范围
- 03 方案 A:模型输入框可编辑化
- 04 方案 B:供应商注册表外置化
- 05 修复 package.json 不一致
- 06 数据结构设计
- 07 注册表加载器实现
- 08 工厂函数改造
- 09 侧边栏面板改造
- 10 文件级变更规格
- 11 迁移路径
- 12 测试要点
01 问题分析
当前 src/ai/factory.ts 中的 registry 是一个纯静态对象,硬编码了 8 个供应商及其模型列表。这种写法在大模型快速迭代的环境下存在三类问题。
1.1 模型列表过期
factory.ts 中的模型列表是编码时手写的快照,一旦供应商发布新模型或下线旧模型,插件无法感知。用户只能使用列表中预置的模型名,即使供应商 API 已经支持新模型,也必须等插件发版后才能选用。
1.2 默认值不一致
| 位置 | 内容 | 问题 |
|---|---|---|
package.json 第 92-96 行 |
ai.provider enum 仅有 deepseek、openai |
工厂注册了 8 个供应商,但配置 schema 只声明了 2 个 |
package.json 第 100 行 |
默认模型 deepseek-chat |
factory.ts 中 DeepSeek 的模型列表为 deepseek-v4-pro、deepseek-v4-flash,不包含 deepseek-chat |
用户首次安装后,默认模型在下拉框中找不到对应项,populateModelOptions() 会回退到列表第一个模型,导致实际使用的模型与配置中记录的不一致。
1.3 无法使用列表外模型
setupView.ts 中模型选择是 <select> 元素,只能从预置列表中选择。如果用户想使用新发布的模型、供应商列表中不存在的 OpenAI 兼容服务(如本地 Ollama、vLLM 部署),无法手动输入模型名。
02 设计目标与范围
优化项清单
| # | 优化项 | 方案 | 涉及文件 | 变更类型 |
|---|---|---|---|---|
| 1 | 模型选择改为可编辑输入框 | A | views/setupView.ts、views/setupView.js |
修改 UI + 逻辑 |
| 2 | 供应商注册表外置为 JSON | B | 新增 providers.json、ai/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 行原来监听 modelSelect 的 change 事件,改为监听 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-compatible、gemini、claude |
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.ts 的 resolveWebviewView() 中注册文件系统监听器,当用户编辑 .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 适配
现有的 GeminiProvider 和 ClaudeProvider 构造函数签名为 constructor(apiKey, baseUrl),不接收 id 和 name。保持不变,在 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 引用替换为 modelInput,populateModelOptions() 改为填充 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
提取 ProviderConfig、ProvidersFile、ProviderMeta、ProviderProtocol 类型定义。factory.ts 和 registry.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.0 → 1.2.0 |
[MODIFY] src/i18n/messages.ts
新增 setup.modelPlaceholder 国际化键(三语言)。
.vscodeignore
确认 providers.json 不在忽略列表中,确保随 VSIX 打包。
11 迁移路径
11.1 内置 providers.json 中的模型名修正
当前 factory.ts 中的模型名(如 deepseek-v4-pro、GPT-5.6 Sol、Claude 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 Key(SecretStorage) | 保留不变 |
ai.outputLanguage |
保留不变 |
11.3 向后兼容保证
createProvider() 的调用方(engine.ts、setupView.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 出现在下拉框 |
| 用户覆盖内置供应商 | 用户文件中 deepseek 的 defaultBaseUrl 改为代理地址 |
合并后 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 节的数据结构和逻辑为实现依据。