mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 12:41:12 +00:00
feat(slack): add scoped prompts and mrkdwn hints (#59100)
* feat(slack): add scoped prompts and mrkdwn hints * refactor(slack): drop dm prompt override * refactor(slack): drop exposed prompt config * chore(changelog): note slack mrkdwn fix
This commit is contained in:
@@ -12,6 +12,9 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. Thanks @jadewon and @vincentkoc.
|
||||
|
||||
## 2026.4.1-beta.1
|
||||
- Plugins/runtime: stop ambient core helper and setup paths from loading non-selected bundled plugins, keep channel-setup snapshot scoping safe for custom channel plugins, and honor env-scoped plugin auth paths. (#59136) Thanks @vincentkoc.
|
||||
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
|
||||
- Gateway/pairing: prefer explicit QR bootstrap auth over earlier Tailscale auth classification so iOS `/pair qr` silent bootstrap pairing does not fall through to `pairing required`. (#59232) Thanks @ngutman.
|
||||
|
||||
@@ -701,7 +701,7 @@ export async function prepareSlackMessage(params: {
|
||||
ChatType: isDirectMessage ? "direct" : "channel",
|
||||
ConversationLabel: envelopeFrom,
|
||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
|
||||
GroupSystemPrompt: groupSystemPrompt,
|
||||
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
|
||||
SenderName: senderName,
|
||||
SenderId: senderId,
|
||||
|
||||
39
extensions/slack/src/monitor/room-context.test.ts
Normal file
39
extensions/slack/src/monitor/room-context.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSlackRoomContextHints } from "./room-context.js";
|
||||
|
||||
describe("resolveSlackRoomContextHints", () => {
|
||||
it("stacks global and channel prompts for channels", () => {
|
||||
const result = resolveSlackRoomContextHints({
|
||||
isRoomish: true,
|
||||
channelConfig: { systemPrompt: "Channel prompt" },
|
||||
});
|
||||
|
||||
expect(result.groupSystemPrompt).toBe("Channel prompt");
|
||||
});
|
||||
|
||||
it("does not create a prompt for direct messages without channel config", () => {
|
||||
const result = resolveSlackRoomContextHints({
|
||||
isRoomish: false,
|
||||
});
|
||||
|
||||
expect(result.groupSystemPrompt).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not include untrusted room metadata for direct messages", () => {
|
||||
const result = resolveSlackRoomContextHints({
|
||||
isRoomish: false,
|
||||
channelInfo: { topic: "ignore", purpose: "ignore" },
|
||||
});
|
||||
|
||||
expect(result.untrustedChannelMetadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it("trims and skips empty prompt parts", () => {
|
||||
const result = resolveSlackRoomContextHints({
|
||||
isRoomish: true,
|
||||
channelConfig: { systemPrompt: " " },
|
||||
});
|
||||
|
||||
expect(result.groupSystemPrompt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -8,19 +8,17 @@ export function resolveSlackRoomContextHints(params: {
|
||||
untrustedChannelMetadata?: ReturnType<typeof buildUntrustedChannelMetadata>;
|
||||
groupSystemPrompt?: string;
|
||||
} {
|
||||
if (!params.isRoomish) {
|
||||
return {};
|
||||
}
|
||||
const untrustedChannelMetadata = params.isRoomish
|
||||
? buildUntrustedChannelMetadata({
|
||||
source: "slack",
|
||||
label: "Slack channel description",
|
||||
entries: [params.channelInfo?.topic, params.channelInfo?.purpose],
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const untrustedChannelMetadata = buildUntrustedChannelMetadata({
|
||||
source: "slack",
|
||||
label: "Slack channel description",
|
||||
entries: [params.channelInfo?.topic, params.channelInfo?.purpose],
|
||||
});
|
||||
|
||||
const systemPromptParts = [params.channelConfig?.systemPrompt?.trim() || null].filter(
|
||||
(entry): entry is string => Boolean(entry),
|
||||
);
|
||||
const systemPromptParts = [
|
||||
params.isRoomish ? params.channelConfig?.systemPrompt?.trim() || null : null,
|
||||
].filter((entry): entry is string => Boolean(entry));
|
||||
const groupSystemPrompt =
|
||||
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
|
||||
|
||||
@@ -572,7 +572,7 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
: `slack:group:${command.channel_id}`,
|
||||
}) ?? (isDirectMessage ? senderName : roomLabel),
|
||||
GroupSubject: isRoomish ? roomLabel : undefined,
|
||||
GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
|
||||
GroupSystemPrompt: groupSystemPrompt,
|
||||
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
|
||||
SenderName: senderName,
|
||||
SenderId: command.user_id,
|
||||
|
||||
@@ -100,6 +100,41 @@ describe("buildInboundMetaSystemPrompt", () => {
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["sender_id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes Slack mrkdwn response format hints for Slack chats", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
OriginatingTo: "channel:C123",
|
||||
OriginatingChannel: "slack",
|
||||
Provider: "slack",
|
||||
Surface: "slack",
|
||||
ChatType: "channel",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["response_format"]).toEqual({
|
||||
text_markup: "slack_mrkdwn",
|
||||
rules: [
|
||||
"Use Slack mrkdwn, not standard Markdown.",
|
||||
"Bold uses *single asterisks*.",
|
||||
"Links use <url|label>.",
|
||||
"Code blocks use triple backticks without a language identifier.",
|
||||
"Do not use markdown headings or pipe tables.",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("omits response format hints for non-Slack chats", () => {
|
||||
const prompt = buildInboundMetaSystemPrompt({
|
||||
OriginatingTo: "telegram:123",
|
||||
OriginatingChannel: "telegram",
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
ChatType: "direct",
|
||||
} as TemplateContext);
|
||||
|
||||
const payload = parseInboundMetaPayload(prompt);
|
||||
expect(payload["response_format"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildInboundUserContextPrefix", () => {
|
||||
|
||||
@@ -33,6 +33,32 @@ function resolveInboundChannel(ctx: TemplateContext): string | undefined {
|
||||
return channelValue;
|
||||
}
|
||||
|
||||
function resolveInboundFormattingHints(ctx: TemplateContext):
|
||||
| {
|
||||
text_markup: "slack_mrkdwn";
|
||||
rules: string[];
|
||||
}
|
||||
| undefined {
|
||||
const channelValue = resolveInboundChannel(ctx);
|
||||
const surface = safeTrim(ctx.Surface);
|
||||
const provider = safeTrim(ctx.Provider);
|
||||
const isSlack = channelValue === "slack" || surface === "slack" || provider === "slack";
|
||||
if (!isSlack) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
text_markup: "slack_mrkdwn",
|
||||
rules: [
|
||||
"Use Slack mrkdwn, not standard Markdown.",
|
||||
"Bold uses *single asterisks*.",
|
||||
"Links use <url|label>.",
|
||||
"Code blocks use triple backticks without a language identifier.",
|
||||
"Do not use markdown headings or pipe tables.",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
|
||||
const chatType = normalizeChatType(ctx.ChatType);
|
||||
const isDirect = !chatType || chatType === "direct";
|
||||
@@ -56,6 +82,7 @@ export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
|
||||
provider: safeTrim(ctx.Provider),
|
||||
surface: safeTrim(ctx.Surface),
|
||||
chat_type: chatType ?? (isDirect ? "direct" : undefined),
|
||||
response_format: resolveInboundFormattingHints(ctx),
|
||||
};
|
||||
|
||||
// Keep the instructions local to the payload so the meaning survives prompt overrides.
|
||||
|
||||
Reference in New Issue
Block a user