Qwen Code CanUseTool 实现分析

Qwen Code CanUseTool 实现分析

分析日期 : 2025-01-02
分析目标 : Qwen Code CLI 的工具权限检查机制
参考项目: C:\Users\LENOVO\Desktop\cli\quan\qwen-code


目录

  1. 架构概述
  2. 核心机制
  3. 代码流程分析
  4. 关键代码位置
  5. [与 Gemini CLI 对比](#与 Gemini CLI 对比)
  6. 改进建议
  7. 测试验证

架构概述

整体架构图

复制代码
┌───────────────────────────────────────────────────────────────┐
│                        SDK Process                             │
│                  (Python/TypeScript SDK)                       │
│                                                                 │
│  ┌──────────────┐      canUseTool()      ┌─────────────────┐  │
│  │  User Code   │ ─────────────────────> │  SDK Core       │  │
│  │              │                         │                 │  │
│  │  callback:   │ <────────────────────── │  Returns:       │  │
│  │  (toolName,  │    permission result    │  {              │  │
│  │   input)     │                         │    behavior:    │  │
│  │              │                         │    'allow' |    │  │
│  └──────────────┘                         │    'deny',      │  │
│                                           │    updatedInput │
│                                           │  }              │
│                                           └─────────────────┘  │
└───────────────────────────────────────────────────────────────┘
                            ↕ control_request/control_response
┌───────────────────────────────────────────────────────────────┐
│                      Qwen Code CLI                             │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │              PermissionController                         │  │
│  │                                                           │  │
│  │  • getToolCallUpdateCallback()                           │  │
│  │  • handleOutgoingPermissionRequest()                     │  │
│  │  • handleCanUseTool()                                    │  │
│  └─────────────────────────────────────────────────────────┘  │
│                            ↕ onToolCallsUpdate                 │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │              CoreToolScheduler                            │  │
│  │                                                           │  │
│  │  executeToolCall()                                       │  │
│  │    → 工具状态: 'awaiting_approval'                       │  │
│  │    → 触发: onToolCallsUpdate(callback)                   │  │
│  │    → 等待: onConfirm(allow/deny)                         │  │
│  └─────────────────────────────────────────────────────────┘  │
│                            ↕ onToolCallsUpdate                 │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │              NonInteractive Flow                         │  │
│  │                                                           │  │
│  │  runNonInteractive()                                     │  │
│  │    → toolCallUpdateCallback =                            │  │
│  │        controlService.permission                         │  │
│  │          .getToolCallUpdateCallback()                    │  │
│  │    → executeToolCall(..., {                              │  │
│  │          onToolCallsUpdate: toolCallUpdateCallback       │  │
│  │        })                                                 │  │
│  └─────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────┘

核心设计理念

事件驱动架构 (Event-Driven Architecture)

Qwen Code 使用事件驱动的回调机制,而不是主动轮询或手动检查:

  1. 注册阶段 : 通过 onToolCallsUpdate 注册回调
  2. 触发阶段 : CoreToolScheduler 检测到 awaiting_approval 状态
  3. 处理阶段 : 回调自动发送 can_use_tool 请求
  4. 响应阶段: SDK 返回权限决定
  5. 继续阶段 : 通过 onConfirm 告知执行结果

核心机制

1. 回调注册机制

文件 : packages/cli/src/nonInteractive/nonInteractiveCli.ts:331-334

typescript 复制代码
const toolCallUpdateCallback =
  inputFormat === InputFormat.STREAM_JSON && options.controlService
    ? options.controlService.permission.getToolCallUpdateCallback()
    : undefined;

关键点:

  • ✅ 只在 stream-json 模式下启用
  • ✅ 从 ControlServicePermissionController 获取回调
  • ✅ 如果不是 SDK 模式,返回 undefined(本地权限检查)

2. 回调传递给工具执行

文件 : packages/cli/src/nonInteractive/nonInteractiveCli.ts:346-360

typescript 复制代码
const toolResponse = await executeToolCall(
  config,
  finalRequestInfo,
  abortController.signal,
  toolCallUpdateCallback
    ? {
        onToolCallsUpdate: toolCallUpdateCallback,  // ← 关键参数
      }
    : undefined,
);

关键点:

  • ✅ 将回调作为 onToolCallsUpdate 参数传递
  • ✅ CoreToolScheduler 会在工具状态变化时调用此回调
  • ✅ 这是事件驱动的核心:SDK 不需要主动调用检查

3. 回调实现:监听工具状态

文件 : packages/cli/src/nonInteractive/control/controllers/permissionController.ts:362-381

typescript 复制代码
getToolCallUpdateCallback(): (toolCalls: unknown[]) => void {
  return (toolCalls: unknown[]) => {
    for (const call of toolCalls) {
      // 监听特定状态:awaiting_approval
      if (
        call &&
        typeof call === 'object' &&
        (call as { status?: string }).status === 'awaiting_approval'
      ) {
        const awaiting = call as WaitingToolCall;

        // 防止重复请求
        if (
          typeof awaiting.confirmationDetails?.onConfirm === 'function' &&
          !this.pendingOutgoingRequests.has(awaiting.request.callId)
        ) {
          this.pendingOutgoingRequests.add(awaiting.request.callId);

          // 异步处理权限请求(不阻塞其他工具)
          void this.handleOutgoingPermissionRequest(awaiting);
        }
      }
    }
  };
}

关键点:

  • 状态监听 : 只处理 awaiting_approval 状态的工具
  • 防重复 : 使用 pendingOutgoingRequests Set 防止重复请求
  • 异步处理 : 使用 void 关键字,不阻塞主流程
  • 批量处理: 一次回调可能包含多个工具的状态更新

4. 发送权限请求到 SDK

文件 : packages/cli/src/nonInteractive/control/controllers/permissionController.ts:416-432

typescript 复制代码
private async handleOutgoingPermissionRequest(
  toolCall: WaitingToolCall,
): Promise<void> {
  try {
    // 1. 检查是否已中断
    if (this.context.abortSignal?.aborted) {
      await toolCall.confirmationDetails.onConfirm(
        ToolConfirmationOutcome.Cancel,
      );
      return;
    }

    const inputFormat = this.context.config.getInputFormat?.();
    const isStreamJsonMode = inputFormat === InputFormat.STREAM_JSON;

    if (!isStreamJsonMode) {
      // 非 stream-json 模式:本地权限检查
      const modeCheck = this.checkPermissionMode();
      const outcome = modeCheck.allowed
        ? ToolConfirmationOutcome.ProceedOnce
        : ToolConfirmationOutcome.Cancel;

      await toolCall.confirmationDetails.onConfirm(outcome);
      return;
    }

    // 2. Stream-json 模式:向 SDK 请求权限
    const permissionSuggestions = this.buildPermissionSuggestions(
      toolCall.confirmationDetails,
    );

    const response = await this.sendControlRequest(
      {
        subtype: 'can_use_tool',
        tool_name: toolCall.request.name,
        tool_use_id: toolCall.request.callId,
        input: toolCall.request.args,
        permission_suggestions: permissionSuggestions,
        blocked_path: null,
      } as CLIControlPermissionRequest,
      undefined, // 使用默认超时
      this.context.abortSignal,
    );

    // 3. 处理 SDK 响应(见下一节)
  } catch (error) {
    // 错误处理...
  }
}

关键点:

  • 双模式支持: stream-json 模式问 SDK,其他模式本地检查
  • 中断检测: 首先检查是否已被中断
  • 权限建议: 构建 UI 友好的权限建议列表
  • 超时控制: 使用默认超时(在 BaseController 中定义)

5. 处理 SDK 响应

文件 : packages/cli/src/nonInteractive/control/controllers/permissionController.ts:434-464

typescript 复制代码
// 等待 SDK 响应
if (response.subtype !== 'success') {
  // 响应失败,取消工具执行
  await toolCall.confirmationDetails.onConfirm(
    ToolConfirmationOutcome.Cancel,
  );
  return;
}

const payload = (response.response || {}) as Record<string, unknown>;
const behavior = String(payload['behavior'] || '').toLowerCase();

if (behavior === 'allow') {
  // 允许执行
  // 1. 处理更新的输入(如果 SDK 修改了参数)
  const updatedInput = payload['updatedInput'];
  if (updatedInput && typeof updatedInput === 'object') {
    toolCall.request.args = updatedInput as Record<string, unknown>;
  }

  // 2. 告知 CoreToolScheduler 继续
  await toolCall.confirmationDetails.onConfirm(
    ToolConfirmationOutcome.ProceedOnce,
  );
} else {
  // 拒绝执行
  const cancelMessage =
    typeof payload['message'] === 'string'
      ? payload['message']
      : undefined;

  // 3. 告知 CoreToolScheduler 取消
  await toolCall.confirmationDetails.onConfirm(
    ToolConfirmationOutcome.Cancel,
    cancelMessage ? { cancelMessage } : undefined,
  );
}

关键点:

  • 输入修改: SDK 可以修改工具的输入参数
  • 取消消息: 支持自定义拒绝消息
  • 确认机制 : 通过 onConfirm 告知 CoreToolScheduler 结果

代码流程分析

完整的执行流程

typescript 复制代码
// ═══════════════════════════════════════════════════════════════
// 阶段 1: 初始化
// ═══════════════════════════════════════════════════════════════

// Session 创建 ControlService
this.controlService = new ControlService({
  config: this.config,
  permissionMode: 'default',
  abortSignal: this.abortController.signal,
  debugMode: this.debugMode,
});

// ControlService 创建 PermissionController
// PermissionController 注册 getToolCallUpdateCallback()

// ═══════════════════════════════════════════════════════════════
// 阶段 2: 工具调用准备
// ═══════════════════════════════════════════════════════════════

// nonInteractiveCli.ts
const toolCallUpdateCallback =
  inputFormat === InputFormat.STREAM_JSON && options.controlService
    ? options.controlService.permission.getToolCallUpdateCallback()
    : undefined;

// ═══════════════════════════════════════════════════════════════
// 阶段 3: 工具执行
// ═══════════════════════════════════════════════════════════════

const toolResponse = await executeToolCall(
  config,
  finalRequestInfo,
  abortController.signal,
  toolCallUpdateCallback
    ? {
        onToolCallsUpdate: toolCallUpdateCallback,
      }
    : undefined,
);

// ═══════════════════════════════════════════════════════════════
// 阶段 4: CoreToolScheduler 检测权限需求
// ═══════════════════════════════════════════════════════════════

// 在 CoreToolScheduler 内部
if (工具需要用户批准) {
  // 更新工具状态
  toolCall.status = 'awaiting_approval';

  // 触发回调(如果已注册)
  if (options.onToolCallsUpdate) {
    options.onToolCallsUpdate([
      {
        status: 'awaiting_approval',
        request: { callId, name, args },
        confirmationDetails: {
          onConfirm: (outcome) => {
            // 等待权限决定...
          }
        }
      }
    ]);
  }
}

// ═══════════════════════════════════════════════════════════════
// 阶段 5: PermissionController 处理
// ═══════════════════════════════════════════════════════════════

// permissionController.ts
// 回调被触发
toolCallUpdateCallback([
  {
    status: 'awaiting_approval',
    request: { callId, name, args },
    confirmationDetails: { onConfirm }
  }
]);

// 检测到 awaiting_approval 状态
if (call.status === 'awaiting_approval') {
  // 发送控制请求到 SDK
  const response = await this.sendControlRequest({
    subtype: 'can_use_tool',
    tool_name: call.request.name,
    tool_use_id: call.request.callId,
    input: call.request.args,
    permission_suggestions: [...],
    blocked_path: null,
  });

  // 等待 SDK 响应...
}

// ═══════════════════════════════════════════════════════════════
// 阶段 6: SDK 处理
// ═══════════════════════════════════════════════════════════════

// SDK 端(Python/TypeScript)
if (subtype == "can_use_tool"):
    # 调用用户提供的回调
    response = await self.can_use_tool(
        tool_name,
        input,
        context,
    )

    # 返回权限决定
    send_control_response({
        "type": "control_response",
        "response": {
            "subtype": "success",
            "request_id": request_id,
            "response": {
                "behavior": "allow",  # 或 "deny"
                "updatedInput": {...},  # 可选:修改后的输入
                "message": "..."  # 可选:拒绝原因
            }
        }
    })

// ═══════════════════════════════════════════════════════════════
// 阶段 7: PermissionController 处理响应
// ═══════════════════════════════════════════════════════════════

// 接收 SDK 响应
const payload = response.response;
const behavior = payload.behavior;  // "allow" 或 "deny"

if (behavior === 'allow') {
  // 处理输入修改
  if (payload.updatedInput) {
    toolCall.request.args = payload.updatedInput;
  }

  // 告知 CoreToolScheduler 继续
  await toolCall.confirmationDetails.onConfirm(
    ToolConfirmationOutcome.ProceedOnce
  );
} else {
  // 拒绝执行
  await toolCall.confirmationDetails.onConfirm(
    ToolConfirmationOutcome.Cancel,
    { cancelMessage: payload.message }
  );
}

// ═══════════════════════════════════════════════════════════════
// 阶段 8: CoreToolScheduler 继续或取消
// ═══════════════════════════════════════════════════════════════

// 在 CoreToolScheduler 内部
onConfirm((outcome) => {
  if (outcome === ToolConfirmationOutcome.ProceedOnce) {
    // 继续执行工具
    executeTool(toolCall.request);
  } else {
    // 取消执行
    skipTool(toolCall.request);
  }
});

// ═══════════════════════════════════════════════════════════════
// 阶段 9: 返回工具结果
// ═══════════════════════════════════════════════════════════════

// 工具执行完成(或被取消)
const toolResponse = {
  responseParts: [...],
  error: errorOrNull,
};

// 返回给调用者
return toolResponse;

关键代码位置

1. PermissionController

路径 : packages/cli/src/nonInteractive/control/controllers/permissionController.ts

方法 行数 功能
handleRequestPayload 45-69 路由控制请求(can_use_tool / set_permission_mode)
handleCanUseTool 79-137 处理 can_use_tool 控制请求(SDK → CLI)
checkPermissionMode 142-160 检查权限模式是否允许工具执行
checkToolRegistry 165-199 检查工具是否在注册表中
buildPermissionSuggestions 246-356 构建权限建议(用于 UI 显示)
getToolCallUpdateCallback 362-381 核心:获取工具状态更新回调
handleOutgoingPermissionRequest 390-492 核心:处理外发权限请求(CLI → SDK)

2. ControlService

路径 : packages/cli/src/nonInteractive/control/ControlService.ts

方法 行数 功能
permission 68-88 权限服务 API
getToolCallUpdateCallback 85-86 暴露回调获取接口
typescript 复制代码
get permission(): PermissionServiceAPI {
  const controller = this.ensureController('permission');
  return {
    buildPermissionSuggestions:
      controller.buildPermissionSuggestions.bind(controller),
    getToolCallUpdateCallback:
      controller.getToolCallUpdateCallback.bind(controller),
  };
}

3. NonInteractive Flow

路径 : packages/cli/src/nonInteractive/nonInteractiveCli.ts

代码段 行数 功能
获取回调 331-334 从 ControlService 获取 toolCallUpdateCallback
工具执行 346-360 将回调传递给 executeToolCall
typescript 复制代码
// Line 331-334: 获取权限回调
const toolCallUpdateCallback =
  inputFormat === InputFormat.STREAM_JSON && options.controlService
    ? options.controlService.permission.getToolCallUpdateCallback()
    : undefined;

// Line 346-360: 传递给工具执行
const toolResponse = await executeToolCall(
  config,
  finalRequestInfo,
  abortController.signal,
  toolCallUpdateCallback
    ? {
        onToolCallsUpdate: toolCallUpdateCallback,
      }
    : undefined,
);

4. 类型定义

路径 : packages/cli/src/nonInteractive/types.ts

typescript 复制代码
// Line 293-300: CanUseTool 控制请求
export interface CLIControlPermissionRequest {
  subtype: 'can_use_tool';
  tool_name: string;
  tool_use_id: string;
  input: unknown;
  permission_suggestions: PermissionSuggestion[] | null;
  blocked_path: string | null;
}

// Line 264-276: 权限建议
export interface PermissionSuggestion {
  type: 'allow' | 'deny' | 'modify';
  label: string;
  description: string;
}

5. 测试文件

路径 : integration-tests/sdk-typescript/permission-control.test.ts

测试覆盖:

  • ✅ canUseTool 回调被调用
  • ✅ allow 行为允许工具执行
  • ✅ deny 行为阻止工具执行
  • ✅ permission_suggestions 传递
  • ✅ abort signal 传递
  • ✅ 默认行为(无回调)
  • ✅ setPermissionMode API
  • ✅ 不同权限模式的行为(default / yolo / plan / auto-edit)

与 Gemini CLI 对比

实现状态对比

特性 Gemini CLI Qwen Code
控制协议实现 ✅ 完整 ✅ 完整
回调注册机制 ❌ 未实现 ✅ 完整
工具集成 ❌ 未调用 ✅ 完整集成
事件驱动 ❌ 无 ✅ 基于回调
输入修改 ✅ 支持 ✅ 支持
权限建议 ✅ 支持 ✅ 支持
超时处理 ✅ 60秒 ✅ 可配置
错误处理 ✅ 默认拒绝 ✅ 取消执行
测试覆盖 ❌ 无 ✅ 完整测试
总体完成度 75% 100%

代码对比

Gemini CLI(未实现集成)
typescript 复制代码
// session.ts:361-366
for (const requestInfo of toolCallRequests) {
  // ❌ 直接执行,没有权限检查
  const completedToolCall = await executeToolCall(
    this.config,
    requestInfo,
    this.abortController.signal,
  );
}

问题:

  • ❌ 没有权限检查
  • ❌ 没有回调传递
  • ❌ 没有与 ControlProtocolHandler 集成

Qwen Code(完整实现)
typescript 复制代码
// nonInteractiveCli.ts:331-360

// 1️⃣ 获取权限回调
const toolCallUpdateCallback =
  inputFormat === InputFormat.STREAM_JSON && options.controlService
    ? options.controlService.permission.getToolCallUpdateCallback()
    : undefined;

// 2️⃣ 传递给工具执行
const toolResponse = await executeToolCall(
  config,
  finalRequestInfo,
  abortController.signal,
  toolCallUpdateCallback
    ? {
        onToolCallsUpdate: toolCallUpdateCallback,  // ← 关键!
      }
    : undefined,
);

// 3️⃣ 回调自动触发权限检查
// (在 PermissionController 中)

优势:

  • ✅ 自动触发,无需手动调用
  • ✅ 与 CoreToolScheduler 深度集成
  • ✅ 事件驱动,性能高效

架构差异

方面 Gemini CLI Qwen Code
架构模式 主动调用 事件驱动
集成方式 独立模块 深度集成
触发机制 需要手动调用 自动触发
状态管理 简单 复杂(Set 防重复)
错误处理 基础 完善
可扩展性 中等

改进建议

对 Gemini CLI 的改进方案

方案 1: 手动权限检查(最小改动)
typescript 复制代码
// session.ts
for (const requestInfo of toolCallRequests) {
  // ✨ 添加:工具执行前检查权限
  if (this.controlHandler && this.isSdkMode) {
    const permissionResult = await this.checkToolPermission(
      requestInfo.name,
      requestInfo.args
    );

    if (permissionResult.behavior === 'deny') {
      // 权限被拒绝
      this.outputAdapter.emitToolResult(requestInfo, {
        error: permissionResult.message || 'Permission denied',
      });
      continue;
    }

    // 使用可能被修改的输入
    if (permissionResult.updatedInput) {
      requestInfo.args = permissionResult.updatedInput;
    }
  }

  // 执行工具
  const completedToolCall = await executeToolCall(
    this.config,
    requestInfo,
    this.abortController.signal,
  );
}

// 新增方法
private async checkToolPermission(
  toolName: string,
  toolInput: unknown
): Promise<{ behavior: 'allow' | 'deny'; updatedInput?: unknown; message?: string }> {
  // 发送 can_use_tool 控制请求
  const requestId = crypto.randomUUID();

  await this.outputAdapter.writeControlRequest({
    type: 'control_request',
    request_id: requestId,
    request: {
      subtype: 'can_use_tool',
      tool_name: toolName,
      tool_use_id: crypto.randomUUID(),
      input: toolInput,
      permission_suggestions: null,
      blocked_path: null,
    },
  });

  // 等待响应
  const response = await this.waitForControlResponse(requestId);

  return {
    behavior: response.response.behavior,
    updatedInput: response.response.updatedInput,
    message: response.response.message,
  };
}

private async waitForControlResponse(
  requestId: string
): Promise<CLIControlResponse> {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error('Permission check timeout'));
    }, 60000);

    const handler = (response: CLIControlResponse) => {
      if (response.response.request_id === requestId) {
        clearTimeout(timeout);
        this.inputReader.off('control_response', handler);
        resolve(response);
      }
    };

    this.inputReader.on('control_response', handler);
  });
}

