mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 09:01:40 +00:00
* refactor(media): delete internal MsgContext.Media* parallel fields for fact-only runtime Internal runtime now carries a fact-only RuntimeMsgContext: the legacy MediaPath/MediaUrl/MediaType parallel fields (and plural/transcribed variants), their alignment and default helpers, and bundled legacy payload builders are removed from internal paths. Staging, hooks, Gateway, media understanding, and the Telegram, QQ, Signal, Slack, iMessage, Discord, QA, and Zalo plugins consume ordered MediaFact[] directly. Retained boundaries per the program audit: public Plugin SDK MsgContext, persisted transcript Media* rows, and documented template variables keep working via projectMediaFacts at the five declared seams (fact owner, channel payload, inbound-event, transcript persistence, SDK adapter). Review-round hardening, each with regressions: - hasStagedMediaProjection requires every path-bearing fact staged (was any-satisfied; mixed contexts skipped staging unstaged facts). - finalizeInboundContext returns Omit<T, LegacyMediaContextKey> so deleted legacy fields leave the type when they leave the object. - QQ image facts carry explicit kind: "image" (remote URLs have no MIME). - resolveStagedMediaFacts adopts staged legacy paths positionally while retaining canonical fact metadata and cardinality (staged projections previously replaced canonical facts wholesale). 1,615 tests across 47 files; goldens untouched; delegated check:changed green (run 30009882062). * test(media): migrate remaining legacy Media* consumer suites to structured facts Exact-head CI caught suites outside the curated affected set that still asserted internal legacy fields. All were stale internal-field assertions migrated to facts, except QA Channel, which had a real migration gap: it still constructed the bundled legacy payload internally and now normalizes saved attachments with toInboundMediaFacts and passes the declared media parameter directly. Exhaustive rg sweep over every test referencing the deleted fields (133 files, each run individually) is green.
165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
// Telegram plugin module implements bot message context.audio transcript support behavior.
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const transcribeFirstAudioMock = vi.fn();
|
|
const DEFAULT_MODEL = "anthropic/claude-opus-4-5";
|
|
const DEFAULT_WORKSPACE = "/tmp/openclaw";
|
|
const DEFAULT_MENTION_PATTERN = "\\bbot\\b";
|
|
|
|
vi.mock("./media-understanding.runtime.js", () => ({
|
|
transcribeFirstAudio: (...args: unknown[]) => transcribeFirstAudioMock(...args),
|
|
}));
|
|
|
|
const { buildTelegramMessageContextForTest } =
|
|
await import("./bot-message-context.test-harness.js");
|
|
|
|
async function buildGroupVoiceContext(params: {
|
|
messageId: number;
|
|
chatId: number;
|
|
title: string;
|
|
date: number;
|
|
fromId: number;
|
|
firstName: string;
|
|
fileId: string;
|
|
mediaPath: string;
|
|
groupDisableAudioPreflight?: boolean;
|
|
topicDisableAudioPreflight?: boolean;
|
|
}) {
|
|
const groupConfig = {
|
|
requireMention: true,
|
|
...(params.groupDisableAudioPreflight === undefined
|
|
? {}
|
|
: { disableAudioPreflight: params.groupDisableAudioPreflight }),
|
|
};
|
|
const topicConfig =
|
|
params.topicDisableAudioPreflight === undefined
|
|
? undefined
|
|
: { disableAudioPreflight: params.topicDisableAudioPreflight };
|
|
|
|
return buildTelegramMessageContextForTest({
|
|
message: {
|
|
message_id: params.messageId,
|
|
chat: { id: params.chatId, type: "supergroup", title: params.title },
|
|
date: params.date,
|
|
text: undefined,
|
|
from: { id: params.fromId, first_name: params.firstName },
|
|
voice: { file_id: params.fileId },
|
|
},
|
|
allMedia: [{ path: params.mediaPath, contentType: "audio/ogg", kind: "audio" }],
|
|
options: { forceWasMentioned: true },
|
|
cfg: {
|
|
agents: { defaults: { model: DEFAULT_MODEL, workspace: DEFAULT_WORKSPACE } },
|
|
channels: { telegram: {} },
|
|
messages: { groupChat: { mentionPatterns: [DEFAULT_MENTION_PATTERN] } },
|
|
},
|
|
resolveGroupActivation: () => true,
|
|
resolveGroupRequireMention: () => true,
|
|
resolveTelegramGroupConfig: () => ({
|
|
groupConfig,
|
|
topicConfig,
|
|
}),
|
|
});
|
|
}
|
|
|
|
function expectTranscriptRendered(
|
|
ctx: Awaited<ReturnType<typeof buildGroupVoiceContext>>,
|
|
transcript: string,
|
|
) {
|
|
const framed = `[Audio transcript (machine-generated, untrusted)]: ${JSON.stringify(transcript)}`;
|
|
expect(ctx).not.toBeNull();
|
|
expect(ctx?.ctxPayload?.BodyForAgent).toBe(framed);
|
|
expect(ctx?.ctxPayload?.Body).toContain(framed);
|
|
expect(ctx?.ctxPayload?.Body).not.toContain("<media:audio>");
|
|
expect(ctx?.ctxPayload?.media?.[0]?.transcribed).toBe(true);
|
|
}
|
|
|
|
function expectAudioFactWithEmptyBody(ctx: Awaited<ReturnType<typeof buildGroupVoiceContext>>) {
|
|
expect(ctx).not.toBeNull();
|
|
expect(ctx?.ctxPayload?.BodyForAgent).toBe("");
|
|
expect(ctx?.ctxPayload?.RawBody).toBe("");
|
|
expect(ctx?.ctxPayload?.media?.[0]?.contentType).toBe("audio/ogg");
|
|
}
|
|
|
|
describe("buildTelegramMessageContext audio transcript body", () => {
|
|
beforeEach(() => {
|
|
transcribeFirstAudioMock.mockReset();
|
|
});
|
|
|
|
it("uses preflight transcript as BodyForAgent for mention-gated group voice messages", async () => {
|
|
transcribeFirstAudioMock.mockResolvedValueOnce("hey bot please help");
|
|
|
|
const ctx = await buildGroupVoiceContext({
|
|
messageId: 1,
|
|
chatId: -1001234567890,
|
|
title: "Test Group",
|
|
date: 1700000000,
|
|
fromId: 42,
|
|
firstName: "Alice",
|
|
fileId: "voice-1",
|
|
mediaPath: "/tmp/voice.ogg",
|
|
});
|
|
|
|
expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1);
|
|
expectTranscriptRendered(ctx, "hey bot please help");
|
|
});
|
|
|
|
it("skips preflight transcription when disableAudioPreflight is true", async () => {
|
|
transcribeFirstAudioMock.mockClear();
|
|
|
|
const ctx = await buildGroupVoiceContext({
|
|
messageId: 2,
|
|
chatId: -1001234567891,
|
|
title: "Test Group 2",
|
|
date: 1700000100,
|
|
fromId: 43,
|
|
firstName: "Bob",
|
|
fileId: "voice-2",
|
|
mediaPath: "/tmp/voice2.ogg",
|
|
groupDisableAudioPreflight: true,
|
|
});
|
|
|
|
expect(transcribeFirstAudioMock).not.toHaveBeenCalled();
|
|
expectAudioFactWithEmptyBody(ctx);
|
|
});
|
|
|
|
it("uses topic disableAudioPreflight=false to override group disableAudioPreflight=true", async () => {
|
|
transcribeFirstAudioMock.mockResolvedValueOnce("topic override transcript");
|
|
|
|
const ctx = await buildGroupVoiceContext({
|
|
messageId: 3,
|
|
chatId: -1001234567892,
|
|
title: "Test Group 3",
|
|
date: 1700000200,
|
|
fromId: 44,
|
|
firstName: "Cara",
|
|
fileId: "voice-3",
|
|
mediaPath: "/tmp/voice3.ogg",
|
|
groupDisableAudioPreflight: true,
|
|
topicDisableAudioPreflight: false,
|
|
});
|
|
|
|
expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1);
|
|
expectTranscriptRendered(ctx, "topic override transcript");
|
|
});
|
|
|
|
it("uses topic disableAudioPreflight=true to override group disableAudioPreflight=false", async () => {
|
|
transcribeFirstAudioMock.mockClear();
|
|
|
|
const ctx = await buildGroupVoiceContext({
|
|
messageId: 4,
|
|
chatId: -1001234567893,
|
|
title: "Test Group 4",
|
|
date: 1700000300,
|
|
fromId: 45,
|
|
firstName: "Dan",
|
|
fileId: "voice-4",
|
|
mediaPath: "/tmp/voice4.ogg",
|
|
groupDisableAudioPreflight: false,
|
|
topicDisableAudioPreflight: true,
|
|
});
|
|
|
|
expect(transcribeFirstAudioMock).not.toHaveBeenCalled();
|
|
expectAudioFactWithEmptyBody(ctx);
|
|
});
|
|
});
|