返回 AiToEarn
PublishDialog.util.ts
根目录 / project / aitoearn-web / src / components / PublishDialog / PublishDialog.util.ts
1 import type { SocialAccount } from '@/api/accounts/account.types'
2 import type {
3 ChannelCreatePublishFlowItem,
4 ChannelCreatePublishFlowParams,
5 ChannelPublishContentInput,
6 ChannelPublishFlowVo,
7 ChannelPublishSource,
8 } from '@/api/channels/channel.types'
9 import type { MediaItem, PromotionMaterial } from '@/api/materials/material.types'
10
11 import type { IImgFile, IPubParams, IVideoFile, PubItem } from '@/components/PublishDialog/publishDialog.type'
12 import { PlatType } from '@/app/config/platConfig'
13 import { PubType } from '@/app/config/publishConfig'
14 import { getPlatformInfoSync } from '@/store/platformMetadata'
15 import { generateUUID, getFilePathName, parseTopicString } from '@/utils/common'
16 import { getOssProxyPath, getOssUrl } from '@/utils/oss'
17
18 type AccountIdentityFields = Pick<SocialAccount, 'id' | 'type' | 'uid'>
19
20 export const PUBLISH_DIALOG_DND_TYPE = 'publish-dialog-draft-media'
21
22 export const PLATFORM_ACCOUNT_BORDER_COLORS: Record<PlatType, string> = {
23 [PlatType.Tiktok]: 'oklch(67% 0.22 190)',
24 [PlatType.Douyin]: 'oklch(64% 0.25 356)',
25 [PlatType.Xhs]: 'oklch(58% 0.24 25)',
26 [PlatType.WxSph]: 'oklch(65% 0.18 150)',
27 [PlatType.KWAI]: 'oklch(66% 0.22 45)',
28 [PlatType.YouTube]: 'oklch(56% 0.25 31)',
29 [PlatType.BILIBILI]: 'oklch(72% 0.16 222)',
30 [PlatType.Twitter]: 'oklch(58% 0.14 255)',
31 [PlatType.WxGzh]: 'oklch(62% 0.17 135)',
32 [PlatType.Facebook]: 'oklch(55% 0.2 265)',
33 [PlatType.Instagram]: 'oklch(62% 0.26 330)',
34 [PlatType.Threads]: 'oklch(57% 0.11 295)',
35 [PlatType.Pinterest]: 'oklch(52% 0.23 18)',
36 [PlatType.LinkedIn]: 'oklch(56% 0.17 240)',
37 }
38
39 export function getPlatformAccountBorderColor(platType: PlatType) {
40 return PLATFORM_ACCOUNT_BORDER_COLORS[platType]
41 }
42
43 const VIDEO_COVER_MIME_TYPE = 'image/jpeg'
44 const VIDEO_COVER_FILE_EXTENSION = 'jpg'
45 const VIDEO_COVER_JPEG_QUALITY = 0.92
46
47 export type PublishDialogDragItem
48 = | { kind: 'draft', material: PromotionMaterial }
49 | { kind: 'media', media: MediaItem }
50
51 export function isPublishTitleSupported(platType: PlatType) {
52 const platInfo = getPlatformInfoSync(platType)
53 if (platInfo)
54 return (platInfo.commonPubParamsConfig.titleMax ?? 0) > 0
55
56 return [
57 PlatType.WxGzh,
58 PlatType.Xhs,
59 PlatType.WxSph,
60 PlatType.Douyin,
61 PlatType.KWAI,
62 PlatType.Pinterest,
63 PlatType.LinkedIn,
64 PlatType.YouTube,
65 PlatType.BILIBILI,
66 ].includes(platType)
67 }
68
69 export function getCommonPublishTitleMax(pubItems: PubItem[]) {
70 const titleMaxList = pubItems
71 .filter(item => isPublishTitleSupported(item.account.type))
72 .map(item => getPlatformInfoSync(item.account.type)?.commonPubParamsConfig.titleMax)
73 .filter((titleMax): titleMax is number => typeof titleMax === 'number' && titleMax > 0)
74
75 if (titleMaxList.length === 0)
76 return undefined
77
78 return Math.min(...titleMaxList)
79 }
80
81 export function getSocialAccountIdentityKeys(account: AccountIdentityFields) {
82 const keys = [`id:${account.id}`]
83
84 if (account.uid) {
85 keys.push(`uid:${account.type}:${account.uid}`)
86 }
87
88 return keys
89 }
90
91 export function isSameSocialAccount(
92 prevAccount: AccountIdentityFields,
93 nextAccount: AccountIdentityFields,
94 ) {
95 if (prevAccount.id === nextAccount.id)
96 return true
97
98 if (prevAccount.type !== nextAccount.type)
99 return false
100
101 if (prevAccount.uid && nextAccount.uid && prevAccount.uid === nextAccount.uid)
102 return true
103
104 return false
105 }
106
107 const INVALID_DESC_TOPIC_REGEX = /#(?:\s+\S+|\S*#\S*)/
108
109 export function hasInvalidDescTopicFormat(value: string) {
110 return INVALID_DESC_TOPIC_REGEX.test(value)
111 }
112
113 export async function formatVideo(file: File): Promise<IVideoFile> {
114 const videoUrl = URL.createObjectURL(file)
115 const videoInfo = await VideoGrabFrame(videoUrl, 0)
116
117 return {
118 filename: file.name,
119 videoUrl,
120 size: file.size!,
121 file,
122 ...videoInfo,
123 }
124 }
125
126 export function VideoGrabFrame(
127 videoUrl: string,
128 currentTime: number,
129 ): Promise<{
130 width: number
131 height: number
132 // 下取整的时长
133 duration: number
134 // 视频首帧
135 cover: IImgFile
136 }> {
137 return new Promise((resolve, reject) => {
138 const video = document.createElement('video')
139 video.crossOrigin = 'anonymous'
140 // 添加查询参数避免浏览器复用非 CORS 缓存(同一 URL 无 crossOrigin 的请求会污染缓存)
141 const url = getOssUrl(videoUrl)
142 video.src = url.startsWith('blob:') ? url : `${url}${url.includes('?') ? '&' : '?'}x-cors=1`
143
144 // 设置超时
145 const timeout = setTimeout(() => {
146 video.remove()
147 reject(new Error('视频加载超时'))
148 }, 30000) // 30秒超时
149
150 // 错误处理
151 video.addEventListener('error', (e) => {
152 clearTimeout(timeout)
153 video.remove()
154 reject(new Error(`视频加载失败: ${video.error?.message || '未知错误'}`))
155 })
156
157 // 当视频元数据加载完毕时执行回调
158 video.addEventListener('loadedmetadata', () => {
159 video.currentTime = currentTime
160 })
161
162 video.addEventListener('seeked', () => {
163 // 获取视频的宽度和高度
164 const width = video.videoWidth
165 const height = video.videoHeight
166 // 获取视频的时长
167 const duration = video.duration
168
169 // 获取视频首帧
170 const canvas = document.createElement('canvas')
171 canvas.width = width
172 canvas.height = height
173 const context = canvas.getContext('2d')!
174 context.fillStyle = 'white'
175 context.fillRect(0, 0, width, height)
176 context.drawImage(video, 0, 0)
177
178 // 尝试导出canvas,可能因为CORS失败
179 try {
180 canvas.toBlob(async (blob) => {
181 if (!blob) {
182 clearTimeout(timeout)
183 video.remove()
184 reject(new Error('Canvas转换为Blob失败'))
185 return
186 }
187
188 try {
189 const cover = await formatImg({
190 blob,
191 path: `cover.${VIDEO_COVER_FILE_EXTENSION}`,
192 })
193 clearTimeout(timeout)
194 resolve({
195 width,
196 height,
197 duration: Math.floor(duration),
198 cover,
199 })
200 video.remove()
201 }
202 catch (formatError) {
203 clearTimeout(timeout)
204 video.remove()
205 reject(new Error(`格式化封面失败: ${formatError}`))
206 }
207 }, VIDEO_COVER_MIME_TYPE, VIDEO_COVER_JPEG_QUALITY)
208 }
209 catch (canvasError) {
210 clearTimeout(timeout)
211 video.remove()
212 reject(new Error(`Canvas操作失败(可能是CORS问题): ${canvasError}`))
213 }
214 })
215
216 // 加载视频
217 video.load()
218 })
219 }
220
221 export async function formatImg({
222 path,
223 file,
224 blob,
225 }: {
226 path: string
227 file?: Uint8Array
228 blob?: Blob
229 }): Promise<IImgFile> {
230 return new Promise((resolve) => {
231 const { filename, suffix } = getFilePathName(path)
232 if (!blob) {
233 // @ts-ignore
234 blob = new Blob([file!], {
235 type: `image/${suffix}`,
236 })
237 }
238 const imgUrl = URL.createObjectURL(blob)
239
240 const img = new Image()
241 img.onload = () => {
242 resolve({
243 id: generateUUID(),
244 width: img.width,
245 height: img.height,
246 imgPath: path,
247 size: blob!.size,
248 filename,
249 file: new File([blob!], filename, { type: blob!.type }),
250 imgUrl,
251 })
252 }
253 img.src = imgUrl
254 })
255 }
256
257 function getBlobSuffix(blob: Blob, fallback = 'png') {
258 return blob.type.split('/')[1] || fallback
259 }
260
261 function getMediaFileName(title: string | undefined, fallback: string) {
262 return title?.trim() || fallback
263 }
264
265 function appendTopicsToDescription(description: string, topics?: string[]) {
266 if (!topics?.length)
267 return description.trim()
268
269 const { topics: descriptionTopics } = parseTopicString(description)
270 const existingTopicKeys = new Set(descriptionTopics.map(getTopicKey))
271 const appendedTopicKeys = new Set<string>()
272 const topicStr = topics
273 .flatMap((topic) => {
274 const topicText = normalizeTopicText(topic)
275 const topicKey = getTopicKey(topicText)
276 if (!topicText || existingTopicKeys.has(topicKey) || appendedTopicKeys.has(topicKey))
277 return []
278
279 appendedTopicKeys.add(topicKey)
280 return [`#${topicText}`]
281 })
282 .join(' ')
283
284 if (!topicStr)
285 return description.trim()
286
287 return `${description}\n${topicStr}`.trim()
288 }
289
290 function getUniqueNormalizedTopics(topics: string[]) {
291 const topicKeys = new Set<string>()
292 const normalizedTopics: string[] = []
293
294 for (const topic of topics) {
295 const topicText = normalizeTopicText(topic)
296 const topicKey = getTopicKey(topicText)
297 if (!topicText || topicKeys.has(topicKey))
298 continue
299
300 topicKeys.add(topicKey)
301 normalizedTopics.push(topicText)
302 }
303
304 return normalizedTopics
305 }
306
307 function buildDraftPublishText(description: string, topics?: string[]) {
308 const { topics: descriptionTopics, cleanedString } = parseTopicString(description)
309 const mergedTopics = getUniqueNormalizedTopics([...descriptionTopics, ...(topics || [])])
310
311 return {
312 des: appendTopicsToDescription(cleanedString, mergedTopics),
313 topics: mergedTopics,
314 }
315 }
316
317 export function createPublishImageFromUrl({
318 url,
319 id = generateUUID(),
320 filename = '',
321 width = 0,
322 height = 0,
323 }: {
324 url: string
325 id?: string
326 filename?: string
327 width?: number
328 height?: number
329 }): IImgFile {
330 return {
331 id,
332 size: 0,
333 file: new File([], filename),
334 imgUrl: url,
335 ossUrl: url || undefined,
336 filename,
337 imgPath: '',
338 width,
339 height,
340 }
341 }
342
343 export async function createPublishImageFromMedia(media: MediaItem): Promise<IImgFile> {
344 try {
345 const ossUrl = getOssUrl(media.url)
346 const req = await fetch(getOssProxyPath(ossUrl))
347 const blob = await req.blob()
348 const imageFile = await formatImg({
349 blob,
350 path: `${getMediaFileName(media.title, 'image')}.${getBlobSuffix(blob)}`,
351 })
352 imageFile.ossUrl = media.url
353 return imageFile
354 }
355 catch {
356 return createPublishImageFromUrl({
357 id: `media-img-${media._id}`,
358 url: media.url,
359 filename: media.title || '',
360 })
361 }
362 }
363
364 export async function createPublishVideoFromMedia(media: MediaItem): Promise<IVideoFile> {
365 const ossUrl = getOssUrl(media.url)
366 let width = 0
367 let height = 0
368 let duration = Math.floor(media.metadata?.duration || 0)
369 let cover = createPublishImageFromUrl({
370 id: `media-cover-${media._id}`,
371 url: media.thumbUrl || '',
372 filename: media.title || '',
373 })
374
375 try {
376 const videoInfo = await VideoGrabFrame(getOssProxyPath(ossUrl), 0)
377 width = videoInfo.width
378 height = videoInfo.height
379 duration = videoInfo.duration
380 cover = videoInfo.cover
381 }
382 catch {}
383
384 if (media.thumbUrl) {
385 try {
386 const coverOss = getOssUrl(media.thumbUrl)
387 const req = await fetch(getOssProxyPath(coverOss))
388 const blob = await req.blob()
389 cover = await formatImg({
390 blob,
391 path: `${getMediaFileName(media.title, 'cover')}_cover.${getBlobSuffix(blob)}`,
392 })
393 cover.ossUrl = media.thumbUrl
394 }
395 catch {}
396 }
397
398 return {
399 ossUrl,
400 videoUrl: ossUrl,
401 file: new Blob([], { type: media.metadata?.mimeType || 'video/mp4' }),
402 filename: getMediaFileName(media.title, `video_${Date.now()}.mp4`),
403 width,
404 height,
405 duration,
406 size: media.metadata?.size || 0,
407 cover,
408 }
409 }
410
411 async function createPublishVideoFromDraft(material: PromotionMaterial, videoUrl: string): Promise<IVideoFile> {
412 try {
413 const videoInfo = await VideoGrabFrame(videoUrl, 0)
414 const cover = material.coverUrl
415 ? createPublishImageFromUrl({
416 url: material.coverUrl,
417 width: videoInfo.width,
418 height: videoInfo.height,
419 })
420 : videoInfo.cover
421
422 return {
423 size: 0,
424 file: new Blob(),
425 videoUrl,
426 ossUrl: videoUrl,
427 filename: '',
428 width: videoInfo.width,
429 height: videoInfo.height,
430 duration: videoInfo.duration,
431 cover,
432 }
433 }
434 catch {
435 return {
436 size: 0,
437 file: new Blob(),
438 videoUrl,
439 ossUrl: videoUrl,
440 filename: '',
441 width: 0,
442 height: 0,
443 duration: 0,
444 cover: createPublishImageFromUrl({
445 url: material.coverUrl || '',
446 }),
447 }
448 }
449 }
450
451 export async function buildPublishParamsFromDraft(material: PromotionMaterial): Promise<Partial<IPubParams>> {
452 const draftText = buildDraftPublishText(material.desc || '', material.topics)
453 const params: Partial<IPubParams> = {
454 des: draftText.des,
455 title: material.title || '',
456 topics: draftText.topics,
457 video: undefined,
458 images: [],
459 }
460
461 const videoMedia = material.mediaList?.find(media => media.type === 'video')
462 if (videoMedia) {
463 params.video = await createPublishVideoFromDraft(material, videoMedia.url)
464 params.images = []
465 return params
466 }
467
468 params.images = material.mediaList
469 ?.filter(media => media.type === 'img')
470 .map((media, index) => createPublishImageFromUrl({
471 id: `draft-img-${index}`,
472 url: media.url,
473 })) || []
474
475 return params
476 }
477
478 export async function buildPublishParamsFromMedia(
479 media: MediaItem,
480 currentImages: IImgFile[] = [],
481 ): Promise<Partial<IPubParams>> {
482 if (media.type === 'video') {
483 return {
484 video: await createPublishVideoFromMedia(media),
485 images: [],
486 }
487 }
488
489 return {
490 video: undefined,
491 images: [...currentImages, await createPublishImageFromMedia(media)],
492 }
493 }
494
495 /**
496 * 判断宽高是否属于指定比例(带缓冲阈值)
497 * @param width 宽
498 * @param height 高
499 * @param ratio 目标比例(宽/高)
500 * @param threshold 缓冲阈值,默认0.02
501 */
502 export function isAspectRatioMatch(
503 width: number,
504 height: number,
505 ratio: number,
506 threshold: number = 0.02,
507 ): boolean {
508 if (height === 0)
509 return false
510 const actualRatio = width / height
511 return Math.abs(actualRatio - ratio) <= threshold
512 }
513
514 /**
515 * 判断宽高比是否在指定范围内(带缓冲阈值)
516 * @param width 宽
517 * @param height 高
518 * @param minRatio 最小比例(宽/高)
519 * @param maxRatio 最大比例(宽/高)
520 * @param threshold 缓冲阈值,默认0.02
521 */
522 export function isAspectRatioInRange(
523 width: number,
524 height: number,
525 minRatio: number,
526 maxRatio: number,
527 threshold: number = 0.02,
528 ): boolean {
529 if (height === 0)
530 return false
531 const actualRatio = width / height
532 return actualRatio >= minRatio - threshold && actualRatio <= maxRatio + threshold
533 }
534
535 function compactPublishOption(record: Record<string, unknown>) {
536 const result: Record<string, unknown> = {}
537
538 Object.entries(record).forEach(([key, value]) => {
539 if (value === undefined || value === null)
540 return
541 if (Array.isArray(value) && value.length === 0)
542 return
543 result[key] = value
544 })
545
546 return Object.keys(result).length > 0 ? result : undefined
547 }
548
549 function normalizeTopicText(topic: string) {
550 return topic
551 .replace(/\[话题\]/g, '')
552 .replace(/^#+/, '')
553 .replace(/#+$/, '')
554 .trim()
555 }
556
557 function getTopicKey(topic: string) {
558 return normalizeTopicText(topic).toLowerCase()
559 }
560
561 export function buildChannelPublishBody(description?: string, topics?: string[]) {
562 const { topics: descTopics, cleanedString } = parseTopicString(description || '')
563 const mergedTopics = getUniqueNormalizedTopics([...descTopics, ...(topics || [])])
564
565 return appendTopicsToDescription(cleanedString, mergedTopics)
566 }
567
568 function buildChannelPublishContent(
569 params: IPubParams,
570 options?: { includeTitle?: boolean },
571 ): ChannelPublishContentInput {
572 const videoUrl = params.video?.ossUrl
573 const imageUrls = params.images
574 ?.map(image => image.ossUrl)
575 .filter((url): url is string => typeof url === 'string' && url.length > 0) || []
576 const media = videoUrl
577 ? [{ url: videoUrl, metadata: { type: 'video' } }]
578 : imageUrls.map(url => ({ url, metadata: { type: 'image' } }))
579 const coverUrl = getChannelPublishCoverUrl(params)
580
581 const content: ChannelPublishContentInput = {
582 body: buildChannelPublishBody(params.des, params.topics),
583 media,
584 cover: coverUrl ? { url: coverUrl, metadata: { type: 'image' } } : undefined,
585 }
586
587 if (options?.includeTitle !== false)
588 content.title = params.title || ''
589
590 return content
591 }
592
593 function getChannelPublishImageUrls(params: IPubParams) {
594 return params.images
595 ?.map(image => image.ossUrl)
596 .filter((url): url is string => typeof url === 'string' && url.length > 0) || []
597 }
598
599 function getChannelPublishCoverUrl(params: IPubParams) {
600 return params.video?.cover.ossUrl || getChannelPublishImageUrls(params)[0]
601 }
602
603 function buildBilibiliOption(item: PubItem) {
604 const option = item.params.option.bilibili
605 if (!option)
606 return undefined
607
608 return compactPublishOption({
609 tid: option.tid,
610 copyright: option.copyright,
611 source: option.source,
612 })
613 }
614
615 function buildFacebookOption(item: PubItem) {
616 const option = item.params.option.facebook
617 const contentCategory = item.params.video ? 'reel' : option?.content_category || 'post'
618
619 return compactPublishOption({
620 content_category: contentCategory,
621 })
622 }
623
624 function buildInstagramOption(item: PubItem) {
625 const option = item.params.option.instagram
626 const contentCategory = item.params.video ? 'reel' : option?.content_category || 'post'
627 const mediaType = option?.media_type || getInstagramMediaType(item, contentCategory)
628
629 return compactPublishOption({
630 content_category: contentCategory,
631 media_type: mediaType,
632 })
633 }
634
635 function getInstagramMediaType(item: PubItem, contentCategory: string) {
636 if (contentCategory === 'reel')
637 return 'REELS'
638
639 if (item.params.video)
640 return 'VIDEO'
641
642 return (item.params.images?.length || 0) > 1 ? 'CAROUSEL' : 'IMAGE'
643 }
644
645 function buildYoutubeOption(item: PubItem) {
646 const option = item.params.option.youtube
647 if (!option)
648 return undefined
649
650 return compactPublishOption({
651 privacyStatus: option.privacyStatus,
652 license: option.license,
653 categoryId: option.categoryId,
654 notifySubscribers: option.notifySubscribers,
655 embeddable: option.embeddable,
656 selfDeclaredMadeForKids: option.selfDeclaredMadeForKids,
657 })
658 }
659
660 function buildPinterestOption(item: PubItem) {
661 const option = item.params.option.pinterest
662 if (!option)
663 return undefined
664
665 const coverImageUrl = item.params.video
666 ? option.coverImageUrl || getChannelPublishCoverUrl(item.params)
667 : option.coverImageUrl
668
669 return compactPublishOption({
670 boardId: option.boardId,
671 coverImageUrl,
672 })
673 }
674
675 function buildTiktokOption(item: PubItem) {
676 const option = item.params.option.tiktok
677 if (!option)
678 return undefined
679
680 return compactPublishOption({
681 privacy_level: option.privacy_level,
682 disable_comment: option.comment_disabled,
683 disable_duet: option.duet_disabled,
684 disable_stitch: option.stitch_disabled,
685 brand_organic_toggle: option.brand_organic_toggle,
686 brand_content_toggle: option.brand_content_toggle,
687 })
688 }
689
690 function buildThreadsOption(item: PubItem) {
691 const option = item.params.option.threads
692 if (!option)
693 return undefined
694
695 return compactPublishOption({
696 location_id: option.location_id,
697 })
698 }
699
700 function buildTwitterPollOption(item: PubItem) {
701 const poll = item.params.option.twitter?.poll
702 if (!poll)
703 return undefined
704
705 return compactPublishOption({
706 options: poll.options.map(option => option.trim()).filter(Boolean),
707 duration_minutes: poll.durationMinutes,
708 })
709 }
710
711 function buildTwitterOption(item: PubItem) {
712 const option = item.params.option.twitter
713 if (!option)
714 return undefined
715
716 const altText = option.mediaMetadata
717 ?.map(metadata => metadata.altText?.trim())
718 .find((value): value is string => Boolean(value))
719
720 return compactPublishOption({
721 reply_settings: option.replySettings,
722 poll: buildTwitterPollOption(item),
723 made_with_ai: option.madeWithAi,
724 alt_text: altText,
725 })
726 }
727
728 export function buildChannelPublishOption(item: PubItem) {
729 switch (item.account.type) {
730 case 'bilibili':
731 return buildBilibiliOption(item)
732 case 'facebook':
733 return buildFacebookOption(item)
734 case 'instagram':
735 return buildInstagramOption(item)
736 case 'youtube':
737 return buildYoutubeOption(item)
738 case 'pinterest':
739 return buildPinterestOption(item)
740 case 'tiktok':
741 return buildTiktokOption(item)
742 case 'threads':
743 return buildThreadsOption(item)
744 case 'twitter':
745 return buildTwitterOption(item)
746 default:
747 return undefined
748 }
749 }
750
751 export interface BuildChannelPublishFlowParamsOptions {
752 publishAt: string
753 userTaskId?: string
754 materialGroupId?: string
755 materialId?: string
756 source?: ChannelPublishSource
757 }
758
759 export function buildChannelPublishFlowParams(
760 pubItems: PubItem[],
761 options: BuildChannelPublishFlowParamsOptions,
762 ): ChannelCreatePublishFlowParams | null {
763 const firstItem = pubItems[0]
764 if (!firstItem)
765 return null
766
767 const context: ChannelCreatePublishFlowParams['context'] = {}
768 const videoUrl = firstItem.params.video?.ossUrl
769 const imgUrlList = getChannelPublishImageUrls(firstItem.params)
770
771 context.type = videoUrl ? PubType.VIDEO : PubType.ImageText
772 if (videoUrl)
773 context.videoUrl = videoUrl
774 else
775 context.imgUrlList = imgUrlList
776
777 if (options.userTaskId)
778 context.userTaskId = options.userTaskId
779 if (options.materialGroupId)
780 context.materialGroupId = options.materialGroupId
781 if (options.materialId)
782 context.materialId = options.materialId
783 if (options.source)
784 context.source = options.source
785
786 const includeBaseTitle = pubItems.every(item => isPublishTitleSupported(item.account.type))
787 const items: ChannelCreatePublishFlowItem[] = pubItems.map((item) => {
788 const flowItem: ChannelCreatePublishFlowItem = {
789 accountId: item.account.id,
790 platform: item.account.type as PlatType,
791 overrides: buildChannelPublishContent(item.params, {
792 includeTitle: isPublishTitleSupported(item.account.type),
793 }),
794 }
795 const option = buildChannelPublishOption(item)
796 if (option)
797 flowItem.option = option
798 return flowItem
799 })
800
801 return {
802 content: buildChannelPublishContent(firstItem.params, { includeTitle: includeBaseTitle }),
803 publishAt: options.publishAt,
804 context: Object.keys(context).length > 0 ? context : undefined,
805 items,
806 }
807 }
808
809 export function getPublishRecordIdFromFlow(flow?: ChannelPublishFlowVo, accountId?: string) {
810 if (!flow?.tasks.length)
811 return undefined
812
813 if (!accountId)
814 return flow.tasks[0]?.id
815
816 return flow.tasks.find(task => task.accountId === accountId)?.id || flow.tasks[0]?.id
817 }
818
818 lines TYPESCRIPT