优点:

  • ✅ 改动最小
  • ✅ 易于实现
  • ✅ 不需要修改 executeToolCall

缺点:

  • ❌ 串行执行(性能较低)
  • ❌ 需要手动管理响应等待
  • ❌ 不是事件驱动

方案 2: 事件驱动集成(推荐,参考 Qwen)
typescript 复制代码
// 1. 创建 PermissionController
// sdk/permissionController.ts

export class GeminiPermissionController {
  private pendingRequests = new Set<string>();

  constructor(
    private outputAdapter: StreamJsonOutputAdapter,
    private inputReader: StreamJsonInputReader,
    private debugMode: boolean = false
  ) {}

  /**
   * 获取工具调用更新回调
   */
  getToolCallUpdateCallback(): (toolCalls: unknown[]) => void {
    return (toolCalls: unknown[]) => {
      for (const call of toolCalls) {
        if (this.isAwaitingApproval(call)) {
          this.handlePermissionRequest(call);
        }
      }
    };
  }

  private isAwaitingApproval(call: unknown): boolean {
    return (
      call &&
      typeof call === 'object' &&
      (call as { status?: string }).status === 'awaiting_approval'
    );
  }

  private async handlePermissionRequest(toolCall: WaitingToolCall): Promise<void> {
    const callId = toolCall.request.callId;

    if (this.pendingRequests.has(callId)) {
      return;
    }

    this.pendingRequests.add(callId);

    try {
      // 发送权限请求
      const requestId = crypto.randomUUID();
      await this.outputAdapter.writeControlRequest({
        type: 'control_request',
        request_id: requestId,
        request: {
          subtype: 'can_use_tool',
          tool_name: toolCall.request.name,
          tool_use_id: callId,
          input: toolCall.request.args,
          permission_suggestions: this.buildSuggestions(toolCall),
          blocked_path: null,
        },
      });

      // 等待响应
      const response = await this.waitForResponse(requestId);

      // 处理响应
      if (response.response.behavior === 'allow') {
        if (response.response.updatedInput) {
          toolCall.request.args = response.response.updatedInput;
        }
        await toolCall.confirmationDetails.onConfirm(
          ToolConfirmationOutcome.ProceedOnce
        );
      } else {
        await toolCall.confirmationDetails.onConfirm(
          ToolConfirmationOutcome.Cancel,
          { cancelMessage: response.response.message }
        );
      }
    } catch (error) {
      if (this.debugMode) {
        console.error('[PermissionController] Error:', error);
      }
      await toolCall.confirmationDetails.onConfirm(
        ToolConfirmationOutcome.Cancel
      );
    } finally {
      this.pendingRequests.delete(callId);
    }
  }

