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

相关推荐
maosheng11463 小时前
RHCSA的第一次作业
linux·运维·服务器
wifi chicken4 小时前
Linux 端口扫描及拓展
linux·端口扫描·网络攻击
旺仔.2914 小时前
Linux 信号详解
linux·运维·网络
放飞梦想C4 小时前
CPU Cache
linux·cache
Hoshino.415 小时前
基于Linux中的数据库操作——下载与安装(1)
linux·运维·数据库
恒创科技HK5 小时前
通用型云服务器与计算型云服务器:您真正需要哪些配置?
运维·服务器
吴佳浩 Alben6 小时前
GPU 生产环境实践:硬件拓扑、显存管理与完整运维体系
运维·人工智能·pytorch·语言模型·transformer·vllm
播播资源6 小时前
CentOS系统 + 宝塔面板 部署 OpenClaw源码开发版完整教程
linux·运维·centos
源远流长jerry7 小时前
在 Ubuntu 22.04 上配置 Soft-RoCE 并运行 RDMA 测试程序
linux·服务器·网络·tcp/ip·ubuntu·架构·ip
学不完的7 小时前
Docker数据卷管理及优化
运维·docker·容器·eureka