Skip to content

Commit d1667ec

Browse files
authored
Merge pull request #130 from odefun/feat/discord-thread-mention-filter-x100b56e
refactor: unify IM incoming message workflow
2 parents caf3c36 + 636a2d2 commit d1667ec

19 files changed

+1556
-1077
lines changed

packages/ims/discord/client.ts

Lines changed: 121 additions & 764 deletions
Large diffs are not rendered by default.

packages/ims/discord/settings.ts

Lines changed: 716 additions & 0 deletions
Large diffs are not rendered by default.

packages/ims/lark/client.ts

Lines changed: 106 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,23 @@ import {
66
getGitHubInfoForUser,
77
getLarkAppCredentials,
88
getLarkTargetChannels,
9-
getWebHost,
10-
getWebPort,
119
getWorkspaces,
1210
} from "@/config";
1311
import { isThreadActive, markThreadActive } from "@/config/local/settings";
1412
import { createCoreRuntime } from "@/core/runtime";
1513
import type { IMAdapter } from "@/core/types";
1614
import { log } from "@/utils";
1715
import { isStopCommand } from "@/ims/shared/stop-command";
16+
import {
17+
toCoreMessageContext,
18+
type UnifiedMessageContext,
19+
} from "@/ims/shared/message-context";
20+
import { evaluateIncomingMessage } from "@/ims/shared/incoming-pipeline";
21+
import { executeIncomingFlow } from "@/ims/shared/incoming-executor";
22+
import { buildIncomingContext } from "@/ims/shared/incoming-normalizer";
23+
import { parseIncomingCommand } from "@/ims/shared/command-router";
24+
import { createRuntimeController } from "@/ims/shared/runtime-controller";
25+
import { sendLarkSettingsCard } from "./settings";
1826

1927
let larkRuntimeStarted = false;
2028

@@ -214,15 +222,6 @@ function parseLarkText(content: string | undefined): string {
214222
}
215223
}
216224

