gongxiangp2/lib/youyidian-notification-center.ts
2026-07-15 17:44:08 +08:00

93 lines
3.4 KiB
TypeScript

import crypto from 'crypto'
type NotificationCenterStatus = 'pending' | 'sending' | 'retrying' | 'sent' | 'unread' | 'read' | 'failed'
export type NotificationCenterDelivery = {
id: string
projectCode: string
status: NotificationCenterStatus
recipientUserId: string
title: string
processQueryKey?: string | null
lastErrorCode?: string | null
lastErrorMessage?: string | null
sentAt?: string | null
readAt?: string | null
raw?: unknown
}
const getConfig = () => ({
baseUrl: String(process.env.YYD_NOTIFICATION_CENTER_URL || '').trim().replace(/\/$/, ''),
projectCode: String(process.env.YYD_NOTIFICATION_CENTER_PROJECT_CODE || '').trim(),
secret: String(process.env.YYD_NOTIFICATION_CENTER_PROJECT_SECRET || '').trim(),
})
export const getNotificationCenterConfigStatus = () => {
const config = getConfig()
const missing: string[] = []
if (!config.baseUrl) missing.push('YYD_NOTIFICATION_CENTER_URL')
if (!config.projectCode) missing.push('YYD_NOTIFICATION_CENTER_PROJECT_CODE')
if (!config.secret) missing.push('YYD_NOTIFICATION_CENTER_PROJECT_SECRET')
return { ready: missing.length === 0, missing }
}
const sha256 = (value: string) => crypto.createHash('sha256').update(value).digest('hex')
const signedHeaders = (input: { method: string; path: string; body: string }) => {
const config = getConfig()
const timestamp = String(Math.floor(Date.now() / 1000))
const nonce = crypto.randomBytes(18).toString('base64url')
const canonical = [timestamp, nonce, input.method.toUpperCase(), input.path, sha256(input.body)].join('\n')
const signature = crypto.createHmac('sha256', config.secret).update(canonical).digest('hex')
return {
'Content-Type': 'application/json',
'x-yyd-project-code': config.projectCode,
'x-yyd-timestamp': timestamp,
'x-yyd-nonce': nonce,
'x-yyd-signature': signature,
}
}
const request = async (path: string, init: { method: 'GET' | 'POST'; body?: Record<string, unknown> }) => {
const config = getConfig()
const status = getNotificationCenterConfigStatus()
if (!status.ready) throw new Error(`由壹点通知中心配置不完整: ${status.missing.join(', ')}`)
const body = init.body ? JSON.stringify(init.body) : ''
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 8_000)
let response: Response
try {
response = await fetch(`${config.baseUrl}${path}`, {
method: init.method,
headers: signedHeaders({ method: init.method, path, body }),
body: body || undefined,
signal: controller.signal,
})
} finally {
clearTimeout(timeout)
}
const data = await response.json().catch(() => ({})) as Record<string, any>
if (!response.ok) {
throw new Error(`由壹点通知中心请求失败: ${data.error || response.status}`)
}
return data
}
export const submitYouyidianNotification = async (input: {
idempotencyKey: string
recipientUserId: string
title: string
content: string
actionTitle: string
actionUrl: string
}): Promise<NotificationCenterDelivery> => {
const data = await request('/v1/notifications', { method: 'POST', body: input })
return { ...(data as NotificationCenterDelivery), raw: data }
}
export const getYouyidianNotificationStatus = async (id: string): Promise<NotificationCenterDelivery> => {
const path = `/v1/notifications/${encodeURIComponent(id)}`
const data = await request(path, { method: 'GET' })
return { ...(data as NotificationCenterDelivery), raw: data }
}