Function bodies 284 total
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 messageLMessageSender.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
: sparseWriteToolInput 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}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: runconnectToCommNode 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 === 'restscheduleReconnect 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: fronScheduleFileScanner.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