← back to hs3180__disclaude

Function bodies 284 total

All specs Real LLM only Function bodies
MessageLogger.createFileHeader method · typescript · L256-L267 (12 LOC)
src/feishu/message-logger.ts
  private createFileHeader(chatId: string): string {
    const now = new Date().toISOString();
    return `# Chat Message Log: ${chatId}

**Chat ID**: ${chatId}
**Created**: ${now}
**Last Updated**: ${now}

---

`;
  }
MessageLogger.getChatHistory method · typescript · L272-L280 (9 LOC)
src/feishu/message-logger.ts
  async getChatHistory(chatId: string): Promise<string> {
    const logPath = this.getChatLogPath(chatId);

    try {
      return await fs.readFile(logPath, 'utf-8');
    } catch (_error) {
      return '';
    }
  }
MessageSender.sendText method · typescript · L43-L92 (50 LOC)
src/feishu/message-sender.ts
  async sendText(chatId: string, text: string, parentId?: string): Promise<void> {
    try {
      // Always use plain text format
      // Use content builder utility for consistent message formatting
      const messageData: {
        receive_id: string;
        msg_type: string;
        content: string;
        parent_id?: string;
      } = {
        receive_id: chatId,
        msg_type: 'text',
        content: buildTextContent(text),
      };

      // Add parent_id for thread replies if provided
      if (parentId) {
        messageData.parent_id = parentId;
      }

      const response = await this.client.im.message.create({
        params: {
          receive_id_type: 'chat_id',
        },
        data: messageData,
      });

      // Track outgoing bot message in history
      // Feishu API returns message_id in response.data.message_id
      const botMessageId = response?.data?.message_id;
      if (botMessageId) {
        // Log to persistent MD file
        await messageL
MessageSender.sendCard method · typescript · L103-L156 (54 LOC)
src/feishu/message-sender.ts
  async sendCard(
    chatId: string,
    card: Record<string, unknown>,
    description?: string,
    parentId?: string
  ): Promise<void> {
    try {
      const messageData: {
        receive_id: string;
        msg_type: string;
        content: string;
        parent_id?: string;
      } = {
        receive_id: chatId,
        msg_type: 'interactive',
        content: JSON.stringify(card),
      };

      // Add parent_id for thread replies if provided
      if (parentId) {
        messageData.parent_id = parentId;
      }

      const response = await this.client.im.message.create({
        params: {
          receive_id_type: 'chat_id',
        },
        data: messageData,
      });

      // Track outgoing bot message in history
      const botMessageId = response?.data?.message_id;
      if (botMessageId) {
        // Log card content to persistent MD file
        const cardContent = description
          ? `[Card] ${description}\n\`\`\`json\n${JSON.stringify(card, null, 2)}\
MessageSender.sendFile method · typescript · L166-L185 (20 LOC)
src/feishu/message-sender.ts
  async sendFile(chatId: string, filePath: string, parentId?: string): Promise<void> {
    try {
      const { uploadAndSendFile } = await import('./file-uploader.js');
      const fileSize = await uploadAndSendFile(this.client, filePath, chatId, parentId);

      // Log file message to persistent MD file
      const fileName = path.basename(filePath);
      const fileContent = `[File] ${fileName}\nPath: ${filePath}`;
      await messageLogger.logOutgoingMessage(
        `file_${Date.now()}`, // File messages may not have a message_id
        chatId,
        fileContent
      );

      this.logger.info({ chatId, filePath, fileSize, parentId }, 'File sent to user');
    } catch (error) {
      this.logger.error({ err: error, filePath, chatId, parentId }, 'Failed to send file to user');
      // Don't throw - file sending failure shouldn't break the main flow
    }
  }
MessageSender.addReaction method · typescript · L195-L215 (21 LOC)
src/feishu/message-sender.ts
  async addReaction(messageId: string, emoji: string): Promise<boolean> {
    try {
      await this.client.im.messageReaction.create({
        path: {
          message_id: messageId,
        },
        data: {
          reaction_type: {
            emoji_type: emoji,
          },
        },
      });

      this.logger.debug({ messageId, emoji }, 'Reaction added');
      return true;
    } catch (error) {
      // Log error but don't throw - reaction failure shouldn't break message processing
      this.logger.warn({ err: error, messageId, emoji }, 'Failed to add reaction');
      return false;
    }
  }
createClient function · typescript · L13-L26 (14 LOC)
src/feishu/sender.ts
function createClient(): lark.Client {
  const appId = Config.FEISHU_APP_ID;
  const appSecret = Config.FEISHU_APP_SECRET;

  if (!appId || !appSecret) {
    throw new Error('FEISHU_APP_ID and FEISHU_APP_SECRET must be set in environment variables');
  }

  return new lark.Client({
    appId,
    appSecret,
    domain: lark.Domain.Feishu,
  });
}
Repobility · code-quality intelligence · https://repobility.com
createFeishuSender function · typescript · L34-L80 (47 LOC)
src/feishu/sender.ts
export function createFeishuSender(): (chatId: string, text: string) => Promise<void> {
  const client = createClient();

  /**
   * Send a message to Feishu via REST API.
   * Uses plain text format for reliability.
   *
   * Note on Rich Text (post) Format:
   * According to official documentation, post format should work with:
   * {post: {zh_cn: {content: [[{tag: 'text', text: '...'}]]}}}
   * After extensive testing with different approaches (domain, content format, etc.),
   * the API still returns 230001 errors.
   * Plain text format is reliable and works consistently.
   *
   * Research sources:
   * - https://open.feishu.cn/document/server-docs/im-v1/message/create
   * - https://open.larksuite.com/document/ukTMukTMukTM/uMDMxEjLzATMx4yMwETM
   * - https://github.com/larksuite/node-sdk
   *
   * @param chatId - Target chat ID to send message to
   * @param text - Message content (plain text)
   */
  return async function sendMessage(chatId: string, text: string): Promise<void>
createFeishuCardSender function · typescript · L88-L117 (30 LOC)
src/feishu/sender.ts
export function createFeishuCardSender(): (chatId: string, card: Record<string, unknown>) => Promise<void> {
  const client = createClient();

  /**
   * Send an interactive card to Feishu via REST API.
   *
   * @param chatId - Target chat ID to send card to
   * @param card - Card JSON structure
   */
  return async function sendCard(chatId: string, card: Record<string, unknown>): Promise<void> {
    try {
      await client.im.message.create({
        params: {
          receive_id_type: 'chat_id',
        },
        data: {
          receive_id: chatId,
          msg_type: 'interactive',
          content: JSON.stringify(card),
        },
      });

      console.error(`[Feishu] Sent card to ${chatId}`);
    } catch (error) {
      // Log error but don't crash
      console.error('[Feishu Error] Failed to send card:', error);
      throw error; // Re-throw to let caller handle it
    }
  };
}
TaskFlowOrchestrator.constructor method · typescript · L38-L56 (19 LOC)
src/feishu/task-flow-orchestrator.ts
  constructor(
    _taskTracker: TaskTracker,
    messageCallbacks: MessageCallbacks,
    logger: Logger
  ) {
    this.messageCallbacks = messageCallbacks;
    this.logger = logger;

    // Initialize file watcher
    const workspaceDir = Config.getWorkspaceDir();
    const tasksDir = path.join(workspaceDir, 'tasks');

    this.fileWatcher = new TaskFileWatcher({
      tasksDir,
      onTaskCreated: (taskPath, messageId, chatId) => {
        this.executeDialoguePhase(chatId, messageId, taskPath);
      },
    });
  }
TaskFlowOrchestrator.executeDialoguePhase method · typescript · L84-L108 (25 LOC)
src/feishu/task-flow-orchestrator.ts
  executeDialoguePhase(
    chatId: string,
    messageId: string,
    taskPath: string
  ): void {
    const agentConfig = Config.getAgentConfig();

    // Run dialogue asynchronously in background
    void this.runDialogue(chatId, messageId, taskPath, agentConfig)
      .catch((error) => {
        this.logger.error({ err: error, chatId, messageId }, 'Async dialogue failed');
        // Send error notification to user (as thread reply)
        this.messageCallbacks.sendMessage(chatId, `❌ 后台任务执行失败: ${error instanceof Error ? error.message : String(error)}`, messageId)
          .catch((sendError) => {
            this.logger.error({ err: sendError }, 'Failed to send error notification');
          });
      })
      .finally(() => {
        // Clean up tracking
        this.runningDialogueTasks.delete(messageId);
        this.logger.debug({ messageId }, 'Async dialogue task completed and cleaned up');
      });

    this.logger.info({ messageId, chatId }, 'Dialogue phase started async'
TaskFlowOrchestrator.runDialogue method · typescript · L113-L211 (99 LOC)
src/feishu/task-flow-orchestrator.ts
  private async runDialogue(
    chatId: string,
    messageId: string,
    taskPath: string,
    agentConfig: { apiKey: string; model: string; apiBaseUrl?: string }
  ): Promise<void> {
    // Import MCP tools to set message tracking callback
    const { setMessageSentCallback } = await import('../mcp/feishu-context-mcp.js');

    // Create bridge with agent configs
    const bridge = new DialogueOrchestrator({
      evaluatorConfig: {
        apiKey: agentConfig.apiKey,
        model: agentConfig.model,
        apiBaseUrl: agentConfig.apiBaseUrl,
        permissionMode: 'bypassPermissions',
      },
    });

    // Set the message sent callback to track when MCP tools send messages
    const messageTracker = bridge.getMessageTracker();
    setMessageSentCallback((_chatId: string) => {
      messageTracker.recordMessageSent();
    });

    // Create output adapter for this chat
    // Pass messageId as parentMessageId for thread replies
    const adapter = new FeishuOutputAdapter({
  
buildWriteContentCard function · typescript · L63-L114 (52 LOC)
src/feishu/write-card-builder.ts
export function buildWriteContentCard(
  writeContent: WriteContent,
  title: string = '✍️ 文件写入',
  template: string = 'green',
  config: WritePreviewConfig = {}
): Record<string, unknown> {
  const elements: Record<string, unknown>[] = [];

  // Merge config with defaults
  const previewConfig = { ...DEFAULT_CONFIG, ...config };

  // File header with language badge and line count
  const languageBadge = writeContent.language ? `\`${writeContent.language}\`` : '';
  const lineCountText = `${writeContent.totalLines} 行`;
  const truncatedBadge = writeContent.isTruncated ? ' *(已截断)*' : '';

  const headerText = `**📄 ${escapeHtml(writeContent.filePath)}** ${languageBadge} • ${lineCountText}${truncatedBadge}\n`;

  elements.push({
    tag: 'markdown',
    content: headerText,
  });

  // Generate content preview
  const contentPreview = generateContentPreview(writeContent, previewConfig);
  elements.push({
    tag: 'markdown',
    content: contentPreview,
  });

  // Add truncation notice 
generateContentPreview function · typescript · L123-L170 (48 LOC)
src/feishu/write-card-builder.ts
function generateContentPreview(writeContent: WriteContent, config: Required<WritePreviewConfig>): string {
  const lines: string[] = [];

  // Build code block with language
  const language = writeContent.language ?? 'text';

  lines.push(`\`\`\`${  language}`);

  // Add each line with line number
  let startLineNumber = 1;

  if (writeContent.isTruncated) {
    // Truncated mode: show start lines, ellipsis, end lines
    for (let i = 0; i < writeContent.previewLines.length; i++) {
      const line = writeContent.previewLines[i];

      // Check if we're in the ellipsis section
      if (line === TRUNCATION_MARKER) {
        lines.push('');
        lines.push('⋮');
        lines.push('');

        // Adjust line number for the end section
        const linesRemaining = writeContent.previewLines.length - i - 1;
        startLineNumber = writeContent.totalLines - linesRemaining + 1;
        continue;
      }

      const lineNumber = i < config.contextLines
        ? i + 1
        : s
parseWriteToolInput function · typescript · L185-L238 (54 LOC)
src/feishu/write-card-builder.ts
export function parseWriteToolInput(
  input: Record<string, unknown> | undefined,
  config: WritePreviewConfig = {}
): WriteContent | null {
  if (!input) { return null; }

  // SDK uses snake_case for Write tool parameters
  const filePath = (input.file_path as string | undefined) || (input.filePath as string | undefined);
  const content = input.content as string | undefined;

  if (!filePath) { return null; }
  if (content === undefined) { return null; }

  // Merge config with defaults
  const previewConfig = { ...DEFAULT_CONFIG, ...config };

  // Detect language from file extension
  const language = detectLanguage(filePath);

  // Split content into lines
  const allLines = content.split('\n');
  const totalLines = allLines.length;

  // Determine if truncation is needed
  const isTruncated = totalLines > previewConfig.maxLines;

  // Generate preview lines
  let previewLines: string[];

  if (isTruncated) {
    // Show first N lines and last N lines with truncation marker
    
Repobility — same analyzer, your code, free for public repos · /scan/
truncateLine function · typescript · L247-L254 (8 LOC)
src/feishu/write-card-builder.ts
function truncateLine(line: string, maxLength: number): string {
  if (line.length <= maxLength) {
    return line;
  }

  // Truncate and add ellipsis
  return `${line.substring(0, maxLength - 3)  }...`;
}
detectLanguage function · typescript · L262-L322 (61 LOC)
src/feishu/write-card-builder.ts
function detectLanguage(filePath: string): string {
  const ext = filePath.split('.').pop()?.toLowerCase();

  const languageMap: Record<string, string> = {
    // Web/Scripting
    js: 'javascript',
    jsx: 'javascript',
    ts: 'typescript',
    tsx: 'typescript',
    vue: 'vue',
    svelte: 'svelte',
    css: 'css',
    scss: 'scss',
    less: 'less',
    html: 'html',
    htm: 'html',
    json: 'json',
    xml: 'xml',

    // Backend
    py: 'python',
    rb: 'ruby',
    php: 'php',
    java: 'java',
    kt: 'kotlin',
    scala: 'scala',
    go: 'go',
    rs: 'rust',
    cpp: 'cpp',
    cc: 'cpp',
    cxx: 'cpp',
    c: 'c',
    h: 'c',
    hpp: 'cpp',
    cs: 'csharp',
    fs: 'fsharp',
    swift: 'swift',
    dart: 'dart',
    lua: 'lua',
    r: 'r',

    // Config/Markup
    yaml: 'yaml',
    yml: 'yaml',
    toml: 'toml',
    ini: 'ini',
    conf: 'ini',
    md: 'markdown',
    markdown: 'markdown',
    sh: 'bash',
    bash: 'bash',
    zsh: 'bash',
    fish: 'bash',
    sql: 
escapeHtml function · typescript · L330-L337 (8 LOC)
src/feishu/write-card-builder.ts
function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}
sendMessageToFeishu function · typescript · L60-L89 (30 LOC)
src/mcp/feishu-context-mcp.ts
async function sendMessageToFeishu(
  client: lark.Client,
  chatId: string,
  msgType: 'text' | 'interactive',
  content: string,
  parentId?: string
): Promise<void> {
  const messageData: {
    receive_id: string;
    msg_type: string;
    content: string;
    parent_id?: string;
  } = {
    receive_id: chatId,
    msg_type: msgType,
    content,
  };

  // Add parent_id for thread replies if provided
  if (parentId) {
    messageData.parent_id = parentId;
  }

  await client.im.message.create({
    params: {
      receive_id_type: 'chat_id',
    },
    data: messageData,
  });
}
isValidFeishuCard function · typescript · L98-L110 (13 LOC)
src/mcp/feishu-context-mcp.ts
function isValidFeishuCard(content: Record<string, unknown>): boolean {
  return (
    typeof content === 'object' &&
    content !== null &&
    'config' in content &&
    'header' in content &&
    'elements' in content &&
    Array.isArray(content.elements) &&
    typeof content.header === 'object' &&
    content.header !== null &&
    'title' in content.header
  );
}
getCardValidationError function · typescript · L119-L155 (37 LOC)
src/mcp/feishu-context-mcp.ts
function getCardValidationError(content: unknown): string {
  if (content === null) {
    return 'content is null';
  }
  if (typeof content !== 'object') {
    return `content is ${typeof content}, expected object`;
  }
  if (Array.isArray(content)) {
    return 'content is array, expected object with config/header/elements';
  }

  const obj = content as Record<string, unknown>;
  const missing: string[] = [];

  if (!('config' in obj)) missing.push('config');
  if (!('header' in obj)) missing.push('header');
  if (!('elements' in obj)) missing.push('elements');

  if (missing.length > 0) {
    return `missing required fields: ${missing.join(', ')}`;
  }

  // Check header structure
  if (typeof obj.header !== 'object' || obj.header === null) {
    return 'header must be an object';
  }
  if (!('title' in (obj.header as Record<string, unknown>))) {
    return 'header.title is missing';
  }

  // Check elements structure
  if (!Array.isArray(obj.elements)) {
    return 'elements must 
send_user_feedback function · typescript · L173-L182 (10 LOC)
src/mcp/feishu-context-mcp.ts
export async function send_user_feedback(params: {
  content: string | Record<string, unknown>;
  format: 'text' | 'card';
  chatId: string;
  parentMessageId?: string;
}): Promise<{
  success: boolean;
  message: string;
  error?: string;
}> {
send_file_to_feishu function · typescript · L358-L372 (15 LOC)
src/mcp/feishu-context-mcp.ts
export async function send_file_to_feishu(params: {
  filePath: string;
  chatId: string;
}): Promise<{
  success: boolean;
  message: string;
  fileName?: string;
  fileSize?: number;
  sizeMB?: string;
  error?: string;
  feishuCode?: string | number;
  feishuMsg?: string;
  feishuLogId?: string;
  troubleshooterUrl?: string;
}> {
If a scraper extracted this row, it came from Repobility (https://repobility.com)
createFeishuSdkMcpServer function · typescript · L674-L680 (7 LOC)
src/mcp/feishu-context-mcp.ts
export function createFeishuSdkMcpServer() {
  return createSdkMcpServer({
    name: 'feishu-context',
    version: '1.0.0',
    tools: feishuSdkTools,
  });
}
send_file_to_feishu function · typescript · L28-L98 (71 LOC)
src/mcp/feishu-mcp-server.ts
async function send_file_to_feishu(args: { filePath: string; chatId: string }) {
  const { filePath, chatId } = args;

  try {
    if (!chatId) {
      throw new Error('chatId is required - cannot send file');
    }

    const appId = process.env.FEISHU_APP_ID;
    const appSecret = process.env.FEISHU_APP_SECRET;
    if (!appId || !appSecret) {
      throw new Error('FEISHU_APP_ID and FEISHU_APP_SECRET must be set in environment variables');
    }

    // Resolve file path
    const workspaceDir = process.env.WORKSPACE_DIR || process.cwd();
    const resolvedPath = path.isAbsolute(filePath)
      ? filePath
      : path.join(workspaceDir, filePath);

    logger.info({ filePath, resolvedPath, workspaceDir, chatId }, 'MCP tool: send_file_to_feishu called');

    // Check file exists
    const stats = await fs.stat(resolvedPath);
    if (!stats.isFile()) {
      throw new Error(`Path is not a file: ${filePath}`);
    }

    // Create Lark client
    const client = new lark.Client({
      
handleMessage function · typescript · L103-L183 (81 LOC)
src/mcp/feishu-mcp-server.ts
async function handleMessage(message: unknown) {
  const msg = message as Record<string, unknown>;
  const { id, method, params } = msg;

  try {
    switch (method) {
      case 'tools/list':
        // Return list of available tools
        return {
          jsonrpc: '2.0',
          id,
          result: {
            tools: [
              {
                name: 'send_file_to_feishu',
                description: 'Send a file to a Feishu chat. Use the chatId from the current context (marked as [Current Chat ID: xxx] in the prompt). Supports images, audio, video, and documents.',
                inputSchema: {
                  type: 'object',
                  properties: {
                    filePath: {
                      type: 'string',
                      description: 'Path to the file to send (relative to workspace or absolute)',
                    },
                    chatId: {
                      type: 'string',
                      description: 'Feishu chat ID 
main function · typescript · L188-L231 (44 LOC)
src/mcp/feishu-mcp-server.ts
function main() {
  logger.info('Starting Feishu MCP Server (stdio)');

  let buffer = '';

  process.stdin.setEncoding('utf-8');
  process.stdin.on('data', (chunk) => {
    buffer += chunk;
    const lines = buffer.split('\n');
    buffer = lines.pop() || '';

    for (const line of lines) {
      if (line.trim()) {
        try {
          const message = JSON.parse(line);
          handleMessage(message).then(response => {
            process.stdout.write(`${JSON.stringify(response)}\n`);
          }).catch(error => {
            logger.error({ err: error }, 'Error handling message');
            const errorResponse = {
              jsonrpc: '2.0',
              id: (message as Record<string, unknown>).id,
              error: {
                code: -32603,
                message: error instanceof Error ? error.message : 'Unknown error',
              },
            };
            process.stdout.write(`${JSON.stringify(errorResponse)}\n`);
          });
        } catch (error) {
 
send_file_to_feishu function · typescript · L34-L42 (9 LOC)
src/mcp/feishu-tools-server.ts
export async function send_file_to_feishu(params: { filePath: string; chatId: string }): Promise<{
  success: boolean;
  message: string;
  fileName?: string;
  fileSize?: number;
  sizeMB?: string;
  filePath?: string;
  error?: string;
}> {
CommunicationNode.constructor method · typescript · L85-L133 (49 LOC)
src/nodes/communication-node.ts
  constructor(config: CommunicationNodeConfig) {
    super();
    this.port = config.port;
    this.host = config.host || '0.0.0.0';

    // Store file storage config for later initialization
    this.fileStorageConfig = config.fileStorage;

    // Register custom channels if provided
    if (config.channels) {
      for (const channel of config.channels) {
        this.registerChannel(channel);
      }
    }

    // Create Feishu channel (for backward compatibility)
    const appId = config.appId || Config.FEISHU_APP_ID;
    const appSecret = config.appSecret || Config.FEISHU_APP_SECRET;
    if (appId && appSecret) {
      const feishuChannel = new FeishuChannel({
        id: 'feishu',
        appId,
        appSecret,
      });

      // Initialize TaskFlowOrchestrator for Feishu channel
      feishuChannel.initTaskFlowOrchestrator({
        sendMessage: this.sendMessage.bind(this),
        sendCard: this.sendCard.bind(this),
        sendFile: this.sendFileToUser.bind(this),
      })
CommunicationNode.registerChannel method · typescript · L138-L156 (19 LOC)
src/nodes/communication-node.ts
  registerChannel(channel: IChannel): void {
    if (this.channels.has(channel.id)) {
      logger.warn({ channelId: channel.id }, 'Channel already registered, replacing');
    }

    this.channels.set(channel.id, channel);

    // Set up message handler
    channel.onMessage(async (message: IncomingMessage) => {
      await this.handleChannelMessage(channel.id, message);
    });

    // Set up control handler
    channel.onControl((command: ControlCommand) => {
      return this.handleControlCommand(command);
    });

    logger.info({ channelId: channel.id, channelName: channel.name }, 'Channel registered');
  }
CommunicationNode.startWebSocketServer method · typescript · L175-L256 (82 LOC)
src/nodes/communication-node.ts
  private async startWebSocketServer(): Promise<void> {
    // Initialize file storage service if configured
    if (this.fileStorageConfig) {
      this.fileStorageService = new FileStorageService(this.fileStorageConfig);
      await this.fileStorageService.initialize();
      logger.info('File storage service initialized');
    }

    // Create file API handler
    const fileApiHandler = this.fileStorageService
      ? createFileTransferAPIHandler({ storageService: this.fileStorageService })
      : null;

    // Create HTTP server for health check, file API, and WebSocket upgrade
    this.httpServer = http.createServer(async (req, res) => {
      const url = req.url || '/';

      // Handle file API requests
      if (fileApiHandler && url.startsWith('/api/files')) {
        const handled = await fileApiHandler(req, res);
        if (handled) {return;}
      }

      // Health check
      if (url === '/health') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
  
Same scanner, your repo: https://repobility.com — Repobility
CommunicationNode.handleChannelMessage method · typescript · L261-L296 (36 LOC)
src/nodes/communication-node.ts
  private async handleChannelMessage(channelId: string, message: IncomingMessage): Promise<void> {
    // Route chat to channel
    this.chatToChannel.set(message.chatId, channelId);

    // Process attachments if present
    let attachments: FileReference[] | undefined;
    if (message.attachments && message.attachments.length > 0 && this.fileStorageService) {
      attachments = [];
      for (const att of message.attachments) {
        try {
          const fileRef = await this.fileStorageService.storeFromLocal(
            att.filePath,
            att.fileName,
            att.mimeType,
            'user',
            message.chatId
          );
          attachments.push(fileRef);
          logger.info({ fileId: fileRef.id, fileName: att.fileName }, 'Attachment stored');
        } catch (error) {
          logger.error({ err: error, fileName: att.fileName }, 'Failed to store attachment');
        }
      }
    }

    // Send prompt to Execution Node
    await this.sendPrompt({
  
CommunicationNode.handleControlCommand method · typescript · L301-L325 (25 LOC)
src/nodes/communication-node.ts
  private async handleControlCommand(command: ControlCommand): Promise<ControlResponse> {
    switch (command.type) {
      case 'reset':
        await this.sendCommand({ type: 'command', command: 'reset', chatId: command.chatId });
        return { success: true, message: '✅ **对话已重置**\n\n新的会话已启动,之前的上下文已清除。' };

      case 'restart':
        await this.sendCommand({ type: 'command', command: 'restart', chatId: command.chatId });
        return { success: true, message: '🔄 **正在重启服务...**' };

      case 'status':
        const status = this.running ? 'Running' : 'Stopped';
        const execConnected = this.execWs?.readyState === WebSocket.OPEN ? 'Connected' : 'Disconnected';
        const channelStatus = Array.from(this.channels.entries())
          .map(([_id, ch]) => `${ch.name}: ${ch.status}`)
          .join(', ');
        return {
          success: true,
          message: `📊 **状态**\n\n状态: ${status}\nExecution Node: ${execConnected}\nChannels: ${channelStatus}`,
        };

      
CommunicationNode.sendPrompt method · typescript · L330-L339 (10 LOC)
src/nodes/communication-node.ts
  private async sendPrompt(message: PromptMessage): Promise<void> {
    if (!this.execWs || this.execWs.readyState !== WebSocket.OPEN) {
      logger.warn('No Execution Node connected');
      await this.sendMessage(message.chatId, '❌ 没有可用的执行节点');
      throw new Error('No Execution Node connected');
    }

    this.execWs.send(JSON.stringify(message));
    logger.info({ chatId: message.chatId, messageId: message.messageId, threadId: message.threadId }, 'Prompt sent to Execution Node');
  }
CommunicationNode.sendCommand method · typescript · L344-L353 (10 LOC)
src/nodes/communication-node.ts
  private async sendCommand(message: CommandMessage): Promise<void> {
    if (!this.execWs || this.execWs.readyState !== WebSocket.OPEN) {
      logger.warn('No Execution Node connected');
      await this.sendMessage(message.chatId, '❌ 没有可用的执行节点');
      throw new Error('No Execution Node connected');
    }

    this.execWs.send(JSON.stringify(message));
    logger.info({ chatId: message.chatId, command: message.command }, 'Command sent to Execution Node');
  }
CommunicationNode.handleFeedback method · typescript · L358-L403 (46 LOC)
src/nodes/communication-node.ts
  private async handleFeedback(message: FeedbackMessage): Promise<void> {
    const { chatId, type, text, card, error, threadId, fileRef } = message;

    try {
      switch (type) {
        case 'text':
          if (text) {
            await this.sendMessage(chatId, text, threadId);
          }
          break;
        case 'card':
          await this.sendCard(chatId, card || {}, undefined, threadId);
          break;
        case 'file':
          if (fileRef && this.fileStorageService) {
            const localPath = this.fileStorageService.getLocalPath(fileRef.id);
            if (localPath) {
              await this.sendFileToUser(chatId, localPath, threadId);
            } else {
              logger.error({ fileId: fileRef.id }, 'File not found in storage');
              await this.sendMessage(chatId, `❌ 文件未找到: ${fileRef.fileName}`, threadId);
            }
          }
          break;
        case 'done':
          logger.info({ chatId }, 'Execution completed');
          /
CommunicationNode.sendMessage method · typescript · L408-L427 (20 LOC)
src/nodes/communication-node.ts
  async sendMessage(chatId: string, text: string, threadMessageId?: string): Promise<void> {
    const channelId = this.chatToChannel.get(chatId);
    if (!channelId) {
      logger.warn({ chatId }, 'No channel found for chat');
      return;
    }

    const channel = this.channels.get(channelId);
    if (!channel) {
      logger.warn({ chatId, channelId }, 'Channel not found');
      return;
    }

    await channel.sendMessage({
      chatId,
      type: 'text',
      text,
      threadId: threadMessageId,
    });
  }
CommunicationNode.sendCard method · typescript · L432-L457 (26 LOC)
src/nodes/communication-node.ts
  async sendCard(
    chatId: string,
    card: Record<string, unknown>,
    description?: string,
    threadMessageId?: string
  ): Promise<void> {
    const channelId = this.chatToChannel.get(chatId);
    if (!channelId) {
      logger.warn({ chatId }, 'No channel found for chat');
      return;
    }

    const channel = this.channels.get(channelId);
    if (!channel) {
      logger.warn({ chatId, channelId }, 'Channel not found');
      return;
    }

    await channel.sendMessage({
      chatId,
      type: 'card',
      card,
      description,
      threadId: threadMessageId,
    });
  }
CommunicationNode.sendFileToUser method · typescript · L462-L481 (20 LOC)
src/nodes/communication-node.ts
  async sendFileToUser(chatId: string, filePath: string, _threadId?: string): Promise<void> {
    const channelId = this.chatToChannel.get(chatId);
    if (!channelId) {
      logger.warn({ chatId }, 'No channel found for chat');
      return;
    }

    const channel = this.channels.get(channelId);
    if (!channel) {
      logger.warn({ chatId, channelId }, 'Channel not found');
      return;
    }

    // TODO: Pass threadId when Issue #68 is implemented
    await channel.sendMessage({
      chatId,
      type: 'file',
      filePath,
    });
  }
Repobility · code-quality intelligence · https://repobility.com
CommunicationNode.start method · typescript · L486-L516 (31 LOC)
src/nodes/communication-node.ts
  async start(): Promise<void> {
    if (this.running) {
      logger.warn('CommunicationNode already running');
      return;
    }

    this.running = true;

    // Start WebSocket server for Execution Node connections
    await this.startWebSocketServer();

    // Start all registered channels
    for (const [channelId, channel] of this.channels) {
      try {
        await channel.start();
        logger.info({ channelId }, 'Channel started');
      } catch (error) {
        logger.error({ err: error, channelId }, 'Failed to start channel');
      }
    }

    logger.info('CommunicationNode started');
    console.log('✓ Communication Node ready');
    console.log();
    console.log(`WebSocket Server: ws://${this.host}:${this.port}`);
    console.log('Channels:');
    for (const [id, channel] of this.channels) {
      console.log(`  - ${channel.name} (${id}): ${channel.status}`);
    }
    console.log('Waiting for Execution Node to connect...');
  }
CommunicationNode.stop method · typescript · L521-L561 (41 LOC)
src/nodes/communication-node.ts
  async stop(): Promise<void> {
    if (!this.running) {return;}

    this.running = false;

    // Shutdown file storage service
    if (this.fileStorageService) {
      this.fileStorageService.shutdown();
      this.fileStorageService = undefined;
    }

    // Stop all channels
    for (const [channelId, channel] of this.channels) {
      try {
        await channel.stop();
        logger.info({ channelId }, 'Channel stopped');
      } catch (error) {
        logger.error({ err: error, channelId }, 'Failed to stop channel');
      }
    }

    // Close WebSocket connection from Execution Node
    if (this.execWs) {
      this.execWs.close();
      this.execWs = undefined;
    }

    // Close WebSocket server
    if (this.wss) {
      this.wss.close();
      this.wss = undefined;
    }

    // Close HTTP server
    if (this.httpServer) {
      this.httpServer.close();
      this.httpServer = undefined;
    }

    logger.info('CommunicationNode stopped');
  }
runCliMode function · typescript · L68-L149 (82 LOC)
src/runners/cli-runner.ts
export async function runCliMode(config: CliModeConfig): Promise<void> {
  const { prompt, feishuChatId } = config;

  // Create unique IDs for this CLI session
  const messageId = `cli-${Date.now()}`;
  const chatId = feishuChatId || 'cli-console';

  logger.info({ prompt: prompt.slice(0, 100), feishuChatId }, 'Starting CLI mode');

  // Create output adapter
  let adapter: ExtendedOutputAdapter;

  if (feishuChatId) {
    // Feishu mode: Use FeishuOutputAdapter
    const sendMessageFn = createFeishuSender();
    const sendCardFn = createFeishuCardSender();

    adapter = new FeishuOutputAdapter({
      sendMessage: async (chatId: string, msg: string) => {
        await sendMessageFn(chatId, msg);
      },
      sendCard: async (chatId: string, card: Record<string, unknown>) => {
        await sendCardFn(chatId, card);
      },
      chatId: feishuChatId,
      throttleIntervalMs: 2000,
    });
    logger.info({ chatId: feishuChatId }, 'Output will be sent to Feishu chat');
  } else {
runCli function · typescript · L154-L231 (78 LOC)
src/runners/cli-runner.ts
export async function runCli(args: string[]): Promise<void> {
  const globalArgs = parseGlobalArgs(args);
  const cliConfig = getCliModeConfig(globalArgs);

  // Handle feishu-chat-id "auto" special value
  let feishuChatId = cliConfig?.feishuChatId || globalArgs.feishuChatId;
  let chatIdSource: 'cli' | 'env' | undefined;

  if (feishuChatId === 'auto') {
    if (Config.FEISHU_CLI_CHAT_ID) {
      feishuChatId = Config.FEISHU_CLI_CHAT_ID;
      chatIdSource = 'env';
    } else {
      logger.error('FEISHU_CLI_CHAT_ID environment variable is not set');
      process.exit(1);
    }
  } else if (feishuChatId) {
    chatIdSource = 'cli';
  }

  // Show usage if no prompt provided
  if (!cliConfig || !cliConfig.prompt.trim()) {
    console.log('');
    console.log(color('═══════════════════════════════════════════════════════', 'cyan'));
    console.log(color('  Disclaude - CLI Mode', 'bold'));
    console.log(color('═════════════════════════════════════════════════════════', 'cyan'));
   
runCommunicationNode function · typescript · L25-L80 (56 LOC)
src/runners/communication-runner.ts
export async function runCommunicationNode(config?: CommNodeConfig): Promise<void> {
  const globalArgs = parseGlobalArgs();
  const runnerConfig = config || getCommNodeConfig(globalArgs);

  logger.info({
    config: {
      port: runnerConfig.port,
      host: runnerConfig.host,
      authToken: runnerConfig.authToken ? '***' : undefined,
      restPort: runnerConfig.restPort,
      enableRestChannel: runnerConfig.enableRestChannel,
    }
  }, 'Starting Communication Node');

  console.log('Initializing Communication Node...');
  console.log('Mode: Communication (Multi-channel + WebSocket Server)');
  console.log();

  // Increase max listeners
  process.setMaxListeners(20);

  // Create Communication Node with all channels
  const commNode = new CommunicationNode({
    port: runnerConfig.port,
    host: runnerConfig.host,
    appId: Config.FEISHU_APP_ID || undefined,
    appSecret: Config.FEISHU_APP_SECRET || undefined,
    restPort: runnerConfig.restPort,
    enableRestChannel: run
connectToCommNode function · typescript · L249-L355 (107 LOC)
src/runners/execution-runner.ts
  function connectToCommNode(): void {
    if (ws?.readyState === WebSocket.OPEN) {
      return;
    }

    logger.info({ url: commUrl }, 'Connecting to Communication Node...');

    ws = new WebSocket(commUrl);

    ws.on('open', () => {
      logger.info('Connected to Communication Node');
      console.log('✓ Connected to Communication Node');
      console.log();
    });

    ws.on('message', async (data) => {
      try {
        const message = JSON.parse(data.toString()) as PromptMessage | CommandMessage;

        // Handle command messages
        if (message.type === 'command') {
          const { command, chatId } = message;
          logger.info({ command, chatId }, 'Received command');

          try {
            if (command === 'reset') {
              // Use reset(chatId) to only reset the specific chat, not all chats
              sharedPilot.reset(chatId);
              logger.info({ chatId }, 'Pilot reset executed for chatId');
            } else if (command === 'rest
scheduleReconnect function · typescript · L360-L370 (11 LOC)
src/runners/execution-runner.ts
  function scheduleReconnect(): void {
    if (reconnectTimer) {
      clearTimeout(reconnectTimer);
    }

    reconnectTimer = setTimeout(() => {
      if (running) {
        connectToCommNode();
      }
    }, reconnectInterval);
  }
slugify function · typescript · L112-L117 (6 LOC)
src/schedule/schedule-file-scanner.ts
function slugify(name: string): string {
  return name
    .toLowerCase()
    .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-') // Keep alphanumeric and Chinese
    .replace(/^-+|-+$/g, ''); // Remove leading/trailing dashes
}
Repobility — same analyzer, your code, free for public repos · /scan/
ScheduleFileScanner.scanAll method · typescript · L148-L175 (28 LOC)
src/schedule/schedule-file-scanner.ts
  async scanAll(): Promise<ScheduleFileTask[]> {
    await this.ensureDir();

    const tasks: ScheduleFileTask[] = [];

    try {
      const files = await fs.readdir(this.schedulesDir);
      const mdFiles = files.filter(f => f.endsWith('.md'));

      for (const file of mdFiles) {
        const filePath = path.join(this.schedulesDir, file);
        const task = await this.parseFile(filePath);
        if (task) {
          tasks.push(task);
        }
      }

      logger.info({ count: tasks.length }, 'Scanned schedule files');
      return tasks;

    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
        logger.debug('Schedules directory does not exist, returning empty');
        return [];
      }
      throw error;
    }
  }
ScheduleFileScanner.parseFile method · typescript · L183-L220 (38 LOC)
src/schedule/schedule-file-scanner.ts
  async parseFile(filePath: string): Promise<ScheduleFileTask | null> {
    try {
      const content = await fs.readFile(filePath, 'utf-8');
      const stats = await fs.stat(filePath);
      const { frontmatter, contentStart } = parseScheduleFrontmatter(content);

      // Validate required fields
      if (!frontmatter['name'] || !frontmatter['cron'] || !frontmatter['chatId']) {
        logger.warn({ filePath }, 'Schedule file missing required fields (name, cron, chatId)');
        return null;
      }

      const prompt = content.slice(contentStart).trim();
      const fileName = path.basename(filePath);

      const task: ScheduleFileTask = {
        id: generateTaskId(fileName),
        name: frontmatter['name'] as string,
        cron: frontmatter['cron'] as string,
        chatId: frontmatter['chatId'] as string,
        prompt,
        enabled: (frontmatter['enabled'] as boolean) ?? true,
        blocking: (frontmatter['blocking'] as boolean) ?? false,
        createdBy: fron
ScheduleFileScanner.writeTask method · typescript · L228-L265 (38 LOC)
src/schedule/schedule-file-scanner.ts
  async writeTask(task: ScheduledTask): Promise<string> {
    await this.ensureDir();

    // Use task ID to generate file name (task.id = "schedule-{slug}")
    // This ensures file name matches task ID for consistent deletion
    const fileName = task.id.startsWith('schedule-')
      ? `${task.id.slice('schedule-'.length)}.md`
      : `${task.id}.md`;
    const filePath = path.join(this.schedulesDir, fileName);

    const frontmatter = [
      '---',
      `name: "${task.name}"`,
      `cron: "${task.cron}"`,
      `enabled: ${task.enabled}`,
      `blocking: ${task.blocking ?? false}`,
      `chatId: ${task.chatId}`,
    ];

    if (task.createdBy) {
      frontmatter.push(`createdBy: ${task.createdBy}`);
    }
    if (task.createdAt) {
      frontmatter.push(`createdAt: "${task.createdAt}"`);
    }
    if (task.lastExecutedAt) {
      frontmatter.push(`lastExecutedAt: "${task.lastExecutedAt}"`);
    }

    frontmatter.push('---', '');

    const content = frontmatter.join('\n') + t
‹ prevpage 3 / 6next ›