惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
J
Java Code Geeks
Blog — PlanetScale
Blog — PlanetScale
F
Fortinet All Blogs
腾讯CDC
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
WordPress大学
WordPress大学
雷峰网
雷峰网
小众软件
小众软件
D
DataBreaches.Net
V
Visual Studio Blog
博客园 - Franky
IT之家
IT之家
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog RSS Feed
博客园 - 聂微东
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Security Blog
Microsoft Security Blog
G
Google Developers Blog
云风的 BLOG
云风的 BLOG

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
refactor(discord): centralize thread channel context · op...
steipete · 2026-04-23 · via Recent Commits to openclaw:main

@@ -32,14 +32,10 @@ import {

3232

resolveDiscordGuildEntry,

3333

shouldEmitDiscordReactionNotification,

3434

} from "./allow-list.js";

35-

import {

36-

resolveDiscordChannelInfoSafe,

37-

resolveDiscordChannelParentIdSafe,

38-

} from "./channel-access.js";

3935

import { formatDiscordReactionEmoji, formatDiscordUserTag } from "./format.js";

40-

import { resolveDiscordChannelInfo } from "./message-utils.js";

4136

import { setPresence } from "./presence-cache.js";

4237

import { isThreadArchived } from "./thread-bindings.discord-api.js";

38+

import { resolveFetchedDiscordThreadLikeChannelContext } from "./thread-channel-context.js";

4339

import { closeDiscordThreadSessions } from "./thread-session-close.js";

4440

import { normalizeDiscordListenerTimeoutMs, runDiscordTaskWithTimeout } from "./timeouts.js";

4541

@@ -77,6 +73,11 @@ type DiscordReactionRoutingParams = {

7773

guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;

7874

};

797576+

type DiscordReactionMode = "off" | "own" | "all" | "allowlist";

77+

type DiscordReactionChannelConfig = ReturnType<typeof resolveDiscordChannelConfigWithFallback>;

78+

type DiscordReactionIngressAccess = Awaited<ReturnType<typeof authorizeDiscordReactionIngress>>;

79+

type DiscordFetchedReactionMessage = { author?: User } | null;

80+8081

const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 30_000;

8182

const discordEventQueueLog = createSubsystemLogger("discord/event-queue");

8283

@@ -409,6 +410,117 @@ async function authorizeDiscordReactionIngress(

409410

return { allowed: true };

410411

}

411412413+

async function handleDiscordThreadReactionNotification(params: {

414+

reactionMode: DiscordReactionMode;

415+

message: DiscordReactionEvent["message"];

416+

parentId?: string;

417+

resolveThreadChannelAccess: () => Promise<{

418+

access: DiscordReactionIngressAccess;

419+

channelConfig: DiscordReactionChannelConfig;

420+

}>;

421+

shouldNotifyReaction: (options: {

422+

mode: DiscordReactionMode;

423+

messageAuthorId?: string;

424+

channelConfig?: DiscordReactionChannelConfig;

425+

}) => boolean;

426+

resolveReactionBase: () => { baseText: string; contextKey: string };

427+

emitReaction: (text: string, parentPeerId?: string) => void;

428+

emitReactionWithAuthor: (message: DiscordFetchedReactionMessage) => void;

429+

}) {

430+

if (params.reactionMode === "off") {

431+

return;

432+

}

433+434+

if (params.reactionMode === "all" || params.reactionMode === "allowlist") {

435+

const { access, channelConfig } = await params.resolveThreadChannelAccess();

436+

if (

437+

!access.allowed ||

438+

!params.shouldNotifyReaction({ mode: params.reactionMode, channelConfig })

439+

) {

440+

return;

441+

}

442+443+

const { baseText } = params.resolveReactionBase();

444+

params.emitReaction(baseText, params.parentId);

445+

return;

446+

}

447+448+

const message = await params.message.fetch().catch(() => null);

449+

const { access, channelConfig } = await params.resolveThreadChannelAccess();

450+

const messageAuthorId = message?.author?.id ?? undefined;

451+

if (

452+

!access.allowed ||

453+

!params.shouldNotifyReaction({

454+

mode: params.reactionMode,

455+

messageAuthorId,

456+

channelConfig,

457+

})

458+

) {

459+

return;

460+

}

461+462+

params.emitReactionWithAuthor(message);

463+

}

464+465+