  private async waitForResponse(requestId: string): Promise<unknown> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error('Permission check timeout (60s)'));
      }, 60000);

      const handler = (response: CLIControlResponse) => {
        if (response.response.request_id === requestId) {
          clearTimeout(timeout);
          this.inputReader.off('control_response', handler);
          resolve(response.response);
        }
      };

      this.inputReader.on('control_response', handler);
    });
  }

  private buildSuggestions(toolCall: WaitingToolCall): PermissionSuggestion[] {
    // 根据工具类型构建建议
    return [
      {
        type: 'allow',
        label: 'Allow',
        description: `Execute ${toolCall.request.name}`,
      },
      {
        type: 'deny',
        label: 'Deny',
        description: 'Block this tool execution',
      },
    ];
  }
}

// 2. 在 Session 中集成
// session.ts

export class Session {
  private permissionController: GeminiPermissionController | null = null;

  async initialize(): Promise<void> {
    // ...现有代码...

    // 如果是 SDK 模式,创建权限控制器
    if (this.isSdkMode) {
      this.permissionController = new GeminiPermissionController(
        this.outputAdapter,
        this.inputReader,
        this.debugMode
      );
    }
  }

  private async processUserMessage(userMessage: CLIUserMessage): Promise<void> {
    // ...现有代码...

    // 传递权限控制器给 runNonInteractive
    await runNonInteractive(
      this.config,
      createMinimalSettings(),
      input,
      promptId,
      {
        abortController: this.abortController,
        adapter: this.outputAdapter,
        permissionController: this.permissionController,  // ← 新增
      },
    );
  }
}

