fix(slack): resolve stream recipient team in shared channels

This commit is contained in:
@zimeg
2026-04-18 04:11:06 -07:00
parent 992b2143dd
commit 25ce5a5822
3 changed files with 105 additions and 2 deletions

View File

@@ -160,6 +160,7 @@ Docs: https://docs.openclaw.ai
- Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment `dailyCount` across days instead of stalling at `1`. (#67091) Thanks @Bartok9.
- Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like `/usr/bin/whoami` no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.
- Codex/gateway: fix gateway crash when the codex-acp subprocess terminates abruptly; an unhandled EPIPE on the child stdin stream now routes through graceful client shutdown, rejecting pending requests instead of propagating as an uncaught exception that crashes the entire gateway daemon and all connected channels. Fixes #67886. (#67947) thanks @openperf
- Slack/streaming: resolve native streaming recipient teams from the inbound user when available, with a monitor-team fallback, so DM and shared-workspace streams target the right recipient more reliably.
## 2026.4.14

View File

@@ -1,7 +1,8 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
createSlackTurnDeliveryTracker,
isSlackStreamingEnabled,
resolveSlackStreamRecipientTeamId,
resolveSlackStreamingThreadHint,
shouldEnableSlackPreviewStreaming,
shouldInitializeSlackDraftStream,
@@ -20,6 +21,79 @@ describe("slack native streaming defaults", () => {
});
});
describe("slack native streaming recipient team", () => {
it("resolves the recipient team through users.info", async () => {
const usersInfo = vi.fn(async () => ({
user: { team_id: "T_LOOKUP" },
}));
expect(
await resolveSlackStreamRecipientTeamId({
client: {
users: {
info: usersInfo,
},
} as never,
token: "xoxb-test",
userId: "U_REMOTE",
fallbackTeamId: "T_LOCAL",
}),
).toBe("T_LOOKUP");
expect(usersInfo).toHaveBeenCalledWith({
token: "xoxb-test",
user: "U_REMOTE",
});
});
it("falls back to profile.team when users.info omits user.team_id", async () => {
expect(
await resolveSlackStreamRecipientTeamId({
client: {
users: {
info: vi.fn(async () => ({
user: { profile: { team: "T_PROFILE" } },
})),
},
} as never,
token: "xoxb-test",
userId: "U_REMOTE",
fallbackTeamId: "T_LOCAL",
}),
).toBe("T_PROFILE");
});
it("falls back to the monitor team when users.info cannot resolve a team", async () => {
expect(
await resolveSlackStreamRecipientTeamId({
client: {
users: {
info: vi.fn(async () => {
throw new Error("user_not_found");
}),
},
} as never,
token: "xoxb-test",
userId: "U_REMOTE",
fallbackTeamId: "T_LOCAL",
}),
).toBe("T_LOCAL");
});
it("falls back to the monitor team when no user id is available", async () => {
expect(
await resolveSlackStreamRecipientTeamId({
client: {
users: {
info: vi.fn(),
},
} as never,
token: "xoxb-test",
fallbackTeamId: "T_LOCAL",
}),
).toBe("T_LOCAL");
});
});
describe("slack turn delivery tracker", () => {
it("treats repeated text payloads on the same thread as duplicates", () => {
const tracker = createSlackTurnDeliveryTracker();

View File

@@ -182,6 +182,29 @@ function shouldUseStreaming(params: {
return true;
}
export async function resolveSlackStreamRecipientTeamId(params: {
client: Pick<PreparedSlackMessage["ctx"]["app"]["client"], "users">;
token: string;
userId?: PreparedSlackMessage["message"]["user"];
fallbackTeamId?: string;
}): Promise<string | undefined> {
if (params.userId) {
try {
const info = await params.client.users.info({
token: params.token,
user: params.userId,
});
const teamId = info.user?.team_id ?? info.user?.profile?.team;
if (teamId) {
return teamId;
}
} catch (err) {
logVerbose(`slack-stream: users.info team lookup failed (${String(err)})`);
}
}
return params.fallbackTeamId;
}
export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessage) {
const { ctx, account, message, route } = prepared;
const cfg = ctx.cfg;
@@ -490,7 +513,12 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
channel: message.channel,
threadTs: streamThreadTs,
text,
teamId: ctx.teamId,
teamId: await resolveSlackStreamRecipientTeamId({
client: ctx.app.client,
token: ctx.botToken,
userId: message.user,
fallbackTeamId: ctx.teamId,
}),
userId: message.user,
});
observedReplyDelivery = true;