Files
qiming/qimingclaw/crates/agent-electron-client/docs/v2/02-MODEL-CONFIG.md

15 KiB
Raw Permalink Blame History

version, last-updated, status
version last-updated status
2.0 2026-03-09 design

02 — 模型配置与多 Provider 管理

一、现状分析

V1 现有实现

当前通过 EngineManager + AgentConfig 支持两种引擎(claude-code / qimingcode 配置项散落在 EngineStartConfigAgentInitConfigsettings DB 表中。

Qiming 服务端已有能力

服务器已有完整的模型管理 APIworkspace/qiming/src/services/modelConfig.ts

端点 能力
POST /api/model/save 新增/更新模型配置
POST /api/model/list 查询可用模型列表
POST /api/model/test-connectivity 测试模型连通性
GET /api/model/{id} 查询指定模型配置
GET /api/model/{id}/delete 删除模型配置
GET /api/model/list/space/{spaceId} 查询空间下模型列表

目标:客户端的模型配置与服务器双向同步,服务器为权威来源。


二、设计方案

2.1 Provider 抽象

/** 模型提供商 */
interface ModelProvider {
  id: string; // 'openai' | 'anthropic' | 'google' | 'local' | 自定义
  name: string; // 显示名称
  type: ProviderType; // 'openai-compat' | 'anthropic' | 'acp-engine' | 'ollama'
  enabled: boolean;
  isDefault: boolean;

  /** 服务器同步信息 */
  serverId?: number; // 服务器端模型配置 ID来自 /api/model/save
  syncedAt?: number; // 上次同步时间
  syncSource: "local" | "server"; // 来源(本地创建 or 服务器拉取)

  connection: {
    baseUrl: string; // API 端点
    apiKey?: string; // 会被加密存储
    timeout?: number;
    maxRetries?: number;
    headers?: Record<string, string>;
  };

  models: ModelEntry[];
  capabilities: ProviderCapability[];
}

type ProviderType =
  | "openai-compat" // OpenAI 兼容 API (GPT, DeepSeek, Qwen, GLM, Moonshot 等)
  | "anthropic" // Claude API
  | "acp-engine" // ACP 协议引擎(现有 claude-code / qimingcode
  | "ollama" // 本地 Ollama
  | "custom";

type ProviderCapability =
  | "chat"
  | "streaming"
  | "function-calling"
  | "vision"
  | "code-execution"
  | "file-access"
  | "embedding"; // 文本嵌入向量化

/** 模型条目 */
interface ModelEntry {
  id: string; // 'gpt-4o' | 'claude-sonnet-4-20250514'
  name: string;
  contextWindow?: number;
  maxOutputTokens?: number;
  pricing?: { inputPer1k: number; outputPer1k: number; currency: string };
  isDefault?: boolean;
}

2.2 Provider 预设

系统内置常见 Provider 预设,用户只需填入 API Key

const BUILT_IN_PRESETS: Partial<ModelProvider>[] = [
  {
    id: "openai",
    name: "OpenAI",
    type: "openai-compat",
    connection: { baseUrl: "https://api.openai.com/v1" },
    models: [
      { id: "gpt-4o", name: "GPT-4o" },
      { id: "gpt-4o-mini", name: "GPT-4o Mini" },
      { id: "o3-mini", name: "o3 Mini" },
    ],
  },
  {
    id: "anthropic",
    name: "Anthropic",
    type: "anthropic",
    connection: { baseUrl: "https://api.anthropic.com" },
    models: [
      { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
      { id: "claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku" },
    ],
  },
  {
    id: "deepseek",
    name: "DeepSeek",
    type: "openai-compat",
    connection: { baseUrl: "https://api.deepseek.com/v1" },
    models: [
      { id: "deepseek-chat", name: "DeepSeek Chat" },
      { id: "deepseek-reasoner", name: "DeepSeek R1" },
    ],
  },
  {
    id: "qwen",
    name: "通义千问 (Qwen)",
    type: "openai-compat",
    connection: {
      baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
    },
    models: [
      { id: "qwen-max", name: "Qwen Max" },
      { id: "qwen-plus", name: "Qwen Plus" },
      { id: "qwen3:32b", name: "Qwen3 32B" },
    ],
  },
  {
    id: "glm",
    name: "智谱 (GLM)",
    type: "openai-compat",
    connection: { baseUrl: "https://open.bigmodel.cn/api/paas/v4" },
    models: [
      { id: "glm-4", name: "GLM-4" },
      { id: "glm-4-flash", name: "GLM-4 Flash" },
      { id: "glm-4.7-anthropic", name: "GLM-4.7 Anthropic" },
    ],
  },
  {
    id: "ollama",
    name: "Ollama (Local)",
    type: "ollama",
    connection: { baseUrl: "http://localhost:11434" },
    models: [], // 运行时从 Ollama API 动态获取
    capabilities: ["chat", "streaming", "embedding"],
  },
  {
    id: "acp-claude",
    name: "Claude Code (ACP)",
    type: "acp-engine",
    capabilities: ["chat", "streaming", "code-execution", "file-access"],
  },
  {
    id: "acp-qimingcode",
    name: "QimingCode (ACP)",
    type: "acp-engine",
    capabilities: ["chat", "streaming", "code-execution", "file-access"],
  },
];

// Embedding 模型预设(服务器已有 text-embedding 模型,用于知识库/记忆向量化)
const EMBEDDING_PRESETS: Partial<ModelProvider>[] = [
  {
    id: "openai-embedding",
    name: "OpenAI Embedding",
    type: "openai-compat",
    connection: { baseUrl: "https://api.openai.com/v1" },
    models: [
      { id: "text-embedding-3-small", name: "Embedding 3 Small" },
      { id: "text-embedding-3-large", name: "Embedding 3 Large" },
    ],
    capabilities: ["embedding"],
  },
];

2.3 凭据安全存储

使用 Electron 内置的 safeStorage API 加密凭据:

import { safeStorage } from "electron";

class ElectronCredentialStore implements CredentialStore {
  async set(providerId: string, key: string, value: string): Promise<void> {
    const encrypted = safeStorage.encryptString(value);
    db.prepare(
      "INSERT OR REPLACE INTO credentials (provider_id, key, value) VALUES (?, ?, ?)",
    ).run(providerId, key, encrypted);
  }

  async get(providerId: string, key: string): Promise<string | null> {
    const row = db
      .prepare(
        "SELECT value FROM credentials WHERE provider_id = ? AND key = ?",
      )
      .get(providerId, key) as { value: Buffer } | undefined;
    if (!row) return null;
    return safeStorage.decryptString(row.value);
  }
}

三、与服务器同步

3.1 同步流程

┌─────────────┐                        ┌──────────────────┐
│  QimingClaw   │                        │  Qiming Agent OS  │
│  Desktop    │                        │  (用户部署)       │
└──────┬──────┘                        └──────┬───────────┘
       │                                      │
       │  ──── 启动 / 定时 / 手动触发同步 ────    │
       │                                      │
       │  POST /api/model/list                 │
       │  { spaceId }                          │
       │ ────────────────────────────────────→ │
       │                                      │
       │  ← ModelConfigInfo[]                  │
       │ ←──────────────────────────────────── │
       │                                      │
       │  合并本地 Provider                    │
       │  • 服务器有/本地无 → 拉取创建         │
       │  • 服务器有/本地有 → 以服务器为准      │
       │  • 仅本地有 → 保留或推送到服务器      │
       │                                      │
       │  POST /api/model/save  (推送本地新增)  │
       │ ────────────────────────────────────→ │
       │                                      │

3.2 ModelSyncAdapter

class ModelSyncAdapter {
  constructor(
    private qimingClient: QimingApiClient,
    private modelGateway: ModelGateway,
    private credentialStore: CredentialStore,
  ) {}

  /** 从服务器拉取并合并模型配置 */
  async pull(): Promise<SyncResult> {
    const spaceId = this.qimingClient.getSpaceId();
    const serverModels = await this.qimingClient.request<ModelConfigInfo[]>(
      "POST",
      `/api/model/list`,
      { spaceId },
    );

    let pulled = 0;
    for (const sm of serverModels) {
      const localProvider = this.modelGateway.findByServerId(sm.id);

      if (!localProvider) {
        // 服务器有,本地无 → 创建本地 Provider
        await this.modelGateway.upsertProvider(this.mapServerToLocal(sm));
        pulled++;
      } else if (sm.updatedAt > localProvider.syncedAt!) {
        // 服务器更新 → 以服务器为准
        await this.modelGateway.upsertProvider({
          ...localProvider,
          ...this.mapServerToLocal(sm),
        });
        pulled++;
      }
    }

    return { success: true, pulled, pushed: 0, conflicts: [] };
  }

  /** 推送本地新增/修改到服务器 */
  async push(): Promise<SyncResult> {
    const localOnly = this.modelGateway
      .listProviders()
      .filter((p) => p.syncSource === "local" && !p.serverId);

    let pushed = 0;
    for (const lp of localOnly) {
      const apiKey = await this.credentialStore.get(lp.id, "apiKey");
      await this.qimingClient.request("POST", "/api/model/save", {
        baseUrl: lp.connection.baseUrl,
        apiKey,
        modelId: lp.models[0]?.id,
        name: lp.name,
        // ... 映射到 ModelSaveParams
      });
      pushed++;
    }

    return { success: true, pulled: 0, pushed, conflicts: [] };
  }

  /** 测试连通性(可以走服务器或本地直连) */
  async testConnection(providerId: string): Promise<{ success: boolean }> {
    const provider = this.modelGateway.getProvider(providerId);

    if (provider.serverId) {
      // 通过服务器测试
      return this.qimingClient.request("POST", "/api/model/test-connectivity", {
        modelId: provider.serverId,
      });
    } else {
      // 本地直连测试
      return this.modelGateway.testConnection(providerId);
    }
  }
}

四、ModelGateway 服务

class ModelGateway {
  private providers: Map<string, ModelProvider> = new Map();
  private credentialStore: CredentialStore;
  private syncAdapter: ModelSyncAdapter;

  /** 获取所有已配置 Provider */
  listProviders(): ModelProvider[];

  /** 添加/更新 Provider */
  upsertProvider(provider: ModelProvider): Promise<void>;

  /** 删除 Provider */
  deleteProvider(id: string): Promise<void>;

  /** 获取默认 Provider + Model */
  getDefault(): { provider: ModelProvider; model: ModelEntry } | null;

  /** 测试连接 */
  testConnection(providerId: string): Promise<{
    success: boolean;
    latencyMs?: number;
    models?: ModelEntry[];
    error?: string;
  }>;

  /** 动态获取模型列表 */
  fetchModels(providerId: string): Promise<ModelEntry[]>;

  /** 创建 Chat 客户端(返回统一接口) */
  createChatClient(providerId: string, modelId: string): ChatClient;

  /** 与服务器同步 */
  syncWithServer(): Promise<SyncResult>;
}

ChatClient 统一接口

interface ChatClient {
  chatStream(
    messages: ChatMessage[],
    options?: ChatOptions,
  ): AsyncIterable<ChatChunk>;
  chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse>;
  abort(): void;
}

interface ChatChunk {
  type: "text" | "tool_call" | "tool_result" | "reasoning" | "done" | "error";
  text?: string;
  toolCall?: { id: string; name: string; arguments: string };
  usage?: { inputTokens: number; outputTokens: number };
}

五、IPC 接口设计

// Provider CRUD
'provider:list'         ModelProvider[]
'provider:get'          ModelProvider
'provider:upsert'       { success: boolean }
'provider:delete'       { success: boolean }
'provider:test'         { success: boolean; latencyMs?: number; error?: string }
'provider:fetchModels'  ModelEntry[]
'provider:setDefault'   { success: boolean }
'provider:getDefault'   { provider: ModelProvider; model: ModelEntry } | null

// 同步
'provider:sync'         SyncResult
'provider:syncStatus'   SyncStatus

六、UI 设计要点

Provider 设置页

┌─ 模型配置 ──────────────────────────────────────────────┐
│                                                          │
│  服务器: https://testagent.xspaceagi.com  [✓ 已连接]     │
│  上次同步: 2 分钟前  [立即同步]                           │
│                                                          │
│  ┌─────────────────────────────────┐  ┌──── 添加 ──┐   │
│  │ ● OpenAI        ✓ 已连接  [默认] │  │ + 添加 Provider │
│  │ ○ Anthropic     ✓ 已连接  [云端] │  └────────────┘   │
│  │ ○ DeepSeek      ✗ 未配置  [云端] │                    │
│  │ ○ Ollama        ✓ 3 个模型 [本地] │                    │
│  └─────────────────────────────────┘                    │
│                                                          │
│  ── OpenAI 配置 ──────────────────                       │
│  来源:  ☁ 来自服务器 / 🖥 本地新增                        │
│  API Key:     [••••••••••••••••]  [测试连接]             │
│  Base URL:    [https://api.openai.com/v1  ]              │
│  默认模型:    [gpt-4o         ▾]                         │
│                                                          │
│  ── 可用模型 ──────────────────────                      │
│  │ gpt-4o        │ 128K │ $2.50/M │ ★ 默认              │
│  │ gpt-4o-mini   │ 128K │ $0.15/M │                     │
│           [刷新模型列表]                                   │
└──────────────────────────────────────────────────────────┘

七、迁移策略

从 V1 迁移

  1. 读取 V1 settings 表中的 apiKeybaseUrlmodel
  2. 自动创建对应 Provider 配置
  3. 将明文 API Key 迁移到 safeStorage 加密存储
  4. 删除 settings 中的明文凭据
  5. 连接到 Qiming server 后,首次同步拉取服务器模型配置

相关文档