// 3. 在 nonInteractiveCli 中使用
// nonInteractiveCli.ts

export async function runNonInteractive(
  config: Config,
  settings: LoadedSettings,
  input: string,
  prompt_id: string,
  options: RunNonInteractiveOptions = {},
): Promise<void> {
  // ...现有代码...

  for (const requestInfo of toolCallRequests) {
    // ✨ 获取权限回调
    const toolCallUpdateCallback =
      options.permissionController?.getToolCallUpdateCallback();

    // ✨ 执行工具(传递回调)
    const toolResponse = await executeToolCall(
      config,
      requestInfo,
      abortController.signal,
      toolCallUpdateCallback
        ? {
            onToolCallsUpdate: toolCallUpdateCallback,
          }
        : undefined,
    );
  }
}

优点:

  • ✅ 事件驱动,性能高
  • ✅ 与 Qwen Code 一致
  • ✅ 可扩展性好
  • ✅ 支持并发权限检查

缺点:

  • ⚠️ 改动较大
  • ⚠️ 需要修改 executeToolCall 接口

改进优先级

优先级 改进项 工作量 影响
P0 实现基础权限检查 核心 SDK 功能
P1 事件驱动集成 性能和可维护性
P2 完善错误处理 稳定性
P3 添加单元测试 质量保证