async function handleDiscordChannelReactionNotification(params: {

466+

isGuildMessage: boolean;

467+

reactionMode: DiscordReactionMode;

468+

message: DiscordReactionEvent["message"];

469+

channelConfig: DiscordReactionChannelConfig;

470+

parentId?: string;

471+

authorizeReactionIngressForChannel: (

472+

channelConfig: DiscordReactionChannelConfig,

473+

) => Promise<DiscordReactionIngressAccess>;

474+

shouldNotifyReaction: (options: {

475+

mode: DiscordReactionMode;

476+

messageAuthorId?: string;

477+

channelConfig?: DiscordReactionChannelConfig;

478+

}) => boolean;

479+

resolveReactionBase: () => { baseText: string; contextKey: string };

480+

emitReaction: (text: string, parentPeerId?: string) => void;

481+

emitReactionWithAuthor: (message: DiscordFetchedReactionMessage) => void;

482+

}) {

483+

if (params.isGuildMessage) {

484+

const access = await params.authorizeReactionIngressForChannel(params.channelConfig);

485+

if (!access.allowed) {

486+

return;

487+

}

488+

}

489+490+

if (params.reactionMode === "off") {

491+

return;

492+

}

493+494+

if (params.reactionMode === "all" || params.reactionMode === "allowlist") {

495+

if (

496+

!params.shouldNotifyReaction({

497+

mode: params.reactionMode,

498+

channelConfig: params.channelConfig,

499+

})

500+

) {

501+

return;

502+

}

503+504+

const { baseText } = params.resolveReactionBase();

505+

params.emitReaction(baseText, params.parentId);

506+

return;

507+

}

508+509+

const message = await params.message.fetch().catch(() => null);

510+

const messageAuthorId = message?.author?.id ?? undefined;

511+

if (

512+

!params.shouldNotifyReaction({

513+

mode: params.reactionMode,

514+

messageAuthorId,

515+

channelConfig: params.channelConfig,

516+

})

517+

) {

518+

return;

519+

}

520+521+

params.emitReactionWithAuthor(message);

522+

}

523+412524

