



























@@ -32,10 +32,35 @@ const BASE_ACTION_MEDIA_SOURCE_PARAM_KEYS = [
3232"image",
3333] as const;
343435+const STRUCTURED_ATTACHMENT_MEDIA_SOURCE_PARAM_KEYS = [
36+"media",
37+"mediaUrl",
38+"path",
39+"filePath",
40+"fileUrl",
41+"url",
42+] as const;
43+const STRUCTURED_ATTACHMENT_FILE_SOURCE_PARAM_KEYS = new Set(["path", "filePath", "fileUrl"]);
44+45+type StructuredAttachmentSource = {
46+attachment: Record<string, unknown>;
47+key: string;
48+value: string;
49+kind: "media" | "file";
50+contentType?: string;
51+filename?: string;
52+};
53+54+type StructuredAttachmentMode = "selected" | "all";
55+3556function readMediaParam(args: Record<string, unknown>, key: string): string | undefined {
3657return readStringParam(args, key, { trim: false });
3758}
385960+function isRecord(value: unknown): value is Record<string, unknown> {
61+return Boolean(value && typeof value === "object" && !Array.isArray(value));
62+}
63+3964function resolveMediaParamEntry(
4065args: Record<string, unknown>,
4166key: string,
@@ -54,6 +79,61 @@ function resolveMediaParamEntry(
5479};
5580}
568182+function hasExplicitAttachmentPayload(
83+args: Record<string, unknown>,
84+extraParamKeys?: readonly string[],
85+): boolean {
86+if (readStringParam(args, "buffer", { trim: false })) {
87+return true;
88+}
89+return buildActionMediaSourceParamKeys(extraParamKeys).some((key) => {
90+const entry = resolveMediaParamEntry(args, key);
91+return Boolean(entry && normalizeOptionalString(entry.value));
92+});
93+}
94+95+function collectStructuredAttachmentSources(
96+args: Record<string, unknown>,
97+): StructuredAttachmentSource[] {
98+const attachments = args.attachments;
99+if (!Array.isArray(attachments)) {
100+return [];
101+}
102+const sources: StructuredAttachmentSource[] = [];
103+for (const attachment of attachments) {
104+if (!isRecord(attachment)) {
105+continue;
106+}
107+for (const key of STRUCTURED_ATTACHMENT_MEDIA_SOURCE_PARAM_KEYS) {
108+const entry = resolveMediaParamEntry(attachment, key);
109+if (!entry || !normalizeOptionalString(entry.value)) {
110+continue;
111+}
112+sources.push({
113+ attachment,
114+key: entry.key,
115+value: entry.value,
116+kind: STRUCTURED_ATTACHMENT_FILE_SOURCE_PARAM_KEYS.has(key) ? "file" : "media",
117+contentType:
118+readStringParam(attachment, "contentType") ?? readStringParam(attachment, "mimeType"),
119+filename: readStringParam(attachment, "filename") ?? readStringParam(attachment, "name"),
120+});
121+break;
122+}
123+}
124+return sources;
125+}
126+127+function resolveStructuredAttachmentSource(
128+args: Record<string, unknown>,
129+extraParamKeys?: readonly string[],
130+): StructuredAttachmentSource | undefined {
131+if (hasExplicitAttachmentPayload(args, extraParamKeys)) {
132+return undefined;
133+}
134+return collectStructuredAttachmentSources(args)[0];
135+}
136+57137function buildActionMediaSourceParamKeys(extraParamKeys?: readonly string[]): string[] {
58138const keys = new Set<string>(BASE_ACTION_MEDIA_SOURCE_PARAM_KEYS);
59139extraParamKeys?.forEach((key) => keys.add(key));
@@ -91,6 +171,7 @@ export function resolveExtraActionMediaSourceParamKeys(params: {
91171export function collectActionMediaSourceHints(
92172args: Record<string, unknown>,
93173extraParamKeys?: readonly string[],
174+options?: { structuredAttachments?: StructuredAttachmentMode },
94175): string[] {
95176const sources: string[] = [];
96177for (const key of buildActionMediaSourceParamKeys(extraParamKeys)) {
@@ -99,6 +180,14 @@ export function collectActionMediaSourceHints(
99180sources.push(entry.value);
100181}
101182}
183+if (options?.structuredAttachments === "all") {
184+sources.push(...collectStructuredAttachmentSources(args).map((source) => source.value));
185+} else {
186+const attachmentSource = resolveStructuredAttachmentSource(args, extraParamKeys);
187+if (attachmentSource) {
188+sources.push(attachmentSource.value);
189+}
190+}
102191return sources;
103192}
104193@@ -306,6 +395,7 @@ export async function normalizeSandboxMediaParams(params: {
306395args: Record<string, unknown>;
307396mediaPolicy: AttachmentMediaPolicy;
308397extraParamKeys?: readonly string[];
398+structuredAttachments?: StructuredAttachmentMode;
309399}): Promise<void> {
310400const sandboxRoot =
311401params.mediaPolicy.mode === "sandbox" ? params.mediaPolicy.sandboxRoot.trim() : undefined;
@@ -323,6 +413,28 @@ export async function normalizeSandboxMediaParams(params: {
323413params.args[entry.key] = normalized;
324414}
325415}
416+const attachmentSources =
417+params.structuredAttachments === "all"
418+ ? collectStructuredAttachmentSources(params.args)
419+ : [resolveStructuredAttachmentSource(params.args, params.extraParamKeys)].filter(
420+(source): source is StructuredAttachmentSource => Boolean(source),
421+);
422+if (attachmentSources.length === 0) {
423+return;
424+}
425+for (const attachmentSource of attachmentSources) {
426+assertMediaNotDataUrl(attachmentSource.value);
427+if (!sandboxRoot) {
428+continue;
429+}
430+const normalized = await resolveSandboxedMediaSource({
431+media: attachmentSource.value,
432+ sandboxRoot,
433+});
434+if (normalized !== attachmentSource.value) {
435+attachmentSource.attachment[attachmentSource.key] = normalized;
436+}
437+}
326438}
327439328440export async function normalizeSandboxMediaList(params: {
@@ -360,11 +472,21 @@ async function hydrateAttachmentActionPayload(params: {
360472allowMessageCaptionFallback?: boolean;
361473mediaPolicy: AttachmentMediaPolicy;
362474optimizeImages?: boolean;
475+extraParamKeys?: readonly string[];
363476}): Promise<void> {
477+const attachmentSource = resolveStructuredAttachmentSource(params.args, params.extraParamKeys);
364478const mediaHint = readAttachmentMediaHint(params.args);
365479const fileHint = readAttachmentFileHint(params.args);
366480const contentTypeParam =
367-readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType");
481+readStringParam(params.args, "contentType") ??
482+readStringParam(params.args, "mimeType") ??
483+attachmentSource?.contentType;
484+if (attachmentSource?.filename && !readStringParam(params.args, "filename")) {
485+params.args.filename = attachmentSource.filename;
486+}
487+if (attachmentSource?.contentType && !readStringParam(params.args, "contentType")) {
488+params.args.contentType = attachmentSource.contentType;
489+}
368490369491if (params.allowMessageCaptionFallback) {
370492const caption = readStringParam(params.args, "caption", { allowEmpty: true })?.trim();
@@ -381,8 +503,9 @@ async function hydrateAttachmentActionPayload(params: {
381503args: params.args,
382504dryRun: params.dryRun,
383505 contentTypeParam,
384- mediaHint,
385- fileHint,
506+mediaHint:
507+mediaHint ?? (attachmentSource?.kind === "media" ? attachmentSource.value : undefined),
508+fileHint: fileHint ?? (attachmentSource?.kind === "file" ? attachmentSource.value : undefined),
386509mediaPolicy: params.mediaPolicy,
387510optimizeImages: params.optimizeImages,
388511});
@@ -396,6 +519,7 @@ export async function hydrateAttachmentParamsForAction(params: {
396519action: ChannelMessageActionName;
397520dryRun?: boolean;
398521mediaPolicy: AttachmentMediaPolicy;
522+extraParamKeys?: readonly string[];
399523}): Promise<void> {
400524const shouldHydrateUploadFile = params.action === "upload-file";
401525// Reply gets the same hydration as sendAttachment so threaded sends with
@@ -421,6 +545,7 @@ export async function hydrateAttachmentParamsForAction(params: {
421545args: params.args,
422546dryRun: params.dryRun,
423547mediaPolicy: params.mediaPolicy,
548+extraParamKeys: params.extraParamKeys,
424549optimizeImages: shouldHydrateUploadFile && forceDocument ? false : undefined,
425550allowMessageCaptionFallback: params.action === "sendAttachment" || shouldHydrateUploadFile,
426551});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。