测试验证

Qwen Code 的测试覆盖

文件 : integration-tests/sdk-typescript/permission-control.test.ts

测试 1: canUseTool 回调被调用
typescript 复制代码
it('should invoke canUseTool callback when tool is requested', async () => {
  const toolCalls: Array<{
    toolName: string;
    input: Record<string, unknown>;
  }> = [];

  const q = query({
    prompt: 'Write a js hello world to file.',
    options: {
      ...SHARED_TEST_OPTIONS,
      cwd: testDir,
      canUseTool: async (toolName, input) => {
        toolCalls.push({ toolName, input });
        return {
          behavior: 'deny',
          message: 'Tool execution denied by user.',
        };
      },
    },
  });

  // 验证回调被调用
  expect(toolCalls.length).toBeGreaterThan(0);
  expect(toolCalls[0].toolName).toBeDefined();
  expect(toolCalls[0].input).toBeDefined();
});
测试 2: allow 行为
typescript 复制代码
it('should allow tool execution when canUseTool returns allow', async () => {
  const q = query({
    prompt: 'Create a file named hello.txt with content "world"',
    options: {
      ...SHARED_TEST_OPTIONS,
      permissionMode: 'default',
      cwd: testDir,
      canUseTool: async (toolName, input) => {
        return {
          behavior: 'allow',
          updatedInput: input,
        };
      },
    },
  });

  // 验证工具成功执行
  expect(callbackInvoked).toBe(true);
  expect(hasToolResult).toBe(true);
});
测试 3: deny 行为
typescript 复制代码
it('should deny tool execution when canUseTool returns deny', async () => {
  const q = query({
    prompt: 'Create a file named test.txt',
    options: {
      ...SHARED_TEST_OPTIONS,
      cwd: testDir,
      permissionMode: 'default',
      canUseTool: async () => {
        return {
          behavior: 'deny',
          message: 'Tool execution denied by test',
        };
      },
    },
  });

  // 验证工具被阻止
  expect(callbackInvoked).toBe(true);
});
测试 4: 不同权限模式
typescript 复制代码
describe('yolo mode', () => {
  it('should auto-approve all tools without canUseTool callback', async () => {
    const q = query({
      prompt: 'Create a file named test-yolo.txt',
      options: {
        ...SHARED_TEST_OPTIONS,
        permissionMode: 'yolo',
        cwd: testDir,
        // No canUseTool callback
      },
    });

    const messages: SDKMessage[] = [];
    for await (const message of q) {
      messages.push(message);
    }

    // yolo 模式下应该自动执行工具
    expect(hasSuccessfulToolResults(messages)).toBe(true);
  });
});

