






















@@ -38,6 +38,12 @@ type MattermostSendText = NonNullable<NonNullable<typeof mattermostPlugin.outbou
3838type MattermostSendTextParams = Parameters<MattermostSendText>[0];
3939type MattermostSendMedia = NonNullable<NonNullable<typeof mattermostPlugin.outbound>["sendMedia"]>;
4040type MattermostSendMediaParams = Parameters<MattermostSendMedia>[0];
41+type MattermostRenderPresentation = NonNullable<
42+NonNullable<typeof mattermostPlugin.outbound>["renderPresentation"]
43+>;
44+type MattermostSendPayload = NonNullable<
45+NonNullable<typeof mattermostPlugin.outbound>["sendPayload"]
46+>;
41474248function getDescribedActions(cfg: OpenClawConfig, accountId?: string): string[] {
4349return [...(mattermostPlugin.actions?.describeMessageTool?.({ cfg, accountId })?.actions ?? [])];
@@ -91,6 +97,22 @@ function requireMattermostChunker() {
9197return chunker;
9298}
9399100+function requireMattermostRenderPresentation(): MattermostRenderPresentation {
101+const renderPresentation = mattermostPlugin.outbound?.renderPresentation;
102+if (!renderPresentation) {
103+throw new Error("mattermost outbound.renderPresentation missing");
104+}
105+return renderPresentation;
106+}
107+108+function requireMattermostSendPayload(): MattermostSendPayload {
109+const sendPayload = mattermostPlugin.outbound?.sendPayload;
110+if (!sendPayload) {
111+throw new Error("mattermost outbound.sendPayload missing");
112+}
113+return sendPayload;
114+}
115+94116function createMattermostActionContext(
95117overrides: Partial<MattermostActionContext>,
96118): MattermostActionContext {
@@ -427,6 +449,41 @@ describe("mattermostPlugin", () => {
427449expect(options.replyToId).toBe("post-root");
428450});
429451452+it("maps presentation buttons without using legacy interactive conversion", async () => {
453+const cfg = createMattermostTestConfig();
454+455+await mattermostPlugin.actions?.handleAction?.(
456+createMattermostActionContext({
457+action: "send",
458+params: {
459+to: "channel:CHAN1",
460+message: "Deploy finished",
461+presentation: {
462+blocks: [
463+{
464+type: "buttons",
465+buttons: [
466+{ label: "Open", value: "open", style: "primary" },
467+{ label: "Docs", url: "https://example.com/docs" },
468+],
469+},
470+],
471+},
472+},
473+ cfg,
474+accountId: "default",
475+}),
476+);
477+478+const options = expectSingleMattermostSend(
479+"channel:CHAN1",
480+"Deploy finished\n\n- Open\n- Docs: https://example.com/docs",
481+);
482+expect(options.buttons).toStrictEqual([
483+[{ text: "Open", callback_data: "open", style: "primary" }],
484+]);
485+});
486+430487it("falls back to trimmed replyTo when replyToId is blank", async () => {
431488const cfg = createMattermostTestConfig();
432489@@ -451,6 +508,86 @@ describe("mattermostPlugin", () => {
451508});
452509453510describe("outbound", () => {
511+it("renders presentation buttons for normal reply payload delivery", async () => {
512+const renderPresentation = requireMattermostRenderPresentation();
513+const sendPayload = requireMattermostSendPayload();
514+const cfg = createMattermostTestConfig();
515+const presentation = {
516+blocks: [
517+{ type: "text" as const, text: "Deploy finished" },
518+{
519+type: "buttons" as const,
520+buttons: [
521+{ label: "Open", value: "open", style: "primary" as const },
522+{ label: "Docs", url: "https://example.com/docs" },
523+],
524+},
525+],
526+};
527+const rendered = await renderPresentation({
528+payload: { presentation },
529+ presentation,
530+ctx: {
531+ cfg,
532+to: "channel:CHAN1",
533+text: "",
534+payload: { presentation },
535+},
536+});
537+538+expect(rendered).toMatchObject({
539+text: "Deploy finished\n\n- Open\n- Docs: https://example.com/docs",
540+channelData: {
541+mattermost: {
542+presentationButtons: [[{ text: "Open", callback_data: "open", style: "primary" }]],
543+},
544+},
545+});
546+547+await sendPayload({
548+ cfg,
549+to: "channel:CHAN1",
550+text: "",
551+payload: rendered!,
552+});
553+554+const options = expectSingleMattermostSend(
555+"channel:CHAN1",
556+"Deploy finished\n\n- Open\n- Docs: https://example.com/docs",
557+);
558+expect(options.buttons).toStrictEqual([
559+[{ text: "Open", callback_data: "open", style: "primary" }],
560+]);
561+});
562+563+it("keeps multi-media presentation payloads on the text/media fallback path", async () => {
564+const renderPresentation = requireMattermostRenderPresentation();
565+const presentation = {
566+blocks: [
567+{
568+type: "buttons" as const,
569+buttons: [{ label: "Open", value: "open" }],
570+},
571+],
572+};
573+574+expect(
575+await renderPresentation({
576+payload: {
577+ presentation,
578+mediaUrls: ["https://example.com/1.png", "https://example.com/2.png"],
579+},
580+ presentation,
581+ctx: {
582+cfg: createMattermostTestConfig(),
583+to: "channel:CHAN1",
584+text: "",
585+payload: { presentation },
586+},
587+}),
588+).toBeNull();
589+});
590+454591it("chunks outbound text without requiring Mattermost runtime initialization", () => {
455592const chunker = requireMattermostChunker();
456593此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。