557 lines
18 KiB
TypeScript
557 lines
18 KiB
TypeScript
type DingtalkWorkNoticeInput = {
|
|
userIds: string[]
|
|
title: string
|
|
text: string
|
|
actionUrl?: string | null
|
|
actionTitle?: string | null
|
|
}
|
|
|
|
type DingtalkWorkNoticeResult = {
|
|
skipped?: boolean
|
|
reason?: string
|
|
taskId?: string
|
|
requestId?: string
|
|
raw?: unknown
|
|
}
|
|
|
|
type DingtalkRobotOtoMessageInput = DingtalkWorkNoticeInput
|
|
|
|
type DingtalkRobotOtoMessageResult = {
|
|
skipped?: boolean
|
|
reason?: string
|
|
processQueryKey?: string
|
|
invalidUserIds: string[]
|
|
filteredUserIds: string[]
|
|
flowControlledUserIds: string[]
|
|
raw?: unknown
|
|
}
|
|
|
|
export type DingtalkRobotOtoReadResult = {
|
|
readUserIds: string[]
|
|
unreadUserIds: string[]
|
|
sendStatus: string
|
|
deliveryFailed: boolean
|
|
raw: unknown
|
|
}
|
|
|
|
export type DingtalkPerformanceSurveyNotificationChannel = 'work_notice' | 'robot_oto' | 'notification_center'
|
|
|
|
export type DingtalkWorkNoticeSendResult = {
|
|
readUserIds: string[]
|
|
unreadUserIds: string[]
|
|
failedUserIds: string[]
|
|
invalidUserIds: string[]
|
|
forbiddenUserIds: string[]
|
|
raw: unknown
|
|
}
|
|
|
|
type DingtalkAppConfig = {
|
|
appKey: string
|
|
appSecret: string
|
|
source: 'login' | 'workNotice'
|
|
}
|
|
|
|
const cachedAppTokens = new Map<string, { token: string; expiresAt: number }>()
|
|
|
|
function getDingtalkAppConfig(): DingtalkAppConfig {
|
|
const appKey = process.env.DINGTALK_CLIENT_ID || process.env.DINGTALK_APP_KEY
|
|
const appSecret = process.env.DINGTALK_CLIENT_SECRET || process.env.DINGTALK_APP_SECRET
|
|
|
|
if (!appKey || !appSecret) {
|
|
throw new Error('钉钉应用未配置 DINGTALK_CLIENT_ID/DINGTALK_CLIENT_SECRET')
|
|
}
|
|
|
|
return { appKey, appSecret, source: 'login' }
|
|
}
|
|
|
|
function getDingtalkWorkNoticeAppConfig(): DingtalkAppConfig {
|
|
const workNoticeAppKey =
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID || process.env.DINGTALK_WORK_NOTICE_APP_KEY
|
|
const workNoticeAppSecret =
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET || process.env.DINGTALK_WORK_NOTICE_APP_SECRET
|
|
|
|
if (workNoticeAppKey || workNoticeAppSecret) {
|
|
if (!workNoticeAppKey || !workNoticeAppSecret) {
|
|
throw new Error('钉钉工作通知应用未完整配置 DINGTALK_WORK_NOTICE_CLIENT_ID/DINGTALK_WORK_NOTICE_CLIENT_SECRET')
|
|
}
|
|
|
|
return {
|
|
appKey: workNoticeAppKey,
|
|
appSecret: workNoticeAppSecret,
|
|
source: 'workNotice',
|
|
}
|
|
}
|
|
|
|
return getDingtalkAppConfig()
|
|
}
|
|
|
|
function getDingtalkWorkNoticeAgentId() {
|
|
return process.env.DINGTALK_WORK_NOTICE_AGENT_ID || process.env.DINGTALK_AGENT_ID || ''
|
|
}
|
|
|
|
function getDingtalkRobotCode() {
|
|
return String(process.env.DINGTALK_ROBOT_CODE || '').trim()
|
|
}
|
|
|
|
function getDingtalkWorkNoticeCorpId() {
|
|
return (
|
|
process.env.DINGTALK_WORK_NOTICE_CORP_ID ||
|
|
process.env.DINGTALK_CORP_ID ||
|
|
process.env.DINGTALK_CORPID ||
|
|
process.env.NEXT_PUBLIC_DINGTALK_CORP_ID ||
|
|
''
|
|
).trim()
|
|
}
|
|
|
|
function buildDingtalkWorkNoticeActionUrl(actionUrl: string, agentId: number) {
|
|
const corpId = getDingtalkWorkNoticeCorpId()
|
|
if (!corpId.startsWith('ding')) {
|
|
return actionUrl
|
|
}
|
|
|
|
return [
|
|
'dingtalk://dingtalkclient/action/openapp',
|
|
`?corpid=${encodeURIComponent(corpId)}`,
|
|
'&container_type=work_platform',
|
|
`&app_id=${encodeURIComponent(`0_${agentId}`)}`,
|
|
'&redirect_type=jump',
|
|
`&redirect_url=${encodeURIComponent(actionUrl)}`,
|
|
].join('')
|
|
}
|
|
|
|
export function getDingtalkWorkNoticeConfigStatus() {
|
|
const workNoticeAppKey =
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID || process.env.DINGTALK_WORK_NOTICE_APP_KEY
|
|
const workNoticeAppSecret =
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET || process.env.DINGTALK_WORK_NOTICE_APP_SECRET
|
|
const loginAppKey = process.env.DINGTALK_CLIENT_ID || process.env.DINGTALK_APP_KEY
|
|
const loginAppSecret = process.env.DINGTALK_CLIENT_SECRET || process.env.DINGTALK_APP_SECRET
|
|
const agentId = getDingtalkWorkNoticeAgentId()
|
|
const numericAgentId = Number(agentId)
|
|
const workNoticeAppPartiallyConfigured = Boolean(workNoticeAppKey || workNoticeAppSecret)
|
|
const workNoticeAppConfigured = Boolean(workNoticeAppKey && workNoticeAppSecret)
|
|
const loginAppConfigured = Boolean(loginAppKey && loginAppSecret)
|
|
const appConfigured = workNoticeAppPartiallyConfigured
|
|
? workNoticeAppConfigured
|
|
: loginAppConfigured
|
|
const agentConfigured = Boolean(agentId && Number.isFinite(numericAgentId) && numericAgentId > 0)
|
|
const missing: string[] = []
|
|
|
|
if (!appConfigured) {
|
|
missing.push(
|
|
workNoticeAppPartiallyConfigured
|
|
? 'DINGTALK_WORK_NOTICE_CLIENT_ID/DINGTALK_WORK_NOTICE_CLIENT_SECRET'
|
|
: 'DINGTALK_WORK_NOTICE_CLIENT_ID/DINGTALK_WORK_NOTICE_CLIENT_SECRET 或 DINGTALK_CLIENT_ID/DINGTALK_CLIENT_SECRET',
|
|
)
|
|
}
|
|
if (!agentConfigured) {
|
|
missing.push('DINGTALK_WORK_NOTICE_AGENT_ID')
|
|
}
|
|
|
|
return {
|
|
ready: appConfigured && agentConfigured,
|
|
appConfigured,
|
|
agentConfigured,
|
|
appSource: workNoticeAppPartiallyConfigured ? 'workNotice' : loginAppConfigured ? 'login' : null,
|
|
agentSource: process.env.DINGTALK_WORK_NOTICE_AGENT_ID
|
|
? 'DINGTALK_WORK_NOTICE_AGENT_ID'
|
|
: process.env.DINGTALK_AGENT_ID
|
|
? 'DINGTALK_AGENT_ID'
|
|
: null,
|
|
missing,
|
|
}
|
|
}
|
|
|
|
export function getDingtalkRobotOtoConfigStatus() {
|
|
const robotCode = getDingtalkRobotCode()
|
|
const workNoticeAppKey = process.env.DINGTALK_WORK_NOTICE_CLIENT_ID ||
|
|
process.env.DINGTALK_WORK_NOTICE_APP_KEY
|
|
const workNoticeAppSecret = process.env.DINGTALK_WORK_NOTICE_CLIENT_SECRET ||
|
|
process.env.DINGTALK_WORK_NOTICE_APP_SECRET
|
|
const workNoticeAppPartiallyConfigured = Boolean(workNoticeAppKey || workNoticeAppSecret)
|
|
const workNoticeAppConfigured = Boolean(workNoticeAppKey && workNoticeAppSecret)
|
|
const loginAppConfigured = Boolean(
|
|
(process.env.DINGTALK_CLIENT_ID || process.env.DINGTALK_APP_KEY) &&
|
|
(process.env.DINGTALK_CLIENT_SECRET || process.env.DINGTALK_APP_SECRET),
|
|
)
|
|
const appConfigured = workNoticeAppPartiallyConfigured
|
|
? workNoticeAppConfigured
|
|
: loginAppConfigured
|
|
const missing: string[] = []
|
|
if (!robotCode) missing.push('DINGTALK_ROBOT_CODE')
|
|
if (!appConfigured) {
|
|
missing.push(
|
|
workNoticeAppPartiallyConfigured
|
|
? 'DINGTALK_WORK_NOTICE_CLIENT_ID/DINGTALK_WORK_NOTICE_CLIENT_SECRET'
|
|
: '钉钉应用 Client ID/Client Secret',
|
|
)
|
|
}
|
|
|
|
return {
|
|
ready: missing.length === 0,
|
|
robotCodeConfigured: Boolean(robotCode),
|
|
appConfigured,
|
|
appSource: workNoticeAppPartiallyConfigured ? 'workNotice' : loginAppConfigured ? 'login' : null,
|
|
missing,
|
|
}
|
|
}
|
|
|
|
export function getDingtalkPerformanceSurveyNotificationChannel(): DingtalkPerformanceSurveyNotificationChannel {
|
|
const value = String(
|
|
process.env.DINGTALK_PERFORMANCE_SURVEY_NOTIFICATION_CHANNEL || 'work_notice',
|
|
).trim().toLowerCase()
|
|
if (value === 'work_notice' || value === 'robot_oto' || value === 'notification_center') return value
|
|
throw new Error(
|
|
'DINGTALK_PERFORMANCE_SURVEY_NOTIFICATION_CHANNEL 仅支持 work_notice、robot_oto 或 notification_center',
|
|
)
|
|
}
|
|
|
|
export function getDingtalkRobotOtoTestUserIds() {
|
|
return Array.from(new Set(
|
|
String(process.env.DINGTALK_ROBOT_OTO_TEST_USER_IDS || '')
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter(Boolean),
|
|
))
|
|
}
|
|
|
|
export function getDingtalkPerformanceSurveyNotificationChannelForUser(
|
|
dingtalkUserId: string,
|
|
): DingtalkPerformanceSurveyNotificationChannel {
|
|
const channel = getDingtalkPerformanceSurveyNotificationChannel()
|
|
if (channel === 'work_notice') return channel
|
|
|
|
const testUserIds = channel === 'notification_center'
|
|
? Array.from(new Set(
|
|
String(process.env.YYD_NOTIFICATION_CENTER_TEST_USER_IDS || '')
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter(Boolean),
|
|
))
|
|
: getDingtalkRobotOtoTestUserIds()
|
|
if (testUserIds.length === 0) return channel
|
|
return testUserIds.includes(String(dingtalkUserId || '').trim()) ? channel : 'work_notice'
|
|
}
|
|
|
|
function truncateText(value: string, maxLength = 4800) {
|
|
const normalized = String(value || '').trim()
|
|
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized
|
|
}
|
|
|
|
export async function getDingtalkAppAccessToken() {
|
|
return getDingtalkAccessToken(getDingtalkAppConfig())
|
|
}
|
|
|
|
async function getDingtalkWorkNoticeAccessToken() {
|
|
return getDingtalkAccessToken(getDingtalkWorkNoticeAppConfig())
|
|
}
|
|
|
|
export function getDingtalkWorkNoticeClientId() {
|
|
return (
|
|
process.env.DINGTALK_WORK_NOTICE_CLIENT_ID ||
|
|
process.env.DINGTALK_WORK_NOTICE_APP_KEY ||
|
|
process.env.DINGTALK_CLIENT_ID ||
|
|
process.env.DINGTALK_APP_KEY ||
|
|
''
|
|
)
|
|
}
|
|
|
|
export async function getDingtalkWorkNoticeAppAccessToken() {
|
|
return getDingtalkWorkNoticeAccessToken()
|
|
}
|
|
|
|
async function getDingtalkAccessToken(config: DingtalkAppConfig) {
|
|
const cacheKey = `${config.source}:${config.appKey}:${config.appSecret.slice(-8)}`
|
|
const cachedAppToken = cachedAppTokens.get(cacheKey)
|
|
if (cachedAppToken && cachedAppToken.expiresAt > Date.now() + 60_000) {
|
|
return cachedAppToken.token
|
|
}
|
|
|
|
const response = await fetch(
|
|
`https://oapi.dingtalk.com/gettoken?appkey=${encodeURIComponent(config.appKey)}&appsecret=${encodeURIComponent(config.appSecret)}`,
|
|
)
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`获取钉钉应用访问令牌失败: HTTP ${response.status}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
if (data.errcode !== 0 || !data.access_token) {
|
|
throw new Error(`获取钉钉应用访问令牌失败: ${data.errmsg || '返回数据异常'}`)
|
|
}
|
|
|
|
const expiresIn = Number(data.expires_in || 7200)
|
|
const nextToken = String(data.access_token)
|
|
cachedAppTokens.set(cacheKey, {
|
|
token: nextToken,
|
|
expiresAt: Date.now() + Math.max(60, expiresIn - 120) * 1000,
|
|
})
|
|
|
|
return nextToken
|
|
}
|
|
|
|
export async function sendDingtalkWorkNotice(
|
|
input: DingtalkWorkNoticeInput,
|
|
): Promise<DingtalkWorkNoticeResult> {
|
|
const agentId = getDingtalkWorkNoticeAgentId()
|
|
if (!agentId) {
|
|
return {
|
|
skipped: true,
|
|
reason: '钉钉工作通知未配置 DINGTALK_WORK_NOTICE_AGENT_ID',
|
|
}
|
|
}
|
|
|
|
const numericAgentId = Number(agentId)
|
|
if (!Number.isFinite(numericAgentId) || numericAgentId <= 0) {
|
|
return {
|
|
skipped: true,
|
|
reason: '钉钉工作通知 DINGTALK_WORK_NOTICE_AGENT_ID 必须是正数',
|
|
}
|
|
}
|
|
|
|
const userIds = Array.from(
|
|
new Set(input.userIds.map((item) => String(item || '').trim()).filter(Boolean)),
|
|
)
|
|
|
|
if (userIds.length === 0) {
|
|
return {
|
|
skipped: true,
|
|
reason: '没有可投递的钉钉 userId',
|
|
}
|
|
}
|
|
|
|
const token = await getDingtalkWorkNoticeAccessToken()
|
|
const messageTitle = truncateText(input.title, 120)
|
|
const actionTitle = truncateText(input.actionTitle || '查看详情', 20)
|
|
const actionUrl = input.actionUrl
|
|
? buildDingtalkWorkNoticeActionUrl(input.actionUrl, numericAgentId)
|
|
: null
|
|
const msg = actionUrl
|
|
? {
|
|
msgtype: 'action_card',
|
|
action_card: {
|
|
title: messageTitle,
|
|
markdown: truncateText(input.text),
|
|
single_title: actionTitle,
|
|
single_url: actionUrl,
|
|
},
|
|
}
|
|
: {
|
|
msgtype: 'markdown',
|
|
markdown: {
|
|
title: messageTitle,
|
|
text: truncateText(input.text),
|
|
},
|
|
}
|
|
|
|
const response = await fetch(
|
|
`https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token=${encodeURIComponent(token)}`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
agent_id: numericAgentId,
|
|
userid_list: userIds.join(','),
|
|
msg,
|
|
}),
|
|
},
|
|
)
|
|
|
|
const data = await response.json().catch(async () => ({ raw: await response.text() }))
|
|
if (!response.ok) {
|
|
throw new Error(`钉钉工作通知发送失败: HTTP ${response.status}`)
|
|
}
|
|
|
|
if (data.errcode !== 0) {
|
|
throw new Error(`钉钉工作通知发送失败: ${data.errmsg || '未知错误'}`)
|
|
}
|
|
|
|
return {
|
|
taskId: data.task_id != null ? String(data.task_id) : undefined,
|
|
requestId: data.request_id != null ? String(data.request_id) : undefined,
|
|
raw: data,
|
|
}
|
|
}
|
|
|
|
export async function sendDingtalkRobotOtoMessage(
|
|
input: DingtalkRobotOtoMessageInput,
|
|
): Promise<DingtalkRobotOtoMessageResult> {
|
|
const robotCode = getDingtalkRobotCode()
|
|
if (!robotCode) {
|
|
throw new Error('钉钉机器人单聊未配置 DINGTALK_ROBOT_CODE')
|
|
}
|
|
|
|
const userIds = Array.from(
|
|
new Set(input.userIds.map((item) => String(item || '').trim()).filter(Boolean)),
|
|
)
|
|
if (userIds.length === 0) {
|
|
return {
|
|
skipped: true,
|
|
reason: '没有可投递的钉钉 userId',
|
|
invalidUserIds: [],
|
|
filteredUserIds: [],
|
|
flowControlledUserIds: [],
|
|
}
|
|
}
|
|
if (userIds.length > 20) {
|
|
throw new Error('钉钉机器人单聊每次最多发送给 20 人')
|
|
}
|
|
|
|
const token = await getDingtalkWorkNoticeAccessToken()
|
|
const messageTitle = truncateText(input.title, 120)
|
|
const actionTitle = truncateText(input.actionTitle || '查看详情', 20)
|
|
const actionUrl = input.actionUrl ? String(input.actionUrl).trim() : ''
|
|
const msgKey = actionUrl ? 'sampleActionCard' : 'sampleMarkdown'
|
|
const msgParam = actionUrl
|
|
? {
|
|
title: messageTitle,
|
|
text: truncateText(input.text),
|
|
singleTitle: actionTitle,
|
|
singleURL: actionUrl,
|
|
}
|
|
: {
|
|
title: messageTitle,
|
|
text: truncateText(input.text),
|
|
}
|
|
|
|
const response = await fetch('https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-acs-dingtalk-access-token': token,
|
|
},
|
|
body: JSON.stringify({
|
|
robotCode,
|
|
userIds,
|
|
msgKey,
|
|
msgParam: JSON.stringify(msgParam),
|
|
}),
|
|
})
|
|
const data = await response.json().catch(async () => ({ raw: await response.text() }))
|
|
if (!response.ok) {
|
|
throw new Error(`钉钉机器人单聊发送失败: HTTP ${response.status}`)
|
|
}
|
|
|
|
const stringList = (value: unknown) => Array.isArray(value)
|
|
? value.map((item) => String(item || '').trim()).filter(Boolean)
|
|
: []
|
|
const invalidUserIds = stringList(data.invalidStaffIdList)
|
|
const filteredUserIds = stringList(data.filteredStaffIdList)
|
|
const flowControlledUserIds = stringList(data.flowControlledStaffIdList)
|
|
const acceptedUserIds = userIds.filter(
|
|
(userId) => !invalidUserIds.includes(userId) &&
|
|
!filteredUserIds.includes(userId) &&
|
|
!flowControlledUserIds.includes(userId),
|
|
)
|
|
const processQueryKey = String(data.processQueryKey || '').trim()
|
|
|
|
if (acceptedUserIds.length === 0) {
|
|
const reason = invalidUserIds.length > 0
|
|
? `收件人钉钉 userId 无效: ${invalidUserIds.join(', ')}`
|
|
: filteredUserIds.length > 0
|
|
? `收件人被钉钉过滤: ${filteredUserIds.join(', ')}`
|
|
: `钉钉机器人单聊发送被限流: ${flowControlledUserIds.join(', ')}`
|
|
throw new Error(reason)
|
|
}
|
|
if (!processQueryKey) {
|
|
throw new Error('钉钉机器人单聊发送失败: 未返回 processQueryKey')
|
|
}
|
|
|
|
return {
|
|
processQueryKey,
|
|
invalidUserIds,
|
|
filteredUserIds,
|
|
flowControlledUserIds,
|
|
raw: data,
|
|
}
|
|
}
|
|
|
|
export async function getDingtalkWorkNoticeSendResult(
|
|
taskId: string,
|
|
): Promise<DingtalkWorkNoticeSendResult> {
|
|
const agentId = Number(getDingtalkWorkNoticeAgentId())
|
|
if (!Number.isFinite(agentId) || agentId <= 0) {
|
|
throw new Error('钉钉工作通知 DINGTALK_WORK_NOTICE_AGENT_ID 必须是正数')
|
|
}
|
|
|
|
const normalizedTaskId = String(taskId || '').trim()
|
|
if (!normalizedTaskId) throw new Error('缺少钉钉工作通知 taskId')
|
|
|
|
const token = await getDingtalkWorkNoticeAccessToken()
|
|
const response = await fetch(
|
|
`https://oapi.dingtalk.com/topapi/message/corpconversation/getsendresult?access_token=${encodeURIComponent(token)}`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
agent_id: agentId,
|
|
task_id: normalizedTaskId,
|
|
}),
|
|
},
|
|
)
|
|
const data = await response.json().catch(async () => ({ raw: await response.text() }))
|
|
if (!response.ok) {
|
|
throw new Error(`查询钉钉工作通知发送结果失败: HTTP ${response.status}`)
|
|
}
|
|
if (data.errcode !== 0) {
|
|
throw new Error(`查询钉钉工作通知发送结果失败: ${data.errmsg || '未知错误'}`)
|
|
}
|
|
|
|
const result = data.send_result && typeof data.send_result === 'object' ? data.send_result : {}
|
|
const stringList = (value: unknown) => Array.isArray(value)
|
|
? value.map((item) => String(item || '').trim()).filter(Boolean)
|
|
: []
|
|
|
|
return {
|
|
readUserIds: stringList(result.read_user_id_list),
|
|
unreadUserIds: stringList(result.unread_user_id_list),
|
|
failedUserIds: stringList(result.failed_user_id_list),
|
|
invalidUserIds: stringList(result.invalid_user_id_list),
|
|
forbiddenUserIds: stringList(result.forbidden_user_id_list),
|
|
raw: data,
|
|
}
|
|
}
|
|
|
|
export async function getDingtalkRobotOtoReadResult(
|
|
processQueryKey: string,
|
|
): Promise<DingtalkRobotOtoReadResult> {
|
|
const robotCode = getDingtalkRobotCode()
|
|
if (!robotCode) throw new Error('钉钉机器人单聊未配置 DINGTALK_ROBOT_CODE')
|
|
|
|
const normalizedQueryKey = String(processQueryKey || '').trim()
|
|
if (!normalizedQueryKey) throw new Error('缺少钉钉机器人单聊 processQueryKey')
|
|
|
|
const token = await getDingtalkWorkNoticeAccessToken()
|
|
const url = new URL('https://api.dingtalk.com/v1.0/robot/oToMessages/readStatus')
|
|
url.searchParams.set('robotCode', robotCode)
|
|
url.searchParams.set('processQueryKey', normalizedQueryKey)
|
|
const response = await fetch(url.toString(), {
|
|
method: 'GET',
|
|
headers: { 'x-acs-dingtalk-access-token': token },
|
|
})
|
|
const data = await response.json().catch(async () => ({ raw: await response.text() }))
|
|
if (!response.ok) {
|
|
throw new Error(`查询钉钉机器人单聊已读状态失败: HTTP ${response.status}`)
|
|
}
|
|
|
|
const readUserIds: string[] = []
|
|
const unreadUserIds: string[] = []
|
|
const rows = Array.isArray(data.messageReadInfoList) ? data.messageReadInfoList : []
|
|
for (const row of rows) {
|
|
const userId = String(row?.userId || '').trim()
|
|
const readStatus = String(row?.readStatus || '').trim().toLowerCase()
|
|
if (!userId) continue
|
|
if (readStatus === 'read') readUserIds.push(userId)
|
|
if (readStatus === 'unread') unreadUserIds.push(userId)
|
|
}
|
|
|
|
const sendStatus = String(data.sendStatus || '').trim().toLowerCase()
|
|
return {
|
|
readUserIds,
|
|
unreadUserIds,
|
|
sendStatus,
|
|
deliveryFailed: sendStatus.includes('fail'),
|
|
raw: data,
|
|
}
|
|
}
|