describe('plan mode', () => {
  it('should not call any write tools in plan mode', async () => {
    const q = query({
      prompt: 'Read test-plan-file.txt and suggest improvements',
      options: {
        ...SHARED_TEST_OPTIONS,
        permissionMode: 'plan',
        cwd: testDir,
      },
    });

    // 验证没有调用写入工具
    const writeToolCalls = allToolCalls.filter((tc) =>
      WRITE_TOOLS.includes(tc.toolUse.name)
    );
    expect(writeToolCalls.length).toBe(0);
  });
});

对 Gemini CLI 的测试建议

typescript 复制代码
// 测试文件:integration-tests/gemini-sdk/permission-control.test.ts

describe('Gemini CLI Permission Control', () => {
  it('should send can_use_tool request before tool execution', async () => {
    const controlRequests: CLIControlRequest[] = [];

    const agent = new ClaudeAgent({
      canUseTool: async (toolName, input) => {
        return { behavior: 'allow', updatedInput: input };
      },
    });

    // 拦截控制请求
    agent.on('control_request', (req) => {
      controlRequests.push(req);
    });

    await agent.run('Create a file test.txt');

    // 验证发送了 can_use_tool 请求
    const permissionRequests = controlRequests.filter(
      req => req.request.subtype === 'can_use_tool'
    );
    expect(permissionRequests.length).toBeGreaterThan(0);
  });

  it('should deny tool when canUseTool returns deny', async () => {
    let callbackInvoked = false;

    const agent = new ClaudeAgent({
      canUseTool: async (toolName, input) => {
        callbackInvoked = true;
        return {
          behavior: 'deny',
          message: 'Test denial',
        };
      },
    });

    const messages = [];
    for await (const msg of agent.run('Delete all files')) {
      messages.push(msg);
    }

    // 验证回调被调用
    expect(callbackInvoked).toBe(true);

    // 验证工具结果包含错误
    const toolResults = messages.filter(
      msg => msg.type === 'tool_result' && msg.content.is_error
    );
    expect(toolResults.length).toBeGreaterThan(0);
  });

  it('should use updated input from SDK', async () => {
    const agent = new ClaudeAgent({
      canUseTool: async (toolName, input) => {
        if (toolName === 'Write') {
          return {
            behavior: 'allow',
            updatedInput: {
              ...input,
              content: 'Modified by SDK',
            },
          };
        }
        return { behavior: 'allow', updatedInput: input };
      },
    });

    const messages = [];
    for await (const msg of agent.run('Write to file.txt: Original')) {
      messages.push(msg);
    }

    // 验证文件内容被修改
    const fs = require('fs');
    const content = fs.readFileSync('file.txt', 'utf-8');
    expect(content).toBe('Modified by SDK');
  });
});

