Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/lib/channels/feishu/inbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@ import type { FeishuConfig } from './types';

const LOG_TAG = '[feishu/inbound]';

/** Find the bot's mention entry in the Feishu mentions array, if present. */
function findBotMention(
mentions: any[] | undefined,
botOpenId: string,
): { key?: string } | undefined {
if (!mentions || !botOpenId) return undefined;
return mentions.find((m: any) => m?.id?.open_id === botOpenId);
}

/** Parse a raw Feishu im.message.receive_v1 event into an InboundMessage. */
export function parseInboundMessage(
eventData: any,
_config: FeishuConfig,
config: FeishuConfig,
botOpenId?: string,
): InboundMessage | null {
try {
const event = eventData?.event ?? eventData;
Expand All @@ -24,7 +34,16 @@ export function parseInboundMessage(
const sender = event.sender?.sender_id?.open_id || '';
const msgType = message.message_type;

// Only handle text messages for now
const isGroupChat = chatId.startsWith('oc_');
const botMention = isGroupChat && botOpenId
? findBotMention(message.mentions, botOpenId)
: undefined;

// When requireMention is enabled, drop group messages that don't @mention the bot.
if (isGroupChat && config.requireMention && !botMention) {
return null;
}

let text = '';
if (msgType === 'text') {
try {
Expand All @@ -34,13 +53,16 @@ export function parseInboundMessage(
text = message.content || '';
}
} else {
// Non-text messages — skip silently
return null;
}

if (!text.trim()) return null;

// Build thread-session address if applicable
// Strip the @bot placeholder so the LLM sees clean input.
if (botMention?.key) {
text = text.replaceAll(botMention.key, '').trim();
}

const rootId = message.root_id || '';
const effectiveChatId = rootId ? `${chatId}:thread:${rootId}` : chatId;

Expand Down
31 changes: 29 additions & 2 deletions src/lib/channels/feishu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { FeishuConfig } from './types';
import { loadFeishuConfig, validateFeishuConfig } from './config';
import { FeishuGateway } from './gateway';
import { parseInboundMessage } from './inbound';
import { getBotInfo } from './identity';
import { sendMessage, addReaction, removeReaction } from './outbound';
import { isUserAuthorized } from './policy';
import { createCardStreamController } from './card-controller';
Expand All @@ -23,6 +24,7 @@ export class FeishuChannelPlugin implements ChannelPlugin<FeishuConfig> {

private config: FeishuConfig | null = null;
private gateway: FeishuGateway | null = null;
private botOpenId: string = '';
private messageQueue: InboundMessage[] = [];
private waitResolve: ((msg: InboundMessage | null) => void) | null = null;
/** Track last received messageId per chatId for reaction acknowledgment. */
Expand Down Expand Up @@ -64,9 +66,11 @@ export class FeishuChannelPlugin implements ChannelPlugin<FeishuConfig> {

this.gateway = new FeishuGateway(this.config);

// Register message handler — pushes to internal queue
// Register message handler — pushes to internal queue.
// Reads this.botOpenId at call time, so mention checks activate
// once resolveBotIdentity() completes after gateway.start().
this.gateway.registerMessageHandler((data: unknown) => {
const msg = parseInboundMessage(data, this.config!);
const msg = parseInboundMessage(data, this.config!, this.botOpenId);
if (!msg) return;
this.enqueueMessage(msg);
});
Expand Down Expand Up @@ -148,6 +152,29 @@ export class FeishuChannelPlugin implements ChannelPlugin<FeishuConfig> {
});

await this.gateway.start();

// Resolve bot identity so mention checks work.
// Handler reads this.botOpenId dynamically — once resolved, all
// subsequent messages get proper mention filtering.
await this.resolveBotIdentity();
}

/** Fetch bot open_id with retry so mention features degrade gracefully. */
private async resolveBotIdentity(maxRetries = 3): Promise<void> {
const client = this.gateway?.getRestClient();
if (!client) return;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const info = await getBotInfo(client);
if (info?.openId) {
this.botOpenId = info.openId;
return;
}
console.warn('[feishu/plugin]', `Bot identity attempt ${attempt}/${maxRetries} failed`);
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 2000 * attempt));
}
}
console.error('[feishu/plugin]', 'Could not resolve bot identity — mention features disabled');
}

async stop(): Promise<void> {
Expand Down