index.js 5.23 KB
/**
 * ClaudeStreamProtocol - Claude CLI stream-json 协议实现
 *
 * 职责:只关心 Claude stream-json 消息格式和业务语义。
 * 不关心传输方式和启动方式 — 那是 Transport 和 Launcher 的事。
 */

const { Protocol } = require('../Protocol');
const { StreamLogger } = require('./logger');
const { handleMessage, registerHandler, registerContentBlockHandler } = require('./handlers');

class ClaudeStreamProtocol extends Protocol {
  constructor(config, options = {}) {
    super(config, options);
    this.workingDir = null;
    this.outputChunks = [];
    this.transport = null;

    // 组件
    this.logger = new StreamLogger({
      logDir: options.logDir || '.autodev-logs',
      prefix: options.logPrefix || 'claude-stream'
    });

    this._customHandlers = new Map();
  }

  get name() {
    return 'claude-stream';
  }

  /**
   * 注册自定义消息处理器
   */
  registerMessageHandler(type, handler) {
    registerHandler(type, handler);
    return this;
  }

  /**
   * 注册自定义 content block 处理器
   */
  registerBlockHandler(type, handler) {
    registerContentBlockHandler(type, handler);
    return this;
  }

  /**
   * 绑定已连接的 transport
   * @param {Transport} transport - 已连接的 Transport 实例
   * @param {string} workingDir - 工作目录
   */
  async connect(transport, workingDir) {
    this.transport = transport;
    this.workingDir = workingDir;

    const agentName = this.config.id || this.config.name || 'agent';
    this.logger.init(workingDir, agentName);

    // 监听 Transport 事件
    this.transport.on('message', (msg) => this._handleMessage(msg));
    this.transport.on('output', (line) => this.emit('output', line));
    this.transport.on('stderr', (text) => {
      this.logger.logInternal('stderr', { content: text });
      this._log(`stderr: ${text}`);
      this.emit('stderr', text);
    });
    this.transport.on('close', (code, signal) => {
      this.logger.logInternal('process_close', { code, signal });
      this.logger.close();
      this.isConnected = false;
      this.emit('disconnected', { code, signal });
    });

    this.isConnected = true;
    this._log('连接成功');
  }

  /**
   * 初始化(Claude 不需要显式初始化)
   */
  async initialize() {
    this._log('initialize (no-op for claude)');
    return { sessionId: this.sessionId };
  }

  /**
   * 创建会话(Claude 不支持多会话)
   */
  async newSession(options = {}) {
    this._log('newSession (no-op for claude)');
    return { sessionId: this.sessionId };
  }

  /**
   * 发送提示消息
   */
  async sendPrompt(text, files = []) {
    if (!this.isConnected) {
      throw new Error('Not connected');
    }

    this.outputChunks = [];

    const content = [{ type: 'text', text }];
    for (const file of files) {
      content.push({ type: 'file', path: file });
    }

    const msg = {
      type: 'user',
      message: { role: 'user', content }
    };

    this._log('发送 user message');
    this.logger.logOutgoing(msg);
    this.transport.send(msg);

    return this._waitForResult();
  }

  /**
   * 等待结果
   */
  _waitForResult() {
    return new Promise((resolve, reject) => {
      const handler = (msg) => {
        if (msg.type === 'result') {
          this.removeListener('_message', handler);
          this.removeListener('disconnected', exitHandler);
          resolve({
            result: msg.result,
            is_error: msg.is_error,
            duration_ms: msg.duration_ms
          });
        }
      };

      const exitHandler = ({ code, signal } = {}) => {
        this.removeListener('_message', handler);
        reject(new Error(`Claude process exited unexpectedly (code: ${code}, signal: ${signal})`));
      };

      this.on('_message', handler);
      this.once('disconnected', exitHandler);
    });
  }

  /**
   * 响应权限请求
   */
  respondPermission(requestId, optionId) {
    this._log('respondPermission (not supported in bypass mode)');
  }

  /**
   * 断开连接 — 完整清理协议层资源
   *
   * 清理 Transport 层的事件监听器,防止已关闭的 transport
   * 继续触发回调导致 event loop 无法退出。
   */
  disconnect() {
    // 清理 Transport 层(解除流上的事件监听器)
    if (this.transport) {
      try { this.transport.close(); } catch { }
      this.transport = null;
    }

    // 清理 logger
    try { this.logger.close(); } catch { }

    this.isConnected = false;
    this.sessionId = null;
  }

  /** @private */
  _handleMessage(msg) {
    this.logger.logIncoming(msg);
    this.emit('_message', msg);

    const ctx = this._createContext();
    handleMessage(msg, ctx);

    this.emit('acp_event', {
      type: msg.type,
      subtype: msg.subtype,
      data: msg,
      timestamp: Date.now()
    });
  }

  /** @private */
  _createContext() {
    return {
      protocol: this,
      emit: (event, data) => this.emit(event, data),
      log: (message) => this._log(message),
      appendOutput: (text) => this.outputChunks.push(text),
      getOutput: () => this.outputChunks.join('')
    };
  }

  /** @private */
  _log(message) {
    console.log(`[ClaudeStreamProtocol] ${message}`);
  }

  getLogStats() {
    return this.logger.getStats();
  }
}

module.exports = { ClaudeStreamProtocol };