总结

Qwen Code 的优势

  1. 完整实现: 从协议到集成完全实现
  2. 事件驱动: 基于回调的高效架构
  3. 深度集成: 与 CoreToolScheduler 无缝集成
  4. 生产就绪: 有完整的测试覆盖
  5. 可扩展性: 易于添加新功能

关键设计模式

观察者模式 (Observer Pattern):

  • CoreToolScheduler 是主题(Subject)
  • PermissionController 是观察者(Observer)
  • 通过 onToolCallsUpdate 回调连接

策略模式 (Strategy Pattern):

  • 不同的权限模式(default / yolo / plan / auto-edit)
  • 每种模式有不同的权限策略

异步模式 (Async/Await):

  • 所有权限检查都是异步的
  • 不阻塞主流程
  • 支持并发权限检查

对 Gemini CLI 的建议

短期(1-2周):

  1. 实现基础权限检查(方案1)
  2. 添加单元测试

中期(1-2月):

  1. 迁移到事件驱动架构(方案2)
  2. 完善错误处理

长期(3-6月):

  1. 添加高级功能(Hooks、MCP)
  2. 性能优化
  3. 完整的测试覆盖

附录

A. 相关文件路径

复制代码
qwen-code/
├── packages/cli/src/nonInteractive/
│   ├── nonInteractiveCli.ts              # 主流程
│   ├── session.ts                        # 会话管理
│   └── control/
│       ├── ControlService.ts             # 控制服务
│       ├── controllers/
│       │   ├── permissionController.ts   # 权限控制器(核心)
│       │   ├── baseController.ts         # 基础控制器
│       │   └── systemController.ts       # 系统控制器
│       └── types/
│           └── serviceAPIs.ts            # 服务 API 类型
│
└── integration-tests/sdk-typescript/
    └── permission-control.test.ts        # 权限控制测试

B. 参考资料

C. 相关 Pull Request


文档版本 : 1.0.0
最后更新 : 2025-01-02
作者: Claude Code Analysis

相关推荐
The Sheep 20231 天前
可视化命中测试
java·服务器·前端
wadesir1 天前
Ubuntu系统安装Miniconda完整指南
linux·运维·ubuntu
开开心心就好1 天前
右键菜单管理工具,添加程序自定义名称位置
linux·运维·服务器·ci/cd·docker·pdf·1024程序员节
qq_310658511 天前
webrtc源码走读(六)核心引擎层——安全模块
服务器·c++·音视频·webrtc
qq_310658511 天前
webrtc源码走读(七)核心引擎层——Qos模块
服务器·c++·音视频·webrtc
Red丶哞1 天前
Docker 部署 File Browser 文件管理系统
运维·docker·容器
oMcLin1 天前
如何在CentOS 7服务器上优化MySQL 8.0数据库性能并进行高并发处理?
服务器·数据库·centos
Red丶哞1 天前
使用Docker部署RustFS分布式对象存储服务
linux·docker·云原生
l1t1 天前
使用docker安装sql server linux版
linux·sql·docker·容器·sqlserver