返回 AiToEarn
utils.ts
1 import type {
2 PlatformInfo,
3 PlatformInfoTuple,
4 PlatformMediaRules,
5 PlatformMetadataVo,
6 } from '@/api/channels/channel.types'
7 import { PlatformStatus, PublishContentMode } from '@/api/channels/channel.constants'
8 import { PlatType } from '@/app/config/platConfig'
9 import { PubType } from '@/app/config/publishConfig'
10 import { isChineseLanguage } from '@/app/i18n/languageConfig'
11 import { getStaticPlatformIcon } from '@/store/platformMetadata/staticIcons'
12
13 export const TASK_EXCLUDED_PLATFORMS = new Set<PlatType>([
14 PlatType.WxSph,
15 PlatType.WxGzh,
16 PlatType.Pinterest,
17 PlatType.LinkedIn,
18 ])
19
20 const COLLECT_UNSUPPORTED_PLATFORMS = new Set<PlatType>([
21 PlatType.Facebook,
22 PlatType.Instagram,
23 PlatType.Twitter,
24 ])
25
26 const VIEW_UNSUPPORTED_PLATFORMS = new Set<PlatType>([
27 PlatType.Xhs,
28 ])
29
30 const platformValues = new Set<string>(Object.values(PlatType))
31
32 const DEFAULT_PLATFORM_DISPLAY_NAMES: Record<PlatType, Partial<Record<string, string>>> = {
33 [PlatType.Tiktok]: {
34 'en': 'TikTok',
35 'en-US': 'TikTok',
36 'zh-CN': 'TikTok',
37 },
38 [PlatType.Douyin]: {
39 'en': 'Douyin',
40 'en-US': 'Douyin',
41 'zh-CN': '抖音',
42 },
43 [PlatType.Xhs]: {
44 'en': 'RedNote',
45 'en-US': 'RedNote',
46 'zh-CN': '小红书',
47 },
48 [PlatType.WxSph]: {
49 'en': 'WeChat Channels',
50 'en-US': 'WeChat Channels',
51 'zh-CN': '微信视频号',
52 },
53 [PlatType.KWAI]: {
54 'en': 'Kwai',
55 'en-US': 'Kwai',
56 'zh-CN': '快手',
57 },
58 [PlatType.YouTube]: {
59 'en': 'YouTube',
60 'en-US': 'YouTube',
61 'zh-CN': 'YouTube',
62 },
63 [PlatType.BILIBILI]: {
64 'en': 'Bilibili',
65 'en-US': 'Bilibili',
66 'zh-CN': '哔哩哔哩',
67 },
68 [PlatType.Twitter]: {
69 'en': 'Twitter(X)',
70 'en-US': 'Twitter(X)',
71 'zh-CN': 'Twitter(X)',
72 },
73 [PlatType.WxGzh]: {
74 'en': 'WeChat Official Account',
75 'en-US': 'WeChat Official Account',
76 'zh-CN': '微信公众号',
77 },
78 [PlatType.Facebook]: {
79 'en': 'Facebook',
80 'en-US': 'Facebook',
81 'zh-CN': 'Facebook',
82 },
83 [PlatType.Instagram]: {
84 'en': 'Instagram',
85 'en-US': 'Instagram',
86 'zh-CN': 'Instagram',
87 },
88 [PlatType.Threads]: {
89 'en': 'Threads',
90 'en-US': 'Threads',
91 'zh-CN': 'Threads',
92 },
93 [PlatType.Pinterest]: {
94 'en': 'Pinterest',
95 'en-US': 'Pinterest',
96 'zh-CN': 'Pinterest',
97 },
98 [PlatType.LinkedIn]: {
99 'en': 'LinkedIn',
100 'en-US': 'LinkedIn',
101 'zh-CN': 'LinkedIn',
102 },
103 }
104
105 const CHINESE_UNTRANSLATED_NAME_ALIASES: Partial<Record<PlatType, Set<string>>> = {
106 [PlatType.Douyin]: new Set(['douyin']),
107 [PlatType.Xhs]: new Set(['rednote', 'xiaohongshu', 'xhs']),
108 [PlatType.WxSph]: new Set(['wechat channels', 'wechat channel', 'wxsph']),
109 [PlatType.KWAI]: new Set(['kwai']),
110 [PlatType.BILIBILI]: new Set(['bilibili']),
111 [PlatType.WxGzh]: new Set(['wechat official account', 'wechat official accounts', 'wxgzh']),
112 }
113
114 function getNumberFromRules(rules: PlatformMediaRules, key: string) {
115 const value = rules[key]
116 return typeof value === 'number' && Number.isFinite(value) ? value : undefined
117 }
118
119 function getOptionalNumber(value: number | undefined) {
120 return typeof value === 'number' && Number.isFinite(value) ? value : undefined
121 }
122
123 export function isPlatType(value: string): value is PlatType {
124 return platformValues.has(value)
125 }
126
127 export function isTaskPlatformSupported(platType: PlatType) {
128 return !TASK_EXCLUDED_PLATFORMS.has(platType)
129 }
130
131 export function isPlatCollectSupported(platType: PlatType) {
132 return !COLLECT_UNSUPPORTED_PLATFORMS.has(platType)
133 }
134
135 export function isPlatViewSupported(platType: PlatType) {
136 return !VIEW_UNSUPPORTED_PLATFORMS.has(platType)
137 }
138
139 export function isPlatformAvailable<T extends Pick<PlatformInfo, 'status'>>(item?: T | null): item is T {
140 return item?.status === PlatformStatus.Available
141 }
142
143 export function isPlatformComingSoon<T extends Pick<PlatformInfo, 'status'>>(item?: T | null): item is T {
144 return item?.status === PlatformStatus.ComingSoon
145 }
146
147 export function isPlatformRegionLimited<T extends Pick<PlatformInfo, 'status'>>(item?: T | null): item is T {
148 return item?.status === PlatformStatus.Unavailable
149 }
150
151 export function isPlatformVisible<T extends Pick<PlatformInfo, 'status'>>(item?: T | null): item is T {
152 return !!item
153 }
154
155 const CHANNEL_PLATFORM_STATUS_ORDER: Record<PlatformStatus, number> = {
156 [PlatformStatus.Available]: 0,
157 [PlatformStatus.Unavailable]: 1,
158 [PlatformStatus.ComingSoon]: 2,
159 }
160
161 function getChannelPlatformStatusOrder(item: Pick<PlatformInfo, 'status'>) {
162 return CHANNEL_PLATFORM_STATUS_ORDER[item.status]
163 }
164
165 function compareChannelPlatformStatus(left: PlatformInfo, right: PlatformInfo) {
166 return getChannelPlatformStatusOrder(left) - getChannelPlatformStatusOrder(right)
167 }
168
169 export function getPlatformDisplayName(item: PlatformMetadataVo, lng: string) {
170 if (isChineseLanguage(lng)) {
171 const displayName = getDisplayNameByLocale(item, ['zh-CN', 'zh', lng])
172 const fallbackName = DEFAULT_PLATFORM_DISPLAY_NAMES[item.platform]?.['zh-CN']
173
174 if (!displayName)
175 return fallbackName ?? item.displayName['en-US'] ?? item.platform
176 if (fallbackName && isUntranslatedChinesePlatformName(item.platform, displayName))
177 return fallbackName
178
179 return displayName
180 }
181
182 return getDisplayNameByLocale(item, getLocaleCandidates(lng))
183 ?? getDefaultPlatformDisplayName(item.platform, lng)
184 ?? item.platform
185 }
186
187 function getDisplayNameByLocale(item: PlatformMetadataVo, locales: string[]) {
188 for (const locale of locales) {
189 const value = item.displayName[locale]
190 if (value)
191 return value
192 }
193
194 return undefined
195 }
196
197 function getLocaleCandidates(lng: string) {
198 const normalizedLng = lng.replace('_', '-')
199 const baseLng = normalizedLng.split('-')[0]
200 const candidates = [lng, normalizedLng, baseLng]
201
202 if (baseLng === 'en')
203 candidates.push('en-US')
204 candidates.push('en-US', 'zh-CN')
205
206 return Array.from(new Set(candidates))
207 }
208
209 function getDefaultPlatformDisplayName(platform: PlatType, lng: string) {
210 const defaultNames = DEFAULT_PLATFORM_DISPLAY_NAMES[platform]
211 if (!defaultNames)
212 return undefined
213
214 const candidates = isChineseLanguage(lng) ? ['zh-CN'] : getLocaleCandidates(lng)
215 for (const locale of candidates) {
216 const value = defaultNames[locale]
217 if (value)
218 return value
219 }
220
221 return undefined
222 }
223
224 function getLocalizedPlatformText(values: PlatformMetadataVo['authInstructions'], lng: string) {
225 if (!values)
226 return undefined
227
228 for (const locale of getLocaleCandidates(lng)) {
229 const value = values[locale]
230 if (value)
231 return value
232 }
233
234 return undefined
235 }
236
237 function isUntranslatedChinesePlatformName(platform: PlatType, displayName: string) {
238 const normalizedName = displayName.trim().toLowerCase()
239 return CHINESE_UNTRANSLATED_NAME_ALIASES[platform]?.has(normalizedName) === true
240 }
241
242 function getPubTypeFromContentMode(mode: string) {
243 switch (mode) {
244 case PubType.VIDEO:
245 return PubType.VIDEO
246 case PubType.ImageText:
247 case PublishContentMode.ImageText:
248 return PubType.ImageText
249 case PubType.Article:
250 case PublishContentMode.Text:
251 return PubType.Article
252 default:
253 return undefined
254 }
255 }
256
257 function derivePubTypes(item: PlatformMetadataVo) {
258 const pubTypes = new Set<PubType>()
259 const modes = item.contentLimits.modes
260
261 if (item.capabilities.publish.supported === false)
262 return pubTypes
263
264 if (!modes) {
265 pubTypes.add(PubType.VIDEO)
266 pubTypes.add(PubType.ImageText)
267 pubTypes.add(PubType.Article)
268 return pubTypes
269 }
270
271 for (const mode of modes) {
272 const pubType = getPubTypeFromContentMode(mode)
273 if (pubType)
274 pubTypes.add(pubType)
275 }
276
277 return pubTypes
278 }
279
280 export function normalizePlatformMetadata(item: PlatformMetadataVo, lng: string): PlatformInfo {
281 const maxImages = item.contentLimits.maxImages ?? getNumberFromRules(item.mediaRules, 'maxImages')
282 const titleMax = getOptionalNumber(item.contentLimits.maxTitleLength)
283 const topicMax = item.topic.supported ? getOptionalNumber(item.topic.maxCount) : 0
284 const topicMaxTotalLength = getOptionalNumber(item.topic.maxTotalLength)
285 ?? getOptionalNumber(item.contentLimits.maxTotalTextLength)
286
287 return {
288 type: item.platform,
289 platform: item.platform,
290 status: item.status,
291 name: getPlatformDisplayName(item, lng),
292 icon: item.logoUrl || getStaticPlatformIcon(item.platform) || '',
293 logoUrl: item.logoUrl || getStaticPlatformIcon(item.platform) || '',
294 authType: item.authType,
295 authInstruction: getLocalizedPlatformText(item.authInstructions, lng),
296 editor: item.editor,
297 capabilities: item.capabilities,
298 contentLimits: item.contentLimits,
299 mediaRules: item.mediaRules,
300 topic: item.topic,
301 optionSchema: item.optionSchema,
302 defaultOption: item.defaultOption,
303 commonPubParamsConfig: {
304 titleMax,
305 topicMax,
306 topicMaxTotalLength,
307 desMax: item.contentLimits.maxBodyLength ?? 0,
308 imagesMax: maxImages,
309 },
310 pubTypes: derivePubTypes(item),
311 }
312 }
313
314 export function normalizePlatformMetadataList(items: PlatformMetadataVo[], lng: string) {
315 const list = items.map(item => normalizePlatformMetadata(item, lng))
316 const map = new Map<PlatType, PlatformInfo>()
317 list.forEach((item) => {
318 map.set(item.type, item)
319 })
320
321 return { list, map }
322 }
323
324 export function platformInfoListToTuples(list: PlatformInfo[]): PlatformInfoTuple[] {
325 return list.map(item => [item.type, item])
326 }
327
328 export function getEnabledPlatformInfos(list: PlatformInfo[]) {
329 return list.filter(isPlatformAvailable)
330 }
331
332 export function getTaskPlatformInfos(list: PlatformInfo[]) {
333 return list.filter(item => isPlatformAvailable(item) && isTaskPlatformSupported(item.type))
334 }
335
336 export function getPublishPlatformInfos(list: PlatformInfo[]) {
337 return list.filter(item => isPlatformAvailable(item) && item.capabilities.publish.supported !== false && item.pubTypes.size > 0)
338 }
339
340 export function getChannelPlatformInfos(list: PlatformInfo[]) {
341 return list
342 .filter(isPlatformVisible)
343 .map((item, index) => ({ item, index }))
344 .sort((left, right) => compareChannelPlatformStatus(left.item, right.item) || left.index - right.index)
345 .map(({ item }) => item)
346 }
347
347 lines TYPESCRIPT