Skip to content

Latest commit

 

History

History
1929 lines (1498 loc) · 70.5 KB

File metadata and controls

1929 lines (1498 loc) · 70.5 KB

aioncli 对 Gemini CLI 的魔改分析报告

本报告深入分析 aioncli 项目对原版 gemini-cli 的改造内容、设计思路、建设历程及 AI Native 工程实践。


目录


一、核心魔改内容总结

1. 多模型 API 兼容层(最核心改动)

新增文件: packages/core/src/core/openaiContentGenerator.ts (2000+ 行)

原版 Gemini CLI aioncli 魔改版
仅支持 Gemini API 支持 Gemini + OpenAI + DeepSeek + Qwen + 任意兼容 API
单一认证方式 新增 AuthType.USE_OPENAI 认证类型
无格式转换 完整的 Gemini ↔ OpenAI 格式双向转换

关键技术实现:

// 1. 请求格式转换:Gemini → OpenAI
convertToOpenAIFormat(request: GenerateContentParameters): OpenAI.Chat.ChatCompletionMessageParam[]

// 2. 响应格式转换:OpenAI → Gemini
convertToGeminiFormat(openaiResponse): GenerateContentResponse

// 3. 工具 Schema 转换(处理各种 API 差异)
convertGeminiParametersToOpenAI(parameters): Record<string, unknown>

2. DeepSeek Reasoner 特殊适配

提交: 3624e63b - DeepSeek API 兼容性修复

// 检测推理模型
private isDeepSeekReasonerModel(): boolean {
  return modelName.includes('deepseek-reasoner') || modelName.includes('deepseek-r1');
}

// 为推理模型添加必需字段
private addReasoningContentForDeepSeek(messages) {
  return messages.map(msg =>
    msg.role === 'assistant' ? { ...msg, reasoning_content: '' } : msg
  );
}

3. 工具调用去重与清理机制

提交: 699e49f2 - 修复 OpenAI 兼容 API 错误

// 解决的问题:某些 API 不接受重复的 tool_call_id
private cleanOrphanedToolCalls(messages): OpenAI.Chat.ChatCompletionMessageParam[] {
  // 1. 收集所有 tool_call_id
  // 2. 去重相同 id 的响应
  // 3. 清理孤立的工具调用(无对应响应)
}

4. 多厂商 Header 适配

// OpenRouter 特殊头
if (baseURL.includes('openrouter.ai')) {
  headers['HTTP-Referer'] = 'https://aionui.com';
  headers['X-Title'] = 'AionUi';
}

// 阿里 DashScope 支持
shouldIncludeMetadata(): boolean {
  return hostname === 'api.openai.com' || hostname === 'dashscope.aliyuncs.com';
}

5. 新增的 ContentGenerator 实现

// packages/core/src/core/contentGenerator.ts
export interface ContentGenerator {
  generateContent(request, userPromptId): Promise<GenerateContentResponse>;
  generateContentStream(
    request,
    userPromptId,
  ): Promise<AsyncGenerator<GenerateContentResponse>>;
  countTokens(request): Promise<CountTokensResponse>;
  embedContent(request): Promise<EmbedContentResponse>;
}

// 实现类:
// 1. GeminiContentGenerator    - 原始 Gemini API(通过 GoogleGenAI SDK)
// 2. OpenAIContentGenerator    - OpenAI/兼容 API ⭐ 新增
// 3. LoggingContentGenerator   - 包装器(日志记录)
// 4. RecordingContentGenerator - 包装器(会话录制)
// 5. FakeContentGenerator      - 测试用途
// 6. CodeAssistServer          - Google Code Assist 服务

二、设计思路分析

架构设计原则

┌─────────────────────────────────────────────────────────┐
│                    CLI Interface                         │
│                   (packages/cli)                         │
├─────────────────────────────────────────────────────────┤
│                 ContentGenerator 接口                    │
│              (统一的内容生成抽象层)                       │
├──────────────┬──────────────┬──────────────────────────┤
│ Gemini SDK   │ OpenAI SDK   │  其他兼容 API             │
│ (原版实现)    │ (新增实现)   │  (可扩展)                │
│              │              │                          │
│ - Google AI  │ - OpenAI     │ - 自建服务               │
│ - Vertex AI  │ - DeepSeek   │ - 其他 LLM              │
│ - Code Assist│ - Qwen       │                          │
│              │ - OpenRouter │                          │
└──────────────┴──────────────┴──────────────────────────┘

核心设计思路:

  1. 接口抽象: 通过 ContentGenerator 接口统一所有模型调用,上层业务代码无需关心底层使用的是哪个 API
  2. 适配器模式: 在适配层做格式转换,保持 Gemini 格式作为内部标准
  3. 环境变量驱动: 通过 OPENAI_BASE_URL + OPENAI_API_KEY 切换后端,零代码修改
  4. 渐进式兼容: 保持与上游 gemini-cli 的合并能力,最小化侵入式修改

为什么这么做?

| 动机 | 解决方案 | 价值 | |------|----------|------|1 | 成本控制 | 支持 DeepSeek 等低成本 API | 降低 90%+ API 成本 | | 模型选择自由 | 用户可选择最适合的模型 | 灵活应对不同场景 | | 私有化部署 | 支持自建 OpenAI 兼容服务 | 满足企业安全合规 | | 快速跟进新模型 | 新模型只需配置 URL 即可使用 | 保持技术领先 | | 保持上游同步 | 最小化侵入式修改 | 持续获得社区更新 |

关键设计决策

// 决策 1: 动态导入避免循环依赖
if (config.authType === AuthType.USE_OPENAI) {
  const { OpenAIContentGenerator } = await import(
    './openaiContentGenerator.js'
  );
  return new OpenAIContentGenerator(config.apiKey, config.model, gcConfig);
}

// 决策 2: 保持 Gemini 格式作为内部标准
// 所有 OpenAI 响应都转换为 Gemini 格式,上层代码无需修改

// 决策 3: 通过子类扩展而非修改
export class OpenAIContentGenerator implements ContentGenerator {
  // 完全独立的实现,不修改原有代码
}

三、从 0 到 1 的建设历程

根据 Git 历史分析,项目经历了以下阶段:

Phase 1: 基础 OpenAI 支持 (PR #5-7)

目标:证明可行性,跑通基本流程

关键提交:
- feat: Add OpenAI support for generateJson functionality
- feat: Improve JSON parsing robustness for OpenAI tool calls
- chore: Exclude OpenAI files from license header check

成果:
✅ 基础的 OpenAI 内容生成
✅ JSON 工具调用支持
✅ 独立的代码结构

Phase 2: 上游合并 + 功能增强 (PR #8-12)

目标:与上游同步,增加生产级功能

关键提交:
- feat: Add API key rotation support for rate limit handling
- feat: Add JSON schema support for OpenAI content generator
- feat: Complete upstream v0.2.2 merge with OpenAI features preserved

成果:
✅ API Key 轮换(应对速率限制)
✅ JSON Schema 支持
✅ 成功合并上游 v0.2.2

Phase 3: 大版本升级 (PR #14-15)

目标:跟上上游快速迭代,保持功能同步

关键提交:
- Merge tag 'v0.18.4'
- fix: Resolve type errors after v0.18.4 merge
- test: Add verification suite for OpenAI and Zed integration

成果:
✅ 成功合并 v0.8.1 → v0.18.4(跨越多个大版本)
✅ 修复类型错误
✅ 添加集成测试验证

Phase 4: 深度兼容优化 (PR #16, 当前)

目标:解决生产环境中遇到的各种边界问题

关键提交:
- fix: Fix DeepSeek API compatibility by converting type: null to type: 'object'
- fix: Fix OpenAI-compatible API errors by deduplicating tool responses

成果:
✅ DeepSeek Reasoner 模型完全兼容
✅ 工具调用去重机制
✅ 数组类型参数处理

版本演进时间线

v0.1.x  ──→  v0.2.2  ──→  v0.8.1  ──→  v0.18.4  ──→  v0.18.5
   │           │           │            │            │
   └─ Phase 1  └─ Phase 2  └─ Phase 3   └────────────┘
      基础支持    功能增强     大版本升级      Phase 4
                                          深度兼容

四、产品方向与节奏把控

产品定位

"多模型兼容的企业级 AI Coding CLI"

核心价值主张:

  • 保持 Gemini CLI 的所有优秀特性
  • 增加模型选择的自由度
  • 降低 API 使用成本
  • 支持企业私有化部署

节奏策略

阶段 重点 时间投入 状态
MVP OpenAI 基础支持 1-2 周 ✅ 完成
稳定 上游同步 + 测试覆盖 持续 ✅ 完成
扩展 多厂商适配 (DeepSeek/Qwen) 1 周 ✅ 完成
优化 错误处理 + 边界情况 持续 🔄 进行中
未来 更多模型 + 企业功能 规划中 📋 待启动

关键产品决策

决策 选择 原因
Fork vs 插件 Fork 需要深度修改核心逻辑
修改原文件 vs 新增文件 新增文件 保持上游可合并性
自定义配置格式 vs 环境变量 环境变量 简单、通用、易于容器化
完全重写 vs 适配器 适配器 复用原有逻辑,降低维护成本

上游同步策略

# 定期同步上游更新
git fetch upstream
git merge upstream/main

# 解决冲突优先级:
# 1. 保留 aioncli 的 OpenAI 相关改动
# 2. 采用上游的通用改进
# 3. 必要时重写冲突部分

五、难点与挑战

1. API 格式差异处理

问题描述:Gemini 和 OpenAI 的工具 Schema 格式存在根本差异。

// Gemini 允许的格式:
{
  type: null;
}
{
  type: ['object', 'null'];
}

// OpenAI 要求的格式:
{
  type: 'object';
} // 必须是单一字符串

解决方案

private convertGeminiParametersToOpenAI(parameters): Record<string, unknown> {
  const convertTypes = (obj: unknown): unknown => {
    if (key === 'type') {
      // 处理 type: null
      if (value === null || value === undefined) {
        result[key] = 'object';
      }
      // 处理数组类型 ["object", "null"]
      else if (Array.isArray(value)) {
        const primaryType = value.find(t => t !== 'null');
        result[key] = primaryType || 'object';
      }
    }
    // ... 递归处理嵌套对象
  };
}

2. 流式响应中的工具调用处理

问题描述:OpenAI 流式响应中,tool_calls 是分块传输的,需要累积后才能使用。

// 流式块示例:
{
  delta: {
    tool_calls: [{ index: 0, function: { arguments: '{"pat' } }];
  }
}
{
  delta: {
    tool_calls: [{ index: 0, function: { arguments: 'h": "' } }];
  }
}
{
  delta: {
    tool_calls: [{ index: 0, function: { arguments: '/src"}' } }];
  }
}

解决方案

private streamingToolCalls: Map<number, {
  id?: string;
  name?: string;
  arguments: string;  // 累积的参数字符串
}> = new Map();

private async *streamGenerator(stream): AsyncGenerator<GenerateContentResponse> {
  for await (const chunk of stream) {
    // 累积工具调用参数
    if (chunk.choices?.[0]?.delta?.tool_calls) {
      for (const toolCall of chunk.choices[0].delta.tool_calls) {
        const accumulated = this.streamingToolCalls.get(toolCall.index) || { arguments: '' };
        accumulated.arguments += toolCall.function?.arguments || '';
        this.streamingToolCalls.set(toolCall.index, accumulated);
      }
    }

    // 只在 finish_reason 出现时发射完整的 functionCall
    if (chunk.choices?.[0]?.finish_reason) {
      for (const [, call] of this.streamingToolCalls) {
        parts.push({ functionCall: { name: call.name, args: JSON.parse(call.arguments) } });
      }
      this.streamingToolCalls.clear();
    }
  }
}

3. 上游合并冲突处理

问题描述:gemini-cli 更新频繁(周更),每次合并都可能有冲突。

解决策略

1. 最小化对原文件的修改
   - 只在 contentGenerator.ts 添加一个 import 和一个 if 分支
   - 所有新逻辑放在独立的 openaiContentGenerator.ts

2. 使用动态 import 避免编译时依赖
   const { OpenAIContentGenerator } = await import('./openaiContentGenerator.js');

3. 保持文件结构与上游一致
   - 不重命名文件
   - 不移动目录结构
   - 不修改导出接口

4. 冲突解决优先级
   - OpenAI 相关代码:保留 aioncli 版本
   - 通用逻辑:采用上游版本
   - 接口变更:适配后保留两边功能

4. 多厂商 API 行为差异

厂商 特殊行为 处理方式
DeepSeek Reasoner 模型需要 reasoning_content 字段 检测模型名,自动添加空字段
DeepSeek 不接受 type: null 转换为 type: 'object'
OpenRouter 需要 HTTP-RefererX-Title Header 检测 URL,自动添加
DashScope 支持 metadata 字段 检测 hostname,条件添加
通用问题 重复 tool_call_id 导致 400 错误 去重清理机制
通用问题 孤立的 tool_calls(无对应响应) 清理孤立消息

5. 超时与错误处理

// 问题:长时间请求可能超时,需要友好的错误提示
private isTimeoutError(error: unknown): boolean {
  const errorMessage = error instanceof Error ? error.message.toLowerCase() : '';
  return (
    errorMessage.includes('timeout') ||
    errorMessage.includes('etimedout') ||
    errorCode === 'ETIMEDOUT' ||
    errorCode === 'ESOCKETTIMEDOUT'
  );
}

// 解决方案:提供具体的排查建议
if (isTimeoutError) {
  throw new Error(
    `${errorMessage}\n\nTroubleshooting tips:\n` +
    `- Reduce input length or complexity\n` +
    `- Increase timeout in config: contentGenerator.timeout\n` +
    `- Check network connectivity\n` +
    `- Consider using streaming mode for long responses`
  );
}

六、AI Native 工程实践

1. AI Coding 工具使用场景

场景 推荐工具 使用方式
格式转换函数生成 Claude / GPT-4 提供两端格式示例,生成转换逻辑
边界测试用例 AI 辅助 基于代码生成测试用例
错误处理模板 Copilot 自动补全常见错误处理
双语注释 AI 翻译 保持中英文注释同步
API 文档理解 Claude 快速理解新 API 的差异

2. 代码中的 AI Native 痕迹

// 典型的 AI 辅助风格:双语注释
// 处理 type: null - 转换为 'object' 以兼容 DeepSeek
// Handle type: null - convert to 'object' for DeepSeek compatibility

// 追踪已添加的工具响应,用于去重
// Track tool responses we've already added to deduplicate
const addedToolResponseIds = new Set<string>();

3. AI 辅助开发工作流

┌──────────────────────────────────────────────────────────┐
│                    开发工作流                             │
├──────────────────────────────────────────────────────────┤
│  1. 需求分析                                              │
│     └─ AI 帮助理解 API 文档差异                           │
│                                                          │
│  2. 设计阶段                                              │
│     └─ AI 生成初始架构方案                                │
│                                                          │
│  3. 编码实现                                              │
│     ├─ AI 生成转换函数骨架                                │
│     ├─ 人工 Review 和调整                                 │
│     └─ AI 补充边界处理                                    │
│                                                          │
│  4. 测试阶段                                              │
│     ├─ AI 生成测试用例                                    │
│     └─ 人工验证覆盖率                                     │
│                                                          │
│  5. 问题修复                                              │
│     ├─ 分析错误日志                                       │
│     ├─ AI 生成修复代码                                    │
│     └─ 回归测试验证                                       │
└──────────────────────────────────────────────────────────┘

4. 效率提升数据(估算)

任务类型 传统方式 AI 辅助 提效比例
API 格式转换 4 小时 1 小时 75%
测试用例编写 2 小时 30 分钟 75%
错误处理完善 2 小时 45 分钟 62%
文档注释 1 小时 15 分钟 75%
代码审查 1 小时 30 分钟 50%

5. AI Native 特征体现

特征 在项目中的体现
快速迭代 12 天内完成 DeepSeek 完全适配(PR #16)
模式识别 统一的错误处理模式,统一的格式转换模式
代码质量 完善的 TypeScript 类型定义
测试覆盖 openaiContentGenerator.test.ts 1500+ 行测试
文档同步 代码注释与实现保持同步

6. 推荐的 AI 工具组合

日常开发:
├── Claude Code (本项目)     # 代码理解、生成、重构
├── GitHub Copilot           # 代码补全
└── Cursor                   # IDE 集成

代码审查:
├── Claude                   # 深度代码分析
└── GPT-4                    # 安全审查

文档生成:
├── Claude                   # 技术文档
└── 翻译工具                 # 双语支持

七、关键代码位置速查

核心文件

功能 文件路径
OpenAI 适配器 packages/core/src/core/openaiContentGenerator.ts
内容生成接口 packages/core/src/core/contentGenerator.ts
模型配置 packages/core/src/config/models.ts
默认模型配置 packages/core/src/config/defaultModelConfigs.ts

Agent 系统

功能 文件路径
Agent 执行器 packages/core/src/agents/executor.ts
Agent 注册表 packages/core/src/agents/registry.ts
Agent 类型定义 packages/core/src/agents/types.ts

工具系统

功能 文件路径
工具注册表 packages/core/src/tools/tool-registry.ts
MCP 客户端 packages/core/src/tools/mcp-client.ts
工具名称常量 packages/core/src/tools/tool-names.ts

遥测与日志

功能 文件路径
API 响应日志 packages/core/src/telemetry/loggers.ts
遥测类型 packages/core/src/telemetry/types.ts
UI 遥测 packages/core/src/telemetry/uiTelemetry.ts

测试文件

功能 文件路径
OpenAI 适配器测试 packages/core/src/core/openaiContentGenerator.test.ts
内容生成器测试 packages/core/src/core/contentGenerator.test.ts

八、总结

魔改策略总结

aioncli 的魔改策略可以概括为 "适配器模式 + 最小侵入"

核心原则:
┌────────────────────────────────────────────┐
│  1. 新增文件 > 修改文件                      │
│  2. 接口抽象 > 硬编码                        │
│  3. 环境变量 > 配置文件                      │
│  4. 保持可合并 > 深度定制                    │
└────────────────────────────────────────────┘

技术亮点

  1. 一个文件解决核心问题:2000 行的 OpenAIContentGenerator 实现所有 OpenAI 兼容逻辑
  2. 零侵入式扩展:原有代码几乎不需要修改
  3. 完善的边界处理:考虑了各种 API 差异和错误情况
  4. 可持续维护:保持与上游的同步能力

工程价值

维度 价值
成本 支持低成本 API,降低 90%+ 费用
灵活性 用户自由选择最合适的模型
可扩展 新增 API 支持只需少量代码
可维护 与上游保持同步,持续获得更新

AI Native 实践总结

这是一个很好的 "站在巨人肩膀上" + "AI 辅助开发" 的开源项目实践案例:

  1. 善用 AI 工具:加速开发,提升代码质量
  2. 保持人工把控:关键决策、架构设计、质量审查
  3. 快速迭代验证:小步快跑,持续交付
  4. 注重可维护性:代码结构清晰,文档完善

附录:环境变量配置

# OpenAI 兼容 API 配置
export OPENAI_API_KEY="your-api-key"
export OPENAI_BASE_URL="https://api.openai.com/v1"  # 或其他兼容 API

# DeepSeek 配置示例
export OPENAI_API_KEY="your-deepseek-key"
export OPENAI_BASE_URL="https://api.deepseek.com/v1"

# 阿里 Qwen (DashScope) 配置示例
export OPENAI_API_KEY="your-dashscope-key"
export OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"

# OpenRouter 配置示例
export OPENAI_API_KEY="your-openrouter-key"
export OPENAI_BASE_URL="https://openrouter.ai/api/v1"

九、aioncli 与 qwen-code 的对比分析

aioncli 在开发过程中借鉴了 qwen-code 的部分实现。本节详细分析两个项目的关系和差异。

1. 项目关系图谱

┌─────────────────┐
│   gemini-cli    │  Google 官方 Gemini CLI
│   (上游源头)     │
└────────┬────────┘
         │
    ┌────┴────┐
    │  Fork   │
    ▼         ▼
┌─────────┐  ┌─────────┐
│qwen-code│  │ aioncli │
│(阿里官方)│  │(本项目) │
└────┬────┘  └────┬────┘
     │            │
     └─────┬──────┘
           │
      借鉴参考

2. 核心借鉴内容

2.1 JSON Schema 转工具调用的方案

qwen-code 的原创方案:将 generateJson 请求转换为工具调用

// qwen-code 的注释出现在 aioncli 代码中
// packages/core/src/core/openaiContentGenerator.ts:262
// Convert JSON schema request to tool call (like qwen-code approach)
const jsonSchemaFunction = {
  type: 'function' as const,
  function: {
    name: 'respond_in_schema',
    description: 'Provide the response in the specified JSON schema format',
    parameters: request.config.responseJsonSchema,
  },
};

借鉴原因:OpenAI API 不原生支持 Gemini 的 responseJsonSchema,qwen-code 创造性地用工具调用模拟此功能。

2.2 工具调用清理机制 (cleanOrphanedToolCalls)

两个项目都实现了几乎相同的孤立工具调用清理逻辑:

功能 qwen-code aioncli
收集 tool_call_id
去重重复响应 ✅ (增强版)
清理孤立调用
合并连续 assistant 消息

aioncli 的增强:增加了 addedToolResponseIds 去重逻辑,解决某些 API 不接受重复 tool_call_id 的问题。

2.3 流式工具调用累积器

// 两个项目都使用相同的累积器模式
interface ToolCallAccumulator {
  id?: string;
  name?: string;
  arguments: string; // 累积的 JSON 字符串
}

qwen-code: 使用独立的 StreamingToolCallParseraioncli: 内联在 OpenAIContentGenerator 中使用 Map<number, ToolCallAccumulator>

2.4 格式转换函数签名

函数 qwen-code aioncli
Gemini → OpenAI 请求 convertGeminiRequestToOpenAI() convertToOpenAIFormat()
OpenAI → Gemini 响应 convertOpenAIResponseToGemini() convertToGeminiFormat()
流式块转换 convertOpenAIChunkToGemini() convertStreamChunkToGeminiFormat()
工具参数转换 convertGeminiToolParametersToOpenAI() convertGeminiParametersToOpenAI()

3. 架构差异对比

3.1 代码组织方式

维度 qwen-code aioncli
文件结构 分离式(多文件模块) 单文件集成
转换器 独立 OpenAIContentConverter 内嵌在 OpenAIContentGenerator
Pipeline 独立 ContentGenerationPipeline 直接在生成器中处理
Provider 抽象 Provider 接口 + 多实现 单一实现 + 环境变量
遥测 独立 TelemetryService 复用原有遥测系统

qwen-code 的目录结构

openaiContentGenerator/
├── openaiContentGenerator.ts    # 主类(精简)
├── converter.ts                 # 格式转换(1100+ 行)
├── pipeline.ts                  # 执行流水线
├── provider/                    # 多 Provider 支持
│   ├── index.ts
│   ├── dashscope.ts            # 阿里 DashScope
│   └── openrouter.ts           # OpenRouter
├── streamingToolCallParser.ts   # 流式解析器
├── telemetryService.ts          # 遥测服务
└── errorHandler.ts              # 错误处理

aioncli 的结构

core/
├── openaiContentGenerator.ts    # 全部集成(2000+ 行)
└── contentGenerator.ts          # 接口定义

3.2 设计哲学对比

方面 qwen-code aioncli
复杂度 高(更多抽象层) 低(直接实现)
可扩展性 强(Provider 接口) 中(需修改代码)
维护成本 较高 较低
上游同步 困难(改动大) 容易(改动小)
学习曲线 陡峭 平缓

4. aioncli 独有的增强

4.1 DeepSeek Reasoner 支持

// aioncli 独有:DeepSeek 推理模型适配
private isDeepSeekReasonerModel(): boolean {
  return modelName.includes('deepseek-reasoner') || modelName.includes('deepseek-r1');
}

private addReasoningContentForDeepSeek(messages) {
  // 为 assistant 消息添加 reasoning_content 字段
}

4.2 数组类型参数处理

// aioncli 独有:处理 ["object", "null"] 类型数组
if (Array.isArray(value)) {
  const primaryType = value.find((t) => t !== 'null');
  result[key] = primaryType || 'object';
}

4.3 工具响应去重

// aioncli 增强:防止重复 tool_call_id
const addedToolResponseIds = new Set<string>();
if (!addedToolResponseIds.has(message.tool_call_id)) {
  cleaned.push(message);
  addedToolResponseIds.add(message.tool_call_id);
}

4.4 超时错误处理

// aioncli 独有:详细的超时错误提示
if (isTimeoutError) {
  throw new Error(
    `${errorMessage}\n\nTroubleshooting tips:\n` +
      `- Reduce input length or complexity\n` +
      `- Increase timeout in config\n` +
      `- Check network connectivity`,
  );
}

5. qwen-code 独有的特性

5.1 Provider 抽象层

// qwen-code: 可插拔的 Provider 架构
interface OpenAICompatibleProvider {
  buildClient(): OpenAI;
  getModelName(): string;
}

// 实现:DashScopeProvider, OpenRouterProvider 等

5.2 Qwen OAuth 认证

// qwen-code: 专门的 Qwen OAuth 支持
class QwenContentGenerator extends OpenAIContentGenerator {
  private qwenClient: IQwenOAuth2Client;
  private sharedManager: SharedTokenManager;

  // 自动 Token 刷新
  private async getValidToken(): Promise<{ token: string; endpoint: string }>;
}

5.3 多模态内容处理

// qwen-code: 更完善的多模态支持
interface ParsedParts {
  thoughtParts: string[]; // 思考内容
  contentParts: string[]; // 文本内容
  functionCalls: FunctionCall[];
  functionResponses: FunctionResponse[];
  mediaParts: Array<{
    // 媒体内容
    type: 'image' | 'audio' | 'file';
    data: string;
    mimeType: string;
  }>;
}

5.4 Chunk 合并策略

// qwen-code: 处理 finishReason 和 usageMetadata 分离发送的情况
private handleChunkMerging(
  response: GenerateContentResponse,
  collectedResponses: GenerateContentResponse[],
  setPendingFinish: (response: GenerateContentResponse) => void,
): boolean;

6. 借鉴总结

借鉴内容 来源 aioncli 改进
JSON Schema → Tool Call qwen-code 原创 直接复用
cleanOrphanedToolCalls qwen-code 增加去重逻辑
流式工具调用累积 qwen-code 简化实现
格式转换架构 qwen-code 单文件集成
Token 估算 (70/30) qwen-code 直接复用

7. 为什么 aioncli 选择简化架构?

  1. 上游同步优先:单文件修改更容易与 gemini-cli 合并
  2. 维护成本:减少抽象层,降低长期维护负担
  3. 快速迭代:直接修改比接口适配更快
  4. 够用就好:当前场景不需要 Provider 抽象

8. 代码溯源证据

aioncli 代码中保留的 qwen-code 痕迹:

// 1. Copyright 声明
// packages/core/src/core/openaiContentGenerator.ts:3
// Copyright 2025 QWEN

// 2. User-Agent
// packages/core/src/core/openaiContentGenerator.ts:119
const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;

// 3. 注释引用
// packages/core/src/core/openaiContentGenerator.ts:262
// Convert JSON schema request to tool call (like qwen-code approach)

// 4. Token 计数注释
// packages/core/src/core/openaiContentGenerator.ts:718
const encoding = get_encoding('cl100k_base'); // GPT-4 encoding, but estimate for qwen

文档生成时间:2025-12-15 aioncli 版本:0.18.5


十、附录:AionCLI 消息协议详解 (A2A Protocol)

aioncli 通过 Server-Sent Events (SSE) 向客户端推送消息。作为 Headless Agent,它通过 a2a-server 与 VSCode 插件、Web UI 等客户端进行交互。

所有发给用户的消息都遵循统一的 Envelope (信封) 结构,并通过 metadata.coderAgent.kind 字段区分具体的业务类型。目前共有 7 种 核心消息类型。

1. 消息信封结构 (Envelope)

所有流式消息的最外层结构如下:

{
  "kind": "status-update",
  "taskId": "UUID-Task-ID",
  "contextId": "UUID-Context-ID",
  "final": false,
  "status": {
    "state": "working", // 枚举: working, input-required, completed, failed
    "timestamp": "2024-01-01T00:00:00Z",
    "message": {
      // ... 具体的消息 Payload,见下文 ...
    }
  },
  "metadata": {
    "coderAgent": {
      "kind": "..." // 关键字段:用于区分消息类型
    },
    // 其他元数据
    "model": "gemini-2.0-flash-exp",
    "userTier": "..."
  }
}

2. 7 种核心消息类型

类型 (Kind) 用途 关键特征
text-content 普通文本回复 message.parts[0].text 包含内容
tool-call-confirmation 请求用户批准高危操作 包含 confirmationDetails,状态变更为 input-required
tool-call-update 工具执行进度更新 包含 liveOutput 实时流
thought AI 思考过程 (CoT) 通常在 UI 中折叠显示
state-change 任务生命周期变更 标志着 working -> completed 等状态跃迁
citation 引用来源 标注参考文档
agent-settings 配置回显 确认客户端配置已生效

3. JSON Payload 示例

(1) 文本对话 (text-content)

AI 返回给用户的普通文本回复:

"message": {
  "kind": "message",
  "role": "agent",
  "messageId": "UUID-Message-ID",
  "parts": [{
    "kind": "text",
    "text": "好的,我已经为你修改了相关文件,请检查。"
  }]
}

(2) 工具调用确认 (tool-call-confirmation)

Agent 请求用户批准执行高危操作(如 Shell 命令、文件修改)。此时 status.state 通常会变为 "input-required",等待客户端回传用户的决定:

"message": {
  "parts": [{
    "kind": "data",
    "data": {
      "status": "pending",
      "request": {
        "callId": "call_12345",
        "name": "run_command",
        "args": { "command": "rm -rf ./temp" }
      },
      "confirmationDetails": {
         "type": "execute",
         "options": [
           { "id": "proceed_once", "name": "Allow Once" },
           { "id": "cancel", "name": "Reject" }
         ]
      }
    }
  }]
}

(3) 工具状态更新 (tool-call-update)

通知客户端工具的执行进度(开始执行、执行成功、失败):

"message": {
  "parts": [{
    "kind": "data",
    "data": {
      "status": "executing", // 或 "success", "error"
      "request": { "callId": "call_12345", "name": "run_command" },
      "liveOutput": "installing packages..." // 实时输出流
    }
  }]
}

(4) 思考链 (thought)

展示 AI 的思考过程 (Reasoning/CoT)。UI 通常会将此类消息折叠显示:

"message": {
  "parts": [{
    "kind": "text",
    "text": "用户想修改 config,我需要先读取当前目录结构,查找可能的配置文件..."
  }]
}

(5) 任务状态变更 (state-change)

标记任务生命周期的变化(例如任务完成)。无具体的 message payload:

"status": {
  "state": "completed", // working -> completed
  "message": null
}

(6) 引用来源 (citation)

标注生成内容的参考来源(如文档片段)。

(7) 客户端配置 (agent-settings)

回显或确认客户端传入的初始化配置信息。

4. 常见交互流程

┌─────────────────────────────────────────────────────────────────┐
│                      AionCLI 交互时序                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. 用户提问        ──→  客户端发送请求                           │
│         ↓                                                        │
│  2. thought         ←──  Agent 发送思考过程                       │
│         ↓                                                        │
│  3. confirmation    ←──  Agent 请求执行 Shell 权限                │
│         ↓                                                        │
│  4. 用户批准        ──→  客户端发送批准指令                        │
│         ↓                                                        │
│  5. tool-update     ←──  Agent 发送执行进度 (executing → success) │
│         ↓                                                        │
│  6. text-content    ←──  Agent 发送最终结果                       │
│         ↓                                                        │
│  7. state-change    ←──  Agent 发送完成状态 (completed)           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

典型流程说明

  1. 用户提问: 客户端发送请求
  2. 思考 (thought): Agent 发送 thought 消息,解释意图
  3. 请求权限 (confirmation): Agent 发送 tool-call-confirmation,请求执行 Shell
  4. 用户批准: 客户端发送批准指令
  5. 工具执行 (update): Agent 发送 tool-call-update (status: executing → success)
  6. 结果回复 (text): Agent 发送 text-content,展示最终结果
  7. 完成 (state-change): Agent 发送 state-change (completed)

十一、为什么选择 Gemini CLI 而非 Codex/Claude Code

在决定基于哪个开源项目进行魔改时,aioncli 团队评估了多个候选方案。本节详细分析为什么最终选择了 Gemini CLI。

1. 候选方案对比

项目 技术栈 开源协议 可修改性 选择结果
Gemini CLI TypeScript Apache 2.0 ✅ 完全可修改 ✅ 选中
Claude Code - 闭源 ❌ 无法修改 ❌ 排除
Codex Rust Apache 2.0 ⚠️ 技术栈不匹配 ❌ 排除

2. Claude Code 排除原因:闭源

关键发现:Claude Code 是 完全闭源 的产品。

/Users/pojian/code/github/claude-code/LICENSE.md 内容:

© Anthropic PBC. All rights reserved.

This repository contains extensions, plugins, and other software
components that extend or integrate with Claude Code...

Anthropic, Claude, and Claude Code are trademarks of Anthropic, PBC.

结论

  • Claude Code 仅公开了插件/扩展的代码
  • 核心 CLI 代码是 Anthropic 的专有软件
  • 无法进行任何形式的魔改或二次开发

3. Codex 排除原因:技术栈不匹配

关键发现:Codex 使用 Rust 编写,而非 TypeScript。

# /Users/pojian/code/github/codex/codex-rs/Cargo.toml
[workspace]
members = [
    "backend-client",
    "ansi-escape",
    "async-utils",
    "app-server",
    # ... 共 52 个 Rust crate
]

[workspace.package]
edition = "2024"  # 使用 Rust 2024 edition

项目规模

  • 52 个 Rust workspace 成员
  • 763 个 .rs 源文件
  • 核心模块:codex-core, codex-exec, codex-tui

排除理由

因素 影响
语言差异 TypeScript vs Rust,完全不同的编程范式
学习曲线 Rust 的所有权、生命周期概念学习成本高
团队技能 现有团队以 Node.js/TypeScript 为主
生态系统 Rust 与 npm 生态无法直接复用
迭代速度 Rust 编译时间长,开发迭代慢于 TypeScript
魔改难度 需要深入理解 Rust 异步运行时、FFI 等

4. Gemini CLI 选中原因

4.1 技术栈契合

// Gemini CLI package.json
{
  "type": "module",
  "engines": { "node": ">=20.0.0" },
  "devDependencies": {
    "typescript": "5.3.3",
    "vitest": "^2.1.8"
  }
}

契合点

  • TypeScript + Node.js:团队已有深厚积累
  • ES Modules:现代模块系统
  • Vitest 测试框架:熟悉的测试工具链

4.2 清晰的抽象接口

核心接口 ContentGenerator

export interface ContentGenerator {
  generateContent(request, userPromptId): Promise<GenerateContentResponse>;
  generateContentStream(request, userPromptId): Promise<AsyncGenerator<...>>;
  countTokens(request): Promise<CountTokensResponse>;
  embedContent(request): Promise<EmbedContentResponse>;
}

优势

  • 抽象层次合理,易于扩展
  • 清晰的接口契约
  • 支持流式和非流式两种模式
  • 内置 Token 计数能力

4.3 开放的许可证

Apache License 2.0

- ✅ 允许商业使用
- ✅ 允许修改和分发
- ✅ 允许私有化部署
- ✅ 仅需保留版权声明

4.4 活跃的社区和快速迭代

版本演进(2024-2025):
v0.1.x → v0.2.2 → v0.8.1 → v0.18.4 → v0.18.5

特点:
- 周级别更新
- 持续的功能增强
- 积极的 Bug 修复

5. 决策矩阵

评估维度 Gemini CLI Codex Claude Code
开源协议 Apache 2.0 ✅ Apache 2.0 ✅ 闭源 ❌
语言匹配 TypeScript ✅ Rust ❌ N/A
接口抽象 ContentGenerator ✅ Rust traits N/A
学习成本 低 ✅ 高 ❌ N/A
迭代速度 快 ✅ ⚠️ N/A
社区活跃 高 ✅ 高 ✅ 仅插件
可合并性 高 ✅ N/A N/A

最终得分

  • Gemini CLI: 7/7 ✅
  • Codex: 2/7 ⚠️
  • Claude Code: 0/7 ❌

6. 总结

aioncli 选择 Gemini CLI 作为基础项目的决策是 技术理性实践导向 的结果:

  1. Claude Code 根本不可行:闭源意味着无法魔改
  2. Codex 技术栈不匹配:Rust 学习成本和迭代效率是实际障碍
  3. Gemini CLI 完美契合
    • TypeScript 技术栈与团队一致
    • Apache 2.0 许可证允许商业化
    • ContentGenerator 接口设计优雅,易于扩展
    • 活跃的社区确保持续获得上游更新

这个选择验证了 "站在巨人肩膀上" 的工程智慧——选择正确的起点,比从零开始更高效。


十二、Codex vs Gemini CLI 架构深度对比

本节从技术架构层面深入分析 Codex 和 Gemini CLI 的本质差异,并探讨如果 aioncli 选择魔改 Codex 来兼容 Gemini API 会遇到的挑战。

1. 核心架构范式对比

维度 Codex (Rust) Gemini CLI (TypeScript)
通信模式 SQ/EQ 异步队列 Promise/AsyncGenerator
消息类型 三层事件(SDK 8种 / Exec 16种 / 内部 52种) 简单的 Request/Response
工具系统 ToolHandler + Orchestrator 简单的 Tool 接口
安全模型 内置沙箱 + 批准机制 依赖 MCP 协议
会话管理 Rollout 系统(undo/compact) 简单状态管理

Codex 事件分层说明

  • SDK 层(8 种):ThreadEvent - 暴露给 TypeScript SDK 用户
  • Exec 层(16 种):8 种事件 + 8 种 ThreadItemDetails - CLI JSON 输出
  • 内部协议层(52 种):EventMsg - Rust 模块间通信

2. 协议层本质差异

2.1 Codex: SQ/EQ (Submission Queue / Event Queue) 模式

// codex-rs/protocol/src/protocol.rs

// 用户提交队列
pub struct Submission {
    pub id: String,
    pub op: Op,  // 用户操作
}

pub enum Op {
    UserTurn { items, cwd, approval_policy, sandbox_policy, model, ... },
    ExecApproval { id, decision },
    PatchApproval { id, decision },
    Interrupt,
    Compact,
    Undo,
    // ... 20+ 操作类型
}

// 事件队列
pub enum EventMsg {
    TaskStarted(TaskStartedEvent),
    TaskComplete(TaskCompleteEvent),
    AgentMessage(AgentMessageEvent),
    AgentMessageDelta(AgentMessageDeltaEvent),
    ExecCommandBegin(ExecCommandBeginEvent),
    ExecCommandEnd(ExecCommandEndEvent),
    ExecApprovalRequest(ExecApprovalRequestEvent),
    // ... 50+ 事件类型
}

特点

  • 完全异步,解耦用户操作和系统响应
  • 细粒度事件(Begin/End/Delta 三阶段)
  • 支持中断、撤销、压缩等复杂操作

2.2 Gemini CLI: 简单的 ContentGenerator 接口

// packages/core/src/core/contentGenerator.ts

export interface ContentGenerator {
  generateContent(request, userPromptId): Promise<GenerateContentResponse>;
  generateContentStream(request, userPromptId): Promise<AsyncGenerator<...>>;
  countTokens(request): Promise<CountTokensResponse>;
  embedContent(request): Promise<EmbedContentResponse>;
}

特点

  • 简单直观,4 个核心方法
  • 请求-响应模式
  • 易于扩展(适配器模式)

3. 消息格式对比

3.1 Codex 的 ResponseItem(丰富的类型变体)

// codex-rs/protocol/src/models.rs

pub enum ResponseItem {
    Message { role, content },
    Reasoning { id, summary, content, encrypted_content },
    LocalShellCall { call_id, status, action },
    FunctionCall { id, name, arguments, call_id },
    FunctionCallOutput { call_id, output },
    CustomToolCall { call_id, name, input },
    WebSearchCall { id, status, action },
    GhostSnapshot { ghost_commit },
    Compaction { encrypted_content },
    Other,
}

复杂性来源

  • 支持推理摘要(Reasoning)
  • 支持 Ghost 提交(版本控制集成)
  • 支持压缩(Compaction)
  • 每种工具调用都有独立类型

3.2 Gemini CLI 的响应格式

// @google/genai 标准格式

interface GenerateContentResponse {
  candidates: [{
    content: {
      role: string;
      parts: Array<TextPart | FunctionCallPart | FunctionResponsePart>;
    };
    finishReason: string;
  }];
  usageMetadata: { ... };
}

简洁性来源

  • 统一的 parts 数组
  • 工具调用是 parts 的一种类型
  • 没有复杂的状态管理

4. 工具系统对比

4.1 Codex: 多层工具架构

// 工具处理器接口
pub trait ToolHandler: Send + Sync {
    fn kind(&self) -> ToolKind;
    async fn is_mutating(&self, invocation: &ToolInvocation) -> bool;
    async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError>;
}

// 工具编排器(包含沙箱和批准逻辑)
pub struct ToolOrchestrator {
    sandbox: SandboxManager,
}

// 执行流程:
// 1. Approval Phase → 检查是否需要用户批准
// 2. Sandbox Selection → 选择执行沙箱
// 3. Execution → 实际执行
// 4. Escalation → 失败时升级处理

复杂特性

  • 内置 macOS Seatbelt / Linux Seccomp 沙箱
  • 批准策略:UnlessTrusted, OnFailure, OnRequest, Never
  • 工具输出截断策略
  • 并行工具调用支持

4.2 Gemini CLI: 简单工具接口

// packages/core/src/tools/tools.ts

interface ToolInvocation<TParams, TResult> {
  params: TParams;
  getDescription(): string;
  shouldConfirmExecute(signal): Promise<...>;
  execute(signal): Promise<TResult>;
}

interface AnyDeclarativeTool {
  name: string;
  description: string;
  build(params): Promise<ToolInvocation<unknown, ToolResult>>;
}

简洁性来源

  • 单层抽象
  • 确认逻辑外置
  • 无内置沙箱

5. 安全模型对比

5.1 Codex: 内置多层安全

// 沙箱策略
pub enum SandboxPolicy {
    DangerFullAccess,           // 无限制(危险)
    ReadOnly,                   // 只读
    WorkspaceWrite {            // 工作区写入
        writable_roots: Vec<AbsolutePathBuf>,
        network_access: bool,
        exclude_tmpdir_env_var: bool,
        exclude_slash_tmp: bool,
    },
}

// 批准策略
pub enum AskForApproval {
    UnlessTrusted,  // 除非是安全命令
    OnFailure,      // 沙箱失败时
    OnRequest,      // 模型请求时(默认)
    Never,          // 从不询问
}

// 可写根目录(带只读子路径)
pub struct WritableRoot {
    pub root: AbsolutePathBuf,
    pub read_only_subpaths: Vec<AbsolutePathBuf>,  // 如 .git
}

5.2 Gemini CLI: 外置安全

  • 依赖 MCP(Model Context Protocol)协议
  • 确认逻辑在工具调用层
  • 无内置沙箱支持

6. 如果魔改 Codex 兼容 Gemini API 的挑战

6.1 协议转换复杂度

Gemini API Request          Codex 内部格式
       ↓                         ↓
GenerateContentRequest   →   Submission { Op::UserTurn }
       ↓                         ↓
       ↓                    50+ EventMsg 类型
       ↓                         ↓
GenerateContentResponse  ←   需要聚合多个事件

挑战

  • 需要将 Codex 的细粒度事件聚合为单一响应
  • 流式响应需要映射 EventMsg::AgentMessageDelta → Gemini 的 delta 格式
  • 工具调用需要双向转换

6.2 工具调用映射

Gemini 格式 Codex 格式 转换难度
FunctionCall { name, args } ResponseItem::FunctionCall 中等
FunctionResponse { name, response } ResponseInputItem::FunctionCallOutput 中等
- ResponseItem::LocalShellCall 需要映射
- ResponseItem::WebSearchCall 需要映射
- ResponseItem::Reasoning 无对应

6.3 沙箱模型不匹配

Gemini CLI 工具执行:
  Tool.execute() → 直接执行 → 返回结果

Codex 工具执行:
  ToolOrchestrator.run()
    → ApprovalPhase (可能等待用户)
    → SandboxSelection
    → FirstAttempt
    → Escalation (可能再次等待)
    → 返回结果

问题

  • Codex 的批准机制会阻塞执行
  • 需要实现 approval 回调机制
  • 沙箱失败升级逻辑无法直接映射

6.4 会话状态管理

功能 Codex Gemini CLI 兼容难度
撤销(Undo) ✅ 内置 ❌ 无
压缩(Compact) ✅ 内置 ❌ 无
恢复(Resume) ✅ Rollout 简单状态
Ghost 提交 ✅ 版本控制 ❌ 无 无法映射

6.5 类型系统转换

// Codex: Rust 强类型枚举
pub enum ContentItem {
    InputText { text: String },
    InputImage { image_url: String },
    OutputText { text: String },
}

// Gemini: TypeScript 联合类型
type Part = TextPart | InlineDataPart | FunctionCallPart | ...;

挑战

  • Rust 枚举 ↔ TypeScript 联合类型的序列化/反序列化
  • 需要处理 #[serde(tag = "type")] 等标记
  • FFI 边界的内存管理

7. 魔改 Codex 的工作量估算

工作项 工作量 难度
学习 Rust + 项目架构 2-4 周
实现 Gemini API 适配层 4-6 周
协议转换(SQ/EQ ↔ Promise) 2-3 周
工具调用映射 2-3 周
沙箱/批准机制适配 1-2 周
测试和调试 2-3 周
总计 13-21 周

8. 反向对比:Gemini CLI 魔改支持 OpenAI 的工作量

工作项 工作量 难度
理解 ContentGenerator 接口 2-3 天
实现 OpenAIContentGenerator 1-2 周
格式转换(Gemini ↔ OpenAI) 1 周
工具调用适配 3-5 天
测试和调试 1 周
总计 3-5 周

9. 架构设计哲学对比

哲学 Codex Gemini CLI
复杂度 企业级,功能完备 简洁实用
扩展方式 实现 Trait,修改枚举 实现接口,新增文件
安全优先 内置沙箱和批准 外置/可选
上游同步 困难(Rust 编译) 容易(TypeScript)
迭代速度 慢(编译时间) 快(解释执行)

10. 结论:为什么 Gemini CLI 是更好的选择

  1. 接口简洁:ContentGenerator 4 个方法 vs Codex 50+ 事件类型
  2. 扩展成本:新增一个文件 vs 修改多个 Rust crate
  3. 转换方向:Gemini → OpenAI(简单)vs Codex → Gemini(复杂)
  4. 维护成本:TypeScript 热更新 vs Rust 编译等待
  5. 团队技能:充分利用已有 TypeScript 专长

核心洞察

Codex 的设计是为了支持复杂的企业级场景(沙箱、批准、撤销、压缩),而 aioncli 的目标是多模型兼容。Gemini CLI 的简洁设计更适合作为适配器层的基础,而不是将复杂的 Codex 架构"削足适履"来适配简单的 API 转换需求。


十三、上游合并记录

v0.29.7 → v0.30.0 合并(2026-02-26)

合并分支merge-upstream-latest 上游版本:google-gemini/gemini-cli v0.30.0(含 141 个 commit)

1. 上游 v0.30.0 主要变更

架构改进

  • LlmRole 遥测系统generateContent / generateContentStream 接口新增第三个参数 role: LlmRole,用于区分不同场景的 LLM 调用(主聊天、工具调用、路由分类等),增强遥测数据粒度
  • 模型分类体系重构:新增 isCustomModel()supportsModernFeatures() 函数,取代原有 isPreviewModel(),支持非 Gemini 模型的特性检测
  • CoreToolCallStatus 枚举:替代原有字符串类型,为工具调用生命周期提供类型安全的状态管理
  • SDK 包引入:新增 packages/sdk 包,提供 Agent SDK 能力(GeminiCliAgent 等)

Plan Mode 增强

  • 5 阶段顺序规划工作流(formalize 5-phase sequential planning workflow
  • 活动处理时自动从审批模式轮转中移除 Plan Mode
  • Plan 文件按 session 隔离,支持技能在 Plan Mode 中启用
  • allowPlanMode 取代 isPlanEnabled 控制审批模式循环

开发者体验

  • getAuthTypeFromEnv() 从 CLI 迁移至 Core 包,统一认证类型检测
  • GEMINI_CLI=1 环境变量自动注入 stdio MCP 服务器传输
  • 自定义推理模型默认支持(support custom reasoning models by default
  • /commands reload 刷新自定义 TOML 命令
  • Ctrl-Z 进程挂起支持、Vim 模式增强

UI/UX 改进

  • Solarized Dark/Light 主题
  • 可搜索的设置列表(generic searchable list
  • AskUser 工具多行文本输入、颜色方案对齐
  • 表格文字自动换行、Markdown 渲染优化
  • 终端能力查询包裹隐藏序列(修复闪烁问题)

安全与策略

  • --policy 标志支持用户自定义策略文件
  • 严格安全带配置(strict seatbelt profiles
  • 弃用 --allowed-toolsexcludeTools,迁移至策略引擎
  • 工具输出掩码默认启用

2. aioncli 冲突解决策略

两边修改文件交集:65 个文件两侧都有改动,其中核心冲突集中在 6 个关键文件。

2.1 contentGenerator.ts — 最核心的冲突

这是整个合并中最关键的文件,双方在不同区域都做了重要改动:

区域 我们的改动 上游的改动 解决方式
AuthType 枚举 新增 USE_OPENAI, USE_ANTHROPIC, USE_BEDROCK 三个值 未改动 保留我们的
ContentGenerator 接口 未改动 generateContent/generateContentStream 新增第三参数 role: LlmRole 接受上游
getAuthTypeFromEnv() 不存在 新增函数(仅含 Google 三种类型) 接受上游
ContentGeneratorConfig 类型 扩展了 model, timeout, maxRetries, samplingParams, awsRegion 未改动 保留我们的
createContentGenerator() 新增 OpenAI/Anthropic/Bedrock 创建分支 未改动 保留我们的
import 区域 新增 DEFAULT_GEMINI_MODEL, debugLogger 新增 LlmRole 两者都保留
// 合并结果示意(枚举部分)
export enum AuthType {
  LOGIN_WITH_GOOGLE = 'oauth-personal',
  USE_GEMINI = 'gemini-api-key',
  USE_VERTEX_AI = 'vertex-ai',
  LEGACY_CLOUD_SHELL = 'cloud-shell',
  COMPUTE_ADC = 'compute-default-credentials',
  USE_OPENAI = 'openai', // ← aioncli 保留
  USE_ANTHROPIC = 'anthropic', // ← aioncli 保留
  USE_BEDROCK = 'bedrock', // ← aioncli 保留
}

// 合并结果示意(接口部分)
export interface ContentGenerator {
  generateContent(
    request: GenerateContentParameters,
    userPromptId: string,
    role: LlmRole, // ← 上游新增,接受
  ): Promise<GenerateContentResponse>;
  // ...
}
2.2 baseLlmClient.ts — 双方都大改

这是最复杂的合并,因为双方在同一个文件的不同层面都做了深度修改:

区域 我们的改动 上游的改动 解决方式
generateJson() 入口 新增 OpenAI 分支跳转 generateJsonForOpenAI() 新增 role: LlmRole 参数解构和传递 两者都保留
generateJsonForOpenAI() 整个方法(~120 行),用工具调用模拟 JSON Schema 不存在 保留我们的,内部也传 role
_generateWithRetry() 未改动 新增 role 参数,传给 contentGenerator.generateContent() 接受上游
import AuthType type 改为值 import(运行时需要值比较) 保持 type import 保留我们的
import 新增 Tool, FunctionDeclaration, Schema, getFunctionCalls LlmRole 两者都保留
// 合并关键点:generateJson 方法入口
async generateJson(options: GenerateJsonOptions): Promise<Record<string, unknown>> {
  const { modelConfigKey, contents, schema, abortSignal,
    systemInstruction, promptId, role, maxAttempts } = options;
  //                                  ^^^^ 上游新增

  // aioncli 保留:OpenAI 分支跳转
  const authType = this.config.getContentGeneratorConfig()?.authType;
  if (authType === AuthType.USE_OPENAI) {
    return this.generateJsonForOpenAI(options);  // options 内含 role
  }
  // 以下为原有 Gemini 逻辑 + 上游的 role 传递...
}
2.3 validateNonInterActiveAuth.ts — 最棘手的冲突

这个文件冲突最棘手,因为上游和我们对同一个函数做了完全相反的操作:

区域 我们的改动 上游的改动 解决方式
getAuthTypeFromEnv() 函数 在本地函数中追加 Bedrock + OpenAI 检测 删除整个本地函数,改为从 core 导入 拒绝上游删除,保留本地扩展版
import 语句 AuthType 作为 import AuthType 作为 type import + 新增 getAuthTypeFromEnv import 保留我们的
错误提示 扩展为多行,包含 Bedrock/OpenAI 说明 保持单行 保留我们的

为什么拒绝上游删除:上游将 getAuthTypeFromEnv() 移至 core 包,但 core 版本只包含 Google 的三种认证类型(OAuth、Vertex AI、API Key),不包含我们新增的 Bedrock/OpenAI/Anthropic 检测。如果接受上游删除,多模型认证将失效。

// 上游想改为:
import { getAuthTypeFromEnv } from '@google/gemini-cli-core';  // 只有 Google 类型

// 我们保留本地版本:
function getAuthTypeFromEnv(): AuthType | undefined {
  // Google 类型(与 core 版一致)
  if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') return AuthType.LOGIN_WITH_GOOGLE;
  if (process.env['GOOGLE_GENAI_USE_VERTEXAI'] === 'true') return AuthType.USE_VERTEX_AI;
  if (process.env['GEMINI_API_KEY']) return AuthType.USE_GEMINI;
  // aioncli 扩展(core 版没有的)
  if (process.env['AWS_ACCESS_KEY_ID'] || ...) return AuthType.USE_BEDROCK;
  if (process.env['OPENAI_API_KEY']) return AuthType.USE_OPENAI;
  if (process.env['ANTHROPIC_API_KEY']) return AuthType.USE_ANTHROPIC;
  return undefined;
}
2.4 models.ts — 自动合并

双方改动在文件不同位置,无实际冲突:

区域 我们的改动 上游的改动
文件末尾 Bedrock 模型区域映射(~140 行)
isGemini3Model 之后 新增 isCustomModel(), supportsModernFeatures()
2.5 auth.ts — 仅我们改

上游未修改此文件,我们新增了 USE_BEDROCKUSE_OPENAI 验证分支,纯追加无冲突。

2.6 package.json — 包名与依赖
字段 解决方式
name 保留 @office-ai/aioncli-core(上游为 @google/gemini-cli-core
version 接受上游 0.30.0
repository.url 保留 iOfficeAI/aioncli
依赖新增 保留我们的 @anthropic-ai/sdk, @aws-sdk/client-bedrock-runtime, openai, tiktoken;接受上游的 OpenTelemetry 升级

3. 合并后修复提交

合并完成后,针对接口变更进行了两个修复提交:

3.1 e6dc35e — fix(core): add LlmRole parameter to custom ContentGenerator adapters

v0.30.0 的 ContentGenerator 接口新增了 role: LlmRole 第三参数,但三个自定义适配器未同步更新:

// openaiContentGenerator.ts / anthropicContentGenerator.ts / bedrockContentGenerator.ts
// 修复前(2 个参数,与接口不匹配)
async generateContent(request, userPromptId): Promise<...>

// 修复后(3 个参数)
async generateContent(request, userPromptId, _role?: LlmRole): Promise<...>

同时修复 openaiContentGenerator.test.ts 中 OpenRouter 请求头测试,将 qwen-code 残留的 HTTP-RefererX-Title 改为 aioncli 品牌值。

3.2 ab76144 — fix(cli): env vars override stored OAuth for non-interactive multi-model auth

测试发现:当用户已缓存 Google OAuth 凭据(selectedType: "oauth-personal")时,即使设置了 OPENAI_API_KEY 环境变量,CLI 仍会走 Google OAuth 路径导致 403 错误。

根因validateNonInteractiveAutheffectiveAuthType = configuredAuthType || getAuthTypeFromEnv() 优先使用了存储的 configuredAuthType

修复:反转优先级,让环境变量优先于存储设置:

// 修复前
const effectiveAuthType = configuredAuthType || getAuthTypeFromEnv();

// 修复后
const envAuthType = getAuthTypeFromEnv();
const effectiveAuthType = envAuthType || configuredAuthType;

同时新增 ANTHROPIC_API_KEY 检测和 auth.ts 中的 Anthropic 验证分支。

3. 版本与依赖更新

包名 版本 说明
@office-ai/aioncli-core 0.30.0 保持自定义包名
@google/gemini-cli 0.30.0 CLI 包
@google/gemini-cli-a2a-server 0.30.0 A2A 服务器
@google/gemini-cli-sdk 0.30.0 新增 SDK 包
@google/gemini-cli-test-utils 0.30.0 测试工具
vscode-ide-companion 0.30.0 VSCode 扩展

新增上游依赖systeminformationws(WebSocket)

保留 aioncli 依赖@anthropic-ai/sdk@aws-sdk/client-bedrock-runtimetiktoken

4. 测试验证结果

文件数 用例数 结果
core 258 4935 251 passed / 7 failed(36 用例失败)
cli 376 5288 全部通过
sdk 4 15 3 passed / 1 failed(预存问题)
vscode 3 41 全部通过

Core 残余失败分析(均为预存问题,非本次合并引入):

  • oauth2.test.ts(20 个):keytar/keychain 环境依赖
  • shell.test.ts(8 个):Shell 工具格式变更导致的描述不匹配
  • mcp-client.test.ts(4 个):HTTP 传输 mock 不完整
  • tokenLimits.test.ts(2 个):默认 Token 上限断言
  • openaiContentGenerator.test.ts(1 个):OpenRouter 头信息
  • turn.test.ts(1 个):错误事件报告

5. 合并后需注意的兼容性要点

  1. LlmRole 传递:所有调用 contentGenerator.generateContent() 的地方都需要传递第三个 role 参数,openaiContentGenerator.ts 中已有的调用点需要在后续维护中注意
  2. supportsModernFeatures:该函数对非 Gemini 模型(如 OpenAI/Claude)默认返回 true(通过 isCustomModel 判断),这与 aioncli 的多模型策略一致
  3. getAuthTypeFromEnv 双重存在:Core 包和 CLI 包各有一份,CLI 版本包含 Bedrock/OpenAI 扩展。后续可考虑统一至 Core 包
  4. SDK 包:新增的 packages/sdk 包暂未做 aioncli 适配,如需使用需要检查与 OpenAI 适配器的兼容性

文档更新时间:2026-02-26