217-
function isSettingsCommand(text: string): boolean {
218-
const normalized = text.trim().replace(/^/, "/");
219-
return /^\/?settings?(?:\s|$)/i.test(normalized);
220-
}
221-
222-
function getLocalSettingsUrl(): string {
223-
return `http://${getWebHost()}:${getWebPort()}/`;
224-
}
225-
226225
async function buildLarkContext(
227226
channelId: string,
228227
threadId: string,
@@ -259,71 +258,19 @@ async function sendMessage(
259258
}
260259

261260
async function sendSettingsCard(channelId: string, threadId: string): Promise<string | undefined> {
262-
const settingsUrl = getLocalSettingsUrl();
263-
logLarkEvent("Lark settings UI launcher triggered", {
261+
return sendLarkSettingsCard({
264262
channelId,
265263
threadId,
266-
settingsUrl,
264+
sendInteractive: (card) =>
265+
sendLarkMessage({
266+
channelId,
267+
threadId,
268+
msgType: "interactive",
269+
content: card,
270+
}),
271+
sendText: (text) => sendMessage(channelId, threadId, text, true),
272+
logEvent: logLarkEvent,
267273
});
268-
const card = {
269-
config: {
270-
wide_screen_mode: true,
271-
},
272-
header: {
273-
template: "blue",
274-
title: {
275-
tag: "plain_text",
276-
content: "Ode Settings",
277-
},
278-
},
279-
elements: [
280-
{
281-
tag: "markdown",
282-
content: `Configure this chat in the local settings UI.\\n\\nChannel: \`${channelId}\``,
283-
},
284-
{
285-
tag: "action",
286-
actions: [
287-
{
288-
tag: "button",
289-
text: {
290-
tag: "plain_text",
291-
content: "Open Local Setting",
292-
},
293-
type: "primary",
294-
url: settingsUrl,
295-
},
296-
],
297-
},
298-
],
299-
};
300-
301-
try {
302-
const messageId = await sendLarkMessage({
303-
channelId,
304-
threadId,
305-
msgType: "interactive",
306-
content: card as unknown as Record<string, unknown>,
307-
});
308-
logLarkEvent("Lark settings card sent", {
309-
channelId,
310-
threadId,
311-
messageId: messageId ?? "",
312-
});
313-
return messageId;
314-
} catch {
315-
logLarkEvent("Lark settings card failed, sending fallback text", {
316-
channelId,
317-
threadId,
318-
});
319-
const fallbackText = [
320-
"Ode settings",
321-
`Open: ${settingsUrl}`,
322-
`Channel: ${channelId}`,
323-
"Use this channel in Local Setting to configure provider/model/directory.",
324-
].join("\n");
325-
return sendMessage(channelId, threadId, fallbackText, true);
326-
}
327274
}
328275

329276
async function updateMessage(
@@ -662,6 +609,18 @@ async function processLarkIncomingEvent(event: LarkIncomingEvent): Promise<void>
662609
: false;
663610
const active = isThreadActive(channelId, threadId);
664611
const text = stripLarkMentionMarkup(rawText);
612+
const messageContext: UnifiedMessageContext = buildIncomingContext({
613+
platform: "lark",
614+
channelId,
615+
threadId,
616+
messageId,
617+
userId: senderOpenId,
618+
isTopLevel: topLevelMessage,
619+
mentionedBot: isMentioned,
620+
activeThread: active,
621+
rawText,
622+
normalizedText: text,
623+
});
665624

666625
logLarkEvent("Lark inbound parsed", {
667626
channelId,
@@ -675,7 +634,8 @@ async function processLarkIncomingEvent(event: LarkIncomingEvent): Promise<void>
675634
textLength: text.length,
676635
});
677636

678-
if (isSettingsCommand(text)) {
637+
const command = parseIncomingCommand(text);
638+
if (command === "setting") {
679639
logLarkEvent("Lark inbound matched /setting", {
680640
channelId,
681641
threadId,
@@ -687,66 +647,50 @@ async function processLarkIncomingEvent(event: LarkIncomingEvent): Promise<void>
687647
return;
688648
}
689649

690-
if (!topLevelMessage) {
691-
if (!isMentioned && !active) {
692-
logLarkEvent("Lark inbound ignored: thread reply without mention and inactive thread", {
650+
const flowResult = evaluateIncomingMessage(messageContext, isStopCommand);
651+
await executeIncomingFlow({
652+
context: messageContext,
653+
flowResult,
654+
markThreadActive,
655+
handleStopCommand: (flowChannelId, flowThreadId) => coreRuntime.handleStopCommand(flowChannelId, flowThreadId),
656+
sendStopAck: async () => {
657+
await sendMessage(channelId, threadId, "Request stopped.", true);
658+
},
659+
onIgnore: (reason) => {
660+
if (reason === "not_mentioned_and_inactive") {
661+
logLarkEvent("Lark inbound ignored: not mentioned and thread inactive", {
662+
channelId,
663+
threadId,
664+
messageId,
665+
reason,
666+
isTopLevel: topLevelMessage,
667+
isMentioned,
668+
activeThread: active,
669+
});
670+
return;
671+
}
672+
logLarkEvent("Lark inbound ignored: empty text after mention stripping", {
673+
channelId,
674+
messageId,
675+
});
676+
},
677+
forwardToCore: async (forwardText) => {
678+
logLarkEvent("Lark inbound accepted: forwarding to core runtime", {
679+
channelId,
680+
threadId,
681+
messageId,
682+
userId: senderOpenId,
683+
});
684+
await coreRuntime.handleIncomingMessage(
685+
toCoreMessageContext(messageContext),
686+
forwardText
687+
);
688+
logLarkEvent("Lark inbound handled by core runtime", {
693689
channelId,
694690
threadId,
695691
messageId,
696692
});
697-
return;
698-
}
699-
} else if (!isMentioned) {
700-
logLarkEvent("Lark inbound ignored: top-level message without mention", {
701-
channelId,
702-
threadId,
703-
messageId,
704-
});
705-
return;
706-
}
707-
708-
if (!text) {
709-
logLarkEvent("Lark inbound ignored: empty text after mention stripping", {
710-
channelId,
711-
messageId,
712-
});
713-
return;
714-
}
715-
716-
if (isStopCommand(text)) {
717-
logLarkEvent("Lark inbound matched stop command", {
718-
channelId,
719-
threadId,
720-
messageId,
721-
});
722-
const stopped = await coreRuntime.handleStopCommand(channelId, threadId);
723-
if (stopped) {
724-
await sendMessage(channelId, threadId, "Request stopped.", true);
725-
}
726-
return;
727-
}
728-
729-
markThreadActive(channelId, threadId);
730-
logLarkEvent("Lark inbound accepted: forwarding to core runtime", {
731-
channelId,
732-
threadId,
733-
messageId,
734-
userId: senderOpenId,
735-
});
736-
await coreRuntime.handleIncomingMessage(
737-
{
738-
channelId,
739-
replyThreadId: threadId,
740-
threadId,
741-
userId: senderOpenId,
742-
messageId,
743693
},
744-
text
745-
);
746-
logLarkEvent("Lark inbound handled by core runtime", {
747-
channelId,
748-
threadId,
749-
messageId,
750694
});
751695
}
752696

@@ -849,32 +793,43 @@ export async function handleLarkEventPayload(payload: unknown): Promise<{ status
849793
}
850794

851795
export async function startLarkRuntime(reason: string): Promise<boolean> {
852-
if (larkRuntimeStarted) return true;
853-
const workspaces = getLarkAppCredentials();
854-
if (workspaces.length === 0) {
855-
log.debug("Lark runtime skipped (Lark app credentials missing)", { reason });
856-
return false;
796+
if (larkRuntimeStarted) {
797+
log.debug("Lark runtime start skipped; already running", { reason });
857798
}
858-
larkRuntimeStarted = true;
859-
tenantTokenCache.clear();
860-
botOpenIdCache.clear();
861-
sentMessageThreadMap.clear();
862-
log.debug("Lark runtime started", {
863-
reason,
864-
workspaceCount: workspaces.length,
865-
});
866-
await startLarkLongConnections(reason);
867-
return true;
799+
return larkRuntimeController.start(reason);
868800
}
869801

870802
export async function stopLarkRuntime(reason: string): Promise<void> {
871-
if (!larkRuntimeStarted) return;
872-
larkRuntimeStarted = false;
873-
await stopLarkLongConnections(reason);
874-
tenantTokenCache.clear();
875-
botOpenIdCache.clear();
876-
sentMessageThreadMap.clear();
877-
log.debug("Lark runtime stopped", { reason });
803+
await larkRuntimeController.stop(reason);
878804
}
879805

806+
const larkRuntimeController = createRuntimeController({
807+
isRunning: () => larkRuntimeStarted,
808+
startInternal: async (reason: string): Promise<boolean> => {
809+
const workspaces = getLarkAppCredentials();
810+
if (workspaces.length === 0) {
811+
log.debug("Lark runtime skipped (Lark app credentials missing)", { reason });
812+
return false;
813+
}
814+
larkRuntimeStarted = true;
815+
tenantTokenCache.clear();
816+
botOpenIdCache.clear();
817+
sentMessageThreadMap.clear();
818+
log.debug("Lark runtime started", {
819+
reason,
820+
workspaceCount: workspaces.length,
821+
});
822+
await startLarkLongConnections(reason);
823+
return true;
824+
},
825+
stopInternal: async (reason: string): Promise<void> => {
826+
larkRuntimeStarted = false;
827+
await stopLarkLongConnections(reason);
828+
tenantTokenCache.clear();
829+
botOpenIdCache.clear();
830+
sentMessageThreadMap.clear();
831+
log.debug("Lark runtime stopped", { reason });
832+
},
833+
});
834+
880835
export const recoverPendingRequests = coreRuntime.recoverPendingRequests;

0 commit comments

Comments
 (0)