Back to Prompt Optimizer

技术实现详解

docs/archives/121-multi-custom-models-support/implementation.md

2.10.26.8 KB
Original Source

技术实现详解

🔧 架构设计

整体架构

用户环境变量 → 环境变量扫描 → 动态模型生成 → 模型注册 → UI显示
     ↓              ↓              ↓           ↓         ↓
VITE_CUSTOM_API_*  scanCustom...  generateDynamic  getAllModels  ModelSelector

核心组件

  1. 环境变量扫描器 (scanCustomModelEnvVars)

    • 统一的环境变量发现和解析逻辑
    • 支持多种环境源(process.env、window.runtime_config等)
    • 配置验证和错误处理
  2. 动态模型生成器 (generateDynamicModels)

    • 基于扫描结果生成模型配置
    • 冲突检测和去重处理
    • 模型配置标准化
  3. 模型配置管理器 (getAllModels)

    • 合并静态和动态模型
    • 提供统一的模型访问接口
    • 缓存和性能优化

数据流设计

typescript
// 1. 环境变量扫描
const customModels = scanCustomModelEnvVars();

// 2. 动态模型生成
const dynamicModels = generateDynamicModels();

// 3. 模型合并
const allModels = { ...staticModels, ...dynamicModels };

🐛 问题诊断与解决

问题1: 模块加载时机问题

问题描述: 担心Electron环境中环境变量在模块加载时未就绪 诊断过程:

  • 分析主进程启动顺序
  • 检查环境变量加载时机
  • 验证模块导入顺序

解决方案:

  • 发现问题是理论性的,实际环境变量在模块加载前已就绪
  • 保持简单的直接导出方式,避免过度设计

问题2: 环境变量检查逻辑错误

问题描述: process.env[key] 检查会忽略空字符串值 诊断过程:

typescript
// 错误的检查方式
if (process.env[key]) { // 空字符串会被忽略
  return process.env[key] || '';
}

// 正确的检查方式  
if (process.env[key] !== undefined) { // 正确处理空字符串
  return process.env[key] || '';
}

解决方案: 修改条件检查逻辑,正确处理空字符串值

问题3: 代码重复和维护性

问题描述: 多个模块重复定义相同的常量和逻辑 诊断过程: 发现Desktop模块重复定义了环境变量扫描常量 解决方案: 统一从core模块导入共享常量,消除重复

问题4: Docker脚本字符转义bug

问题描述: echosed 的字符转义不正确 诊断过程:

  • echo "$value" 会解释控制字符
  • sed 's/\n/\\n/g' 匹配字面字符串而非实际换行符

解决方案: 使用 printf '%s' 替代 echo,简化转义逻辑

问题5: 过度的生产环境判断

问题描述: 大量 NODE_ENV !== 'production' 判断是过度设计 诊断过程: 分析日志需求和调试价值 解决方案: 移除所有过度的环境判断,保持日志简洁直接

📝 实施步骤

第一阶段: 核心功能实现

  1. 创建环境变量扫描函数

    • 实现 scanCustomModelEnvVars 函数
    • 支持多环境源和配置验证
    • 添加完整的错误处理
  2. 修改Core模块

    • 更新 defaults.ts 中的模型生成逻辑
    • 修改 electron-config.ts 保持一致性
    • 实现动态模型生成和合并

第二阶段: 模块适配

  1. MCP Server适配

    • 扩展环境变量映射逻辑
    • 支持动态后缀的环境变量
    • 更新错误提示信息
  2. Desktop模块适配

    • 修改环境变量检查逻辑
    • 更新IPC处理器
    • 实现动态环境变量同步
  3. Docker模块适配

    • 修改运行时配置生成脚本
    • 支持动态环境变量扫描
    • 更新配置文件生成逻辑

第三阶段: 质量保证

  1. 配置验证和容错

    • 实现配置完整性检查
    • 添加冲突检测机制
    • 完善错误处理和日志
  2. 文档和示例

    • 更新 env.local.example
    • 创建用户配置指南
    • 添加配置示例和说明
  3. 测试验证

    • 编写14个测试用例
    • 验证各种配置场景
    • 确保向后兼容性

🔍 调试过程

调试工具

  • 环境变量检查: 使用 console.log 跟踪变量传递
  • 模块验证: 逐模块验证环境变量读取
  • 配置追踪: 记录配置生成和合并过程

调试技巧

  1. 分层调试: 从环境变量 → 扫描 → 生成 → 注册逐层验证
  2. 对比测试: 新旧配置方式并行测试确保兼容性
  3. 边界测试: 测试空配置、部分配置、错误配置等边界情况

🧪 测试验证

测试场景

  1. 基础功能测试

    • 单个自定义模型配置
    • 多个自定义模型配置
    • 混合静态和动态模型
  2. 边界条件测试

    • 空配置处理
    • 部分配置处理
    • 无效后缀名处理
  3. 兼容性测试

    • 原有配置保持不变
    • 新旧配置混合使用
    • 升级场景测试
  4. 环境测试

    • Web环境测试
    • Desktop环境测试
    • Docker环境测试

测试结果

  • 测试用例: 14个
  • 通过率: 100%
  • 覆盖场景: 完整覆盖所有使用场景
  • 性能影响: 无明显性能影响

🔧 关键技术点

环境变量扫描

typescript
export const scanCustomModelEnvVars = (): Record<string, CustomModelEnvConfig> => {
  const customModels: Record<string, CustomModelEnvConfig> = {};
  const customApiPattern = /^VITE_CUSTOM_API_(KEY|BASE_URL|MODEL)_(.+)$/;
  
  // 多环境源合并
  const mergedEnv = {
    ...getProcessEnv(),
    ...getRuntimeConfig(),
    ...getElectronEnv()
  };
  
  // 扫描和分组
  Object.entries(mergedEnv).forEach(([key, value]) => {
    const match = key.match(customApiPattern);
    if (match) {
      const [, configType, suffix] = match;
      // 配置验证和分组逻辑
    }
  });
  
  return customModels;
};

动态模型生成

typescript
export function generateDynamicModels(): Record<string, ModelConfig> {
  const customModelConfigs = scanCustomModelEnvVars();
  const dynamicModels: Record<string, ModelConfig> = {};
  
  Object.entries(customModelConfigs).forEach(([suffix, envConfig]) => {
    // 配置验证
    if (!envConfig.apiKey || !envConfig.baseURL || !envConfig.model) {
      return; // 跳过不完整配置
    }
    
    // 冲突检测
    const staticModelKeys = ['openai', 'gemini', 'deepseek', 'siliconflow', 'zhipu', 'custom'];
    if (staticModelKeys.includes(suffix)) {
      return; // 跳过冲突配置
    }
    
    // 生成模型配置
    const modelKey = `custom_${suffix}`;
    dynamicModels[modelKey] = generateModelConfig(envConfig);
  });
  
  return dynamicModels;
}

配置验证

typescript
// 后缀名验证
const SUFFIX_PATTERN = /^[a-zA-Z0-9_-]+$/;
const MAX_SUFFIX_LENGTH = 50;

if (!suffix || suffix.length > MAX_SUFFIX_LENGTH || !SUFFIX_PATTERN.test(suffix)) {
  console.warn(`Invalid suffix: ${suffix}`);
  return;
}

// 配置完整性验证
if (!envConfig.apiKey) {
  console.warn(`Missing API key for ${suffix}`);
  return;
}