mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 08:11:42 +00:00
fix(slack): thread agent identity through channel reply path (openclaw#27134) thanks @hou-rong
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: hou-rong <8758438+hou-rong@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -232,6 +232,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Slack/Identity: thread agent outbound identity (`chat:write.customize` overrides) through the channel reply delivery path so per-agent username, icon URL, and icon emoji are applied to all Slack replies including media messages. (#27134) Thanks @hou-rong.
|
||||||
- Agents/Subagents delivery: refactor subagent completion announce dispatch into an explicit queue/direct/fallback state machine, recover outbound channel-plugin resolution in cold/stale plugin-registry states across announce/message/gateway send paths, finalize cleanup bookkeeping when announce flow rejects, and treat Telegram sends without `message_id` as delivery failures (instead of false-success `"unknown"` IDs). (#26867, #25961, #26803, #25069, #26741) Thanks @SmithLabsLLC and @docaohieu2808.
|
- Agents/Subagents delivery: refactor subagent completion announce dispatch into an explicit queue/direct/fallback state machine, recover outbound channel-plugin resolution in cold/stale plugin-registry states across announce/message/gateway send paths, finalize cleanup bookkeeping when announce flow rejects, and treat Telegram sends without `message_id` as delivery failures (instead of false-success `"unknown"` IDs). (#26867, #25961, #26803, #25069, #26741) Thanks @SmithLabsLLC and @docaohieu2808.
|
||||||
- Telegram/Webhook: pre-initialize webhook bots, switch webhook processing to callback-mode JSON handling, and preserve full near-limit payload reads under delayed handlers to prevent webhook request hangs and dropped updates. (#26156)
|
- Telegram/Webhook: pre-initialize webhook bots, switch webhook processing to callback-mode JSON handling, and preserve full near-limit payload reads under delayed handlers to prevent webhook request hangs and dropped updates. (#26156)
|
||||||
- Slack/Session threads: prevent oversized parent-session inheritance from silently bricking new thread sessions, surface embedded context-overflow empty-result failures to users, and add configurable `session.parentForkMaxTokens` (default `100000`, `0` disables). (#26912) Thanks @markshields-tl.
|
- Slack/Session threads: prevent oversized parent-session inheritance from silently bricking new thread sessions, surface embedded context-overflow empty-result failures to users, and add configurable `session.parentForkMaxTokens` (default `100000`, `0` disables). (#26912) Thanks @markshields-tl.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { createReplyPrefixOptions } from "../../../channels/reply-prefix.js";
|
|||||||
import { createTypingCallbacks } from "../../../channels/typing.js";
|
import { createTypingCallbacks } from "../../../channels/typing.js";
|
||||||
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
|
||||||
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
|
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
|
||||||
|
import { resolveAgentOutboundIdentity } from "../../../infra/outbound/identity.js";
|
||||||
import { removeSlackReaction } from "../../actions.js";
|
import { removeSlackReaction } from "../../actions.js";
|
||||||
import { createSlackDraftStream } from "../../draft-stream.js";
|
import { createSlackDraftStream } from "../../draft-stream.js";
|
||||||
import {
|
import {
|
||||||
@@ -70,6 +71,16 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
|||||||
const cfg = ctx.cfg;
|
const cfg = ctx.cfg;
|
||||||
const runtime = ctx.runtime;
|
const runtime = ctx.runtime;
|
||||||
|
|
||||||
|
// Resolve agent identity for Slack chat:write.customize overrides.
|
||||||
|
const outboundIdentity = resolveAgentOutboundIdentity(cfg, route.agentId);
|
||||||
|
const slackIdentity = outboundIdentity
|
||||||
|
? {
|
||||||
|
username: outboundIdentity.name,
|
||||||
|
iconUrl: outboundIdentity.avatarUrl,
|
||||||
|
iconEmoji: outboundIdentity.emoji,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (prepared.isDirectMessage) {
|
if (prepared.isDirectMessage) {
|
||||||
const sessionCfg = cfg.session;
|
const sessionCfg = cfg.session;
|
||||||
const storePath = resolveStorePath(sessionCfg?.store, {
|
const storePath = resolveStorePath(sessionCfg?.store, {
|
||||||
@@ -190,6 +201,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
|||||||
textLimit: ctx.textLimit,
|
textLimit: ctx.textLimit,
|
||||||
replyThreadTs,
|
replyThreadTs,
|
||||||
replyToMode: ctx.replyToMode,
|
replyToMode: ctx.replyToMode,
|
||||||
|
...(slackIdentity ? { identity: slackIdentity } : {}),
|
||||||
});
|
});
|
||||||
replyPlan.markSent();
|
replyPlan.markSent();
|
||||||
};
|
};
|
||||||
|
|||||||
56
src/slack/monitor/replies.test.ts
Normal file
56
src/slack/monitor/replies.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const sendMock = vi.fn();
|
||||||
|
vi.mock("../send.js", () => ({
|
||||||
|
sendMessageSlack: (...args: unknown[]) => sendMock(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { deliverReplies } from "./replies.js";
|
||||||
|
|
||||||
|
function baseParams(overrides?: Record<string, unknown>) {
|
||||||
|
return {
|
||||||
|
replies: [{ text: "hello" }],
|
||||||
|
target: "C123",
|
||||||
|
token: "xoxb-test",
|
||||||
|
runtime: { log: () => {}, error: () => {}, exit: () => {} },
|
||||||
|
textLimit: 4000,
|
||||||
|
replyToMode: "off" as const,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("deliverReplies identity passthrough", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sendMock.mockReset();
|
||||||
|
});
|
||||||
|
it("passes identity to sendMessageSlack for text replies", async () => {
|
||||||
|
sendMock.mockResolvedValue(undefined);
|
||||||
|
const identity = { username: "Bot", iconEmoji: ":robot:" };
|
||||||
|
await deliverReplies(baseParams({ identity }));
|
||||||
|
|
||||||
|
expect(sendMock).toHaveBeenCalledOnce();
|
||||||
|
expect(sendMock.mock.calls[0][2]).toMatchObject({ identity });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes identity to sendMessageSlack for media replies", async () => {
|
||||||
|
sendMock.mockResolvedValue(undefined);
|
||||||
|
const identity = { username: "Bot", iconUrl: "https://example.com/icon.png" };
|
||||||
|
await deliverReplies(
|
||||||
|
baseParams({
|
||||||
|
identity,
|
||||||
|
replies: [{ text: "caption", mediaUrls: ["https://example.com/img.png"] }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(sendMock).toHaveBeenCalledOnce();
|
||||||
|
expect(sendMock.mock.calls[0][2]).toMatchObject({ identity });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits identity key when not provided", async () => {
|
||||||
|
sendMock.mockResolvedValue(undefined);
|
||||||
|
await deliverReplies(baseParams());
|
||||||
|
|
||||||
|
expect(sendMock).toHaveBeenCalledOnce();
|
||||||
|
expect(sendMock.mock.calls[0][2]).not.toHaveProperty("identity");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
|
|||||||
import type { MarkdownTableMode } from "../../config/types.base.js";
|
import type { MarkdownTableMode } from "../../config/types.base.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import { markdownToSlackMrkdwnChunks } from "../format.js";
|
import { markdownToSlackMrkdwnChunks } from "../format.js";
|
||||||
import { sendMessageSlack } from "../send.js";
|
import { sendMessageSlack, type SlackSendIdentity } from "../send.js";
|
||||||
|
|
||||||
export async function deliverReplies(params: {
|
export async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
@@ -17,6 +17,7 @@ export async function deliverReplies(params: {
|
|||||||
textLimit: number;
|
textLimit: number;
|
||||||
replyThreadTs?: string;
|
replyThreadTs?: string;
|
||||||
replyToMode: "off" | "first" | "all";
|
replyToMode: "off" | "first" | "all";
|
||||||
|
identity?: SlackSendIdentity;
|
||||||
}) {
|
}) {
|
||||||
for (const payload of params.replies) {
|
for (const payload of params.replies) {
|
||||||
// Keep reply tags opt-in: when replyToMode is off, explicit reply tags
|
// Keep reply tags opt-in: when replyToMode is off, explicit reply tags
|
||||||
@@ -38,6 +39,7 @@ export async function deliverReplies(params: {
|
|||||||
token: params.token,
|
token: params.token,
|
||||||
threadTs,
|
threadTs,
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
|
...(params.identity ? { identity: params.identity } : {}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let first = true;
|
let first = true;
|
||||||
@@ -49,6 +51,7 @@ export async function deliverReplies(params: {
|
|||||||
mediaUrl,
|
mediaUrl,
|
||||||
threadTs,
|
threadTs,
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
|
...(params.identity ? { identity: params.identity } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user