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

推荐订阅源

美团技术团队
P
Privacy International News Feed
P
Proofpoint News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
C
CXSECURITY Database RSS Feed - CXSecurity.com
Know Your Adversary
Know Your Adversary
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
NISL@THU
NISL@THU
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
GbyAI
GbyAI
N
News and Events Feed by Topic
N
News | PayPal Newsroom
Y
Y Combinator Blog
C
CERT Recently Published Vulnerability Notes
N
Netflix TechBlog - Medium
S
Security Affairs
Spread Privacy
Spread Privacy
罗磊的独立博客
腾讯CDC
MyScale Blog
MyScale Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
L
LINUX DO - 热门话题
The Cloudflare Blog
L
LangChain Blog
博客园_首页
H
Hacker News: Front Page
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
SecWiki News
SecWiki News
A
Arctic Wolf
爱范儿
爱范儿
Google Online Security Blog
Google Online Security Blog
T
Threat Research - Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
有赞技术团队
有赞技术团队
The GitHub Blog
The GitHub Blog
Cyberwarzone
Cyberwarzone
博客园 - 叶小钗
V
Visual Studio Blog
V
V2EX
T
Tailwind CSS Blog
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
MongoDB | Blog
MongoDB | Blog
D
Docker

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 test: trim doctor command hotspots · openclaw/openclaw@c66f16a test: isolate agent auth and spawn hotspots · openclaw/openclaw@9285935 test: stabilize MCP startup disposal race · openclaw/openclaw@dd9d2eb test: merge browser contract server suites · openclaw/openclaw@5817a76 test: narrow ollama provider discovery setup · openclaw/openclaw@a0d9598 build: declare qa-lab aimock runtime dependency · openclaw/openclaw@24431e5 test: speed up safe-bins exec harness · openclaw/openclaw@ee856ab test: preserve tool helpers in embedded runner mocks · openclaw/openclaw@acd86a0 refactor: move memory embeddings into provider plugins · openclaw/openclaw@77e6e4c test: reuse system-run temp fixtures · openclaw/openclaw@7e9ff0f test: trim hotspot wait overhead · openclaw/openclaw@12a59b0 Check: avoid duplicate boundary prep · openclaw/openclaw@baf11b8 test: reduce hotspot fixture overhead · openclaw/openclaw@3a59edd feat(ui): overhaul settings and slash command UX (#67819) thanks @Bun… · openclaw/openclaw@2cfb660 QA Matrix: exit cleanly on failure · openclaw/openclaw@42805d2 QA Matrix: isolate scenario coverage · openclaw/openclaw@7e659e1 Matrix: refresh crypto bootstrap state · openclaw/openclaw@94081d8 QA Lab: add provider registry · openclaw/openclaw@bb7e982 Matrix: add plugin changelog · openclaw/openclaw@4acab55 test: trim more hotspot overhead · openclaw/openclaw@f485311 test: trim remaining hotspot tests · openclaw/openclaw@6ba8626 test: narrow hotspot mocks · openclaw/openclaw@dbc8179 test: isolate gemini embedding request helpers · openclaw/openclaw@cd330f5 test: trim memory and mcp hotspots · openclaw/openclaw@fd48dfa test: slim provider registry mocks · openclaw/openclaw@2e08c77 test: harden Parallels update smoke · openclaw/openclaw@1a98090 feat: default Anthropic to Opus 4.7 · openclaw/openclaw@628b454 fix: harden node-host shell payload mutability checks · openclaw/openclaw@75c551e fix: land node-host approval binding for native binaries (#66731) (th… · openclaw/openclaw@29919bb CI: add daily schedule to CodeQL workflow (#67645) · openclaw/openclaw@69d25f5 fix(gateway): capture config hash after plugin auto-enable to prevent… · openclaw/openclaw@8c11210 fix: repair sanitized replay tool results before send (#67620) (thank… · openclaw/openclaw@c3c7a99 fix: restrict HTML timeout short-circuit to transient statuses · openclaw/openclaw@de129a6 fix: keep TUI watchdog bound to active run (#67401) (thanks @xantorres) · openclaw/openclaw@3525273 Gateway/skills: dedupe skills prefix-match + drop dead fallback on log · openclaw/openclaw@d7f489f Extensions/lmstudio: back off inference preload after consecutive fai… · openclaw/openclaw@b555214 TUI/streaming: add watchdog that resets the activity indicator after … · openclaw/openclaw@f44ab20 Agents/tool-loop: enable unknown-tool stream guard by default · openclaw/openclaw@36ed367 Gateway/skills: invalidate session skills snapshot on config write · openclaw/openclaw@b23d59a fix: classify HTML provider error pages correctly (#67642) (thanks @s… · openclaw/openclaw@e588e90 fix(skills): remove unused model-usage import (#67641) · openclaw/openclaw@55f05df docs(changelog): credit codex fix superseded PRs · openclaw/openclaw@e485f24 fix(openai-codex): normalize stale transport metadata in resolution a… · openclaw/openclaw@90801ba CI: pin Docker-related GitHub Actions (#67632) · openclaw/openclaw@f697b01 Android: modernize WebView and discovery API usage (#67627) · openclaw/openclaw@44a6e50 fix(deps): bump hono to 4.12.14 and @hono/node-server to 1.19.14 (GHS… · openclaw/openclaw@fbccc18 fix(deps): bump dompurify to 3.4.0 (#67614) · openclaw/openclaw@2c2dc00 CI: add explicit permissions to all workflow jobs (fixes code-scannin… · openclaw/openclaw@01b7516 fix: register bundled TTS providers and route overrides correctly (#6… · openclaw/openclaw@6ea3cdd fix: align host tilde paths with OS home (#62804) (thanks @stainlu) · openclaw/openclaw@ecfaf64 fix: flush creds queue before reconnect socket open (#67464) (thanks … · openclaw/openclaw@405c63f fix: strip standalone <function> tool call tags from visible text (#6… · openclaw/openclaw@78df859 fix(agents): preserve cli session metadata before transcript persist … · openclaw/openclaw@898fd04 docs(changelog): move cli transcript entry · openclaw/openclaw@c1817c6 fix(agents): normalize cli transcript api field · openclaw/openclaw@3a3fae0 docs(changelog): note cli transcript persistence · openclaw/openclaw@6c343f1 fix(agents): persist cli transcript turns · openclaw/openclaw@b8ef507 fix(msteams): harden security-sensitive flows (#65841) · openclaw/openclaw@c56b56e [Dashboard] Fix exec approval modal overflow for long command content… · openclaw/openclaw@053c5b0 Docs: remove QA changelog entry · openclaw/openclaw@7fd5771 QA: fix private runtime source loading (#67428) · openclaw/openclaw@d5933af docs(gateway): correct protocol.md schema path, hello-ok example, aut… · openclaw/openclaw@489404d CI: pin Node 22 runners to 22.18.0 · openclaw/openclaw@4ffa621 models.authStatus: normalize provider ids + tighten env-backed escape… · openclaw/openclaw@f2fdb9d Update CHANGELOG.md · openclaw/openclaw@7694a92 test(parallels): clean up npm update guard jobs · openclaw/openclaw@045ea7b Plugins: prefer scanDir override paths · openclaw/openclaw@b2974da fix(dreaming): default storage.mode to "separate" so phase blocks sto… · openclaw/openclaw@8c392f0 fix(memory-core): skip dreaming transcript ingestion via session stor… · openclaw/openclaw@a1b01f0 fix: dedupe replayed exec.finished node events (#67281) · openclaw/openclaw@5dcf526
refactor(discord): centralize thread channel context · openclaw/openclaw@8bd3879
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

}