async function handleDiscordReactionEvent(

413525

params: {

414526

data: DiscordReactionEvent;

@@ -449,16 +561,17 @@ async function handleDiscordReactionEvent(

449561

if (!channel) {

450562

return;

451563

}

452-

const channelInfo = resolveDiscordChannelInfoSafe(channel);

453-

const channelName = channelInfo.name;

454-

const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";

455-

const channelType = channelInfo.type;

564+

const channelContext = await resolveFetchedDiscordThreadLikeChannelContext({

565+

client,

566+

channel,

567+

channelIdFallback: data.channel_id,

568+

});

569+

const channelName = channelContext.channelName;

570+

const channelSlug = channelContext.channelSlug;

571+

const channelType = channelContext.channelType;

456572

const isDirectMessage = channelType === ChannelType.DM;

457573

const isGroupDm = channelType === ChannelType.GroupDM;

458-

const isThreadChannel =

459-

channelType === ChannelType.PublicThread ||

460-

channelType === ChannelType.PrivateThread ||

461-

channelType === ChannelType.AnnouncementThread;

574+

const isThreadChannel = channelContext.isThreadChannel;

462575

const memberRoleIds = Array.isArray(data.rawMember?.roles)

463576

? data.rawMember.roles.map((roleId: string) => roleId)

464577

: [];

@@ -490,9 +603,9 @@ async function handleDiscordReactionEvent(

490603

return;

491604

}

492605

}

493-

let parentId = resolveDiscordChannelParentIdSafe(channel);

494-

let parentName: string | undefined;

495-

let parentSlug = "";

606+

const parentId = isThreadChannel ? channelContext.threadParentId : channelContext.parentId;

607+

const parentName = isThreadChannel ? channelContext.threadParentName : undefined;

608+

const parentSlug = isThreadChannel ? channelContext.threadParentSlug : "";

496609

let reactionBase: { baseText: string; contextKey: string } | null = null;

497610

const resolveReactionBase = () => {

498611

if (reactionBase) {

@@ -535,9 +648,9 @@ async function handleDiscordReactionEvent(

535648

});

536649

};

537650

const shouldNotifyReaction = (options: {

538-

mode: "off" | "own" | "all" | "allowlist";

651+

mode: DiscordReactionMode;

539652

messageAuthorId?: string;

540-

channelConfig?: ReturnType<typeof resolveDiscordChannelConfigWithFallback>;

653+

channelConfig?: DiscordReactionChannelConfig;

541654

}) =>

542655

shouldEmitDiscordReactionNotification({

543656

mode: options.mode,

@@ -557,14 +670,6 @@ async function handleDiscordReactionEvent(

557670

const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;

558671

emitReaction(text, parentId);

559672

};

560-

const loadThreadParentInfo = async () => {

561-

if (!parentId) {

562-

return;

563-

}

564-

const parentInfo = await resolveDiscordChannelInfo(client, parentId);

565-

parentName = parentInfo?.name;

566-

parentSlug = parentName ? normalizeDiscordSlug(parentName) : "";

567-

};

568673

const resolveThreadChannelConfig = () =>

569674

resolveDiscordChannelConfigWithFallback({

570675

guildInfo,

@@ -577,77 +682,30 @@ async function handleDiscordReactionEvent(

577682

scope: "thread",

578683

});

579684

const authorizeReactionIngressForChannel = async (

580-

channelConfig: ReturnType<typeof resolveDiscordChannelConfigWithFallback>,

685+

channelConfig: DiscordReactionChannelConfig,

581686

) =>

582687

await authorizeDiscordReactionIngress({

583688

...reactionIngressBase,

584689

channelConfig,

585690

});

586-

const resolveThreadChannelAccess = async (channelInfo: { parentId?: string } | null) => {

587-

parentId = channelInfo?.parentId;

588-

await loadThreadParentInfo();

691+

const resolveThreadChannelAccess = async () => {

589692

const channelConfig = resolveThreadChannelConfig();

590693

const access = await authorizeReactionIngressForChannel(channelConfig);

591694

return { access, channelConfig };

592695

};

593696594-

// Parallelize async operations for thread channels

595697

if (isThreadChannel) {

596698

const reactionMode = guildInfo?.reactionNotifications ?? "own";

597-598-

// Early exit: skip fetching message if notifications are off

599-

if (reactionMode === "off") {

600-

return;

601-

}

602-603-

const channelInfoPromise = parentId

604-

? Promise.resolve({ parentId })

605-

: resolveDiscordChannelInfo(client, data.channel_id);

606-607-

// Fast path: for "all" and "allowlist" modes, we don't need to fetch the message

608-

if (reactionMode === "all" || reactionMode === "allowlist") {

609-

const channelInfo = await channelInfoPromise;

610-

const { access: threadAccess, channelConfig: threadChannelConfig } =

611-

await resolveThreadChannelAccess(channelInfo);

612-

if (!threadAccess.allowed) {

613-

return;

614-

}

615-

if (

616-

!shouldNotifyReaction({

617-

mode: reactionMode,

618-

channelConfig: threadChannelConfig,

619-

})

620-

) {

621-

return;

622-

}

623-624-

const { baseText } = resolveReactionBase();

625-

emitReaction(baseText, parentId);

626-

return;

627-

}

628-629-

// For "own" mode, we need to fetch the message to check the author

630-

const messagePromise = data.message.fetch().catch(() => null);

631-632-

const [channelInfo, message] = await Promise.all([channelInfoPromise, messagePromise]);

633-

const { access: threadAccess, channelConfig: threadChannelConfig } =

634-

await resolveThreadChannelAccess(channelInfo);

635-

if (!threadAccess.allowed) {

636-

return;

637-

}

638-639-

const messageAuthorId = message?.author?.id ?? undefined;

640-

if (

641-

!shouldNotifyReaction({

642-

mode: reactionMode,

643-

messageAuthorId,

644-

channelConfig: threadChannelConfig,

645-

})

646-

) {

647-

return;

648-

}

649-650-

emitReactionWithAuthor(message);

699+

await handleDiscordThreadReactionNotification({

700+

reactionMode,

701+

message: data.message,

702+

parentId,

703+

resolveThreadChannelAccess,

704+

shouldNotifyReaction,

705+

resolveReactionBase,

706+

emitReaction,

707+

emitReactionWithAuthor,

708+

});

651709

return;

652710

}

653711

@@ -662,39 +720,19 @@ async function handleDiscordReactionEvent(

662720

parentSlug,

663721

scope: "channel",

664722

});

665-

if (isGuildMessage) {

666-

const channelAccess = await authorizeReactionIngressForChannel(channelConfig);

667-

if (!channelAccess.allowed) {

668-

return;

669-

}

670-

}

671-672723

const reactionMode = guildInfo?.reactionNotifications ?? "own";

673-674-

// Early exit: skip fetching message if notifications are off

675-

if (reactionMode === "off") {

676-

return;

677-

}

678-679-

// Fast path: for "all" and "allowlist" modes, we don't need to fetch the message

680-

if (reactionMode === "all" || reactionMode === "allowlist") {

681-

if (!shouldNotifyReaction({ mode: reactionMode, channelConfig })) {

682-

return;

683-

}

684-685-

const { baseText } = resolveReactionBase();

686-

emitReaction(baseText, parentId);

687-

return;

688-

}

689-690-

// For "own" mode, we need to fetch the message to check the author

691-

const message = await data.message.fetch().catch(() => null);

692-

const messageAuthorId = message?.author?.id ?? undefined;

693-

if (!shouldNotifyReaction({ mode: reactionMode, messageAuthorId, channelConfig })) {

694-

return;

695-

}

696-697-

emitReactionWithAuthor(message);

724+

await handleDiscordChannelReactionNotification({

725+

isGuildMessage,

726+

reactionMode,

727+

message: data.message,

728+

channelConfig,

729+

parentId,

730+

authorizeReactionIngressForChannel,

731+

shouldNotifyReaction,

732+

resolveReactionBase,

733+

emitReaction,

734+

emitReactionWithAuthor,

735+

});

698736

} catch (err) {

699737

params.logger.error(danger(`discord reaction handler failed: ${String(err)}`));

700738

}