全新出发
This commit is contained in:
1 parent
b06c117b1e
commit
fa4edebed6
463 files changed
+181610
-125
No files matched your search
@@ -0,0 +1,95 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { normalizeTimestampSeconds } from './timestamp'
|
||||
import { ExportOptions } from '../types'
|
||||
|
||||
export function sanitizeExportFileNamePart(value: string): string {
|
||||
return String(value || '')
|
||||
.replace(/[<>:"\/\\|?*]/g, '_')
|
||||
.replace(/\.+$/, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function resolveFileAttachmentExtensionDir(msg: any, fileName: string): string {
|
||||
const rawExt = String(msg?.fileExt || '').trim() || path.extname(String(fileName || ''))
|
||||
const normalizedExt = rawExt.replace(/^\.+/, '').trim().toLowerCase()
|
||||
const safeExt = sanitizeExportFileNamePart(normalizedExt).replace(/\s+/g, '_')
|
||||
return safeExt || 'no-extension'
|
||||
}
|
||||
|
||||
export function normalizeFileNamingMode(value: unknown): 'classic' | 'date-range' {
|
||||
return String(value || '').trim().toLowerCase() === 'date-range' ? 'date-range' : 'classic'
|
||||
}
|
||||
|
||||
export function formatDateTokenBySeconds(seconds?: number): string | null {
|
||||
const normalizedSeconds = normalizeTimestampSeconds(seconds)
|
||||
if (normalizedSeconds <= 0) return null
|
||||
const date = new Date(normalizedSeconds * 1000)
|
||||
if (Number.isNaN(date.getTime())) return null
|
||||
const y = date.getFullYear()
|
||||
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const d = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${y}${m}${d}`
|
||||
}
|
||||
|
||||
export function buildDateRangeFileNamePart(dateRange?: { start: number; end: number } | null): string {
|
||||
const start = formatDateTokenBySeconds(dateRange?.start)
|
||||
const end = formatDateTokenBySeconds(dateRange?.end)
|
||||
if (start && end) {
|
||||
if (start === end) return start
|
||||
return start < end ? `${start}-${end}` : `${end}-${start}`
|
||||
}
|
||||
if (start) return `${start}-至今`
|
||||
if (end) return `截至-${end}`
|
||||
return '全部时间'
|
||||
}
|
||||
|
||||
export function buildSessionExportBaseName(
|
||||
sessionId: string,
|
||||
displayName: string,
|
||||
options: ExportOptions
|
||||
): string {
|
||||
const baseName = sanitizeExportFileNamePart(displayName || sessionId) || sanitizeExportFileNamePart(sessionId) || 'session'
|
||||
const suffix = sanitizeExportFileNamePart(options.fileNameSuffix || '')
|
||||
const namingMode = normalizeFileNamingMode(options.fileNamingMode)
|
||||
const parts = [baseName]
|
||||
if (suffix) parts.push(suffix)
|
||||
if (namingMode === 'date-range') {
|
||||
parts.push(buildDateRangeFileNamePart(options.dateRange))
|
||||
}
|
||||
return sanitizeExportFileNamePart(parts.join('_')) || 'session'
|
||||
}
|
||||
|
||||
async function pathExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(p)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function reserveUniqueOutputPath(preferredPath: string, reservedPaths: Set<string>): Promise<string> {
|
||||
const dir = path.dirname(preferredPath)
|
||||
const ext = path.extname(preferredPath)
|
||||
const base = path.basename(preferredPath, ext)
|
||||
|
||||
for (let attempt = 0; attempt < 10000; attempt += 1) {
|
||||
const candidate = attempt === 0
|
||||
? preferredPath
|
||||
: path.join(dir, `${base}_${attempt + 1}${ext}`)
|
||||
|
||||
if (reservedPaths.has(candidate)) continue
|
||||
|
||||
const exists = await pathExists(candidate)
|
||||
if (reservedPaths.has(candidate)) continue
|
||||
if (exists) continue
|
||||
|
||||
reservedPaths.add(candidate)
|
||||
return candidate
|
||||
}
|
||||
|
||||
const fallback = path.join(dir, `${base}_${Date.now()}${ext}`)
|
||||
reservedPaths.add(fallback)
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export function escapeHtml(value: string): string {
|
||||
if (!value) return ''
|
||||
return String(value).replace(/[&<>"']/g, c => {
|
||||
switch (c) {
|
||||
case '&': return '&'
|
||||
case '<': return '<'
|
||||
case '>': return '>'
|
||||
case '"': return '"'
|
||||
case "'": return '''
|
||||
default: return c
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function escapeAttribute(value: string): string {
|
||||
if (!value) return ''
|
||||
return String(value).replace(/[&<>"'`]/g, c => {
|
||||
switch (c) {
|
||||
case '&': return '&'
|
||||
case '<': return '<'
|
||||
case '>': return '>'
|
||||
case '"': return '"'
|
||||
case "'": return '''
|
||||
case '`': return '`'
|
||||
default: return c
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function renderMultilineText(value: string): string {
|
||||
return escapeHtml(value).replace(/\r?\n/g, '<br />')
|
||||
}
|
||||
|
||||
export function decodeHtmlEntities(value: string): string {
|
||||
if (!value) return ''
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/`/g, '`')
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 并发控制:限制同时执行的 Promise 数量
|
||||
*/
|
||||
export async function parallelLimit<T, R>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
fn: (item: T, index: number) => Promise<R>
|
||||
): Promise<R[]> {
|
||||
const results: R[] = new Array(items.length)
|
||||
let currentIndex = 0
|
||||
let hasError = false
|
||||
let globalError: any = null
|
||||
|
||||
const worker = async () => {
|
||||
while (currentIndex < items.length && !hasError) {
|
||||
const index = currentIndex++
|
||||
try {
|
||||
results[index] = await fn(items[index], index)
|
||||
} catch (err) {
|
||||
hasError = true
|
||||
globalError = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker())
|
||||
await Promise.all(workers)
|
||||
|
||||
if (hasError) throw globalError
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
export function normalizeTimestampSeconds(value: unknown): number {
|
||||
const raw = Number(value)
|
||||
if (!Number.isFinite(raw) || raw <= 0) return 0
|
||||
let normalized = Math.floor(raw)
|
||||
// 兼容毫秒/微秒/纳秒时间戳输入,统一降到秒级。
|
||||
while (normalized > 10000000000) {
|
||||
normalized = Math.floor(normalized / 1000)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function formatTimestamp(timestamp: number): string {
|
||||
const date = new Date(timestamp * 1000)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
export function formatIsoTimestamp(timestamp: number): string {
|
||||
return new Date(timestamp * 1000).toISOString()
|
||||
}
|
||||
|
||||
export function parseCompactDateTimeDigitsToSeconds(digits: string): number {
|
||||
if (digits.length < 14) return 0
|
||||
const year = parseInt(digits.substring(0, 4), 10)
|
||||
const month = parseInt(digits.substring(4, 6), 10) - 1
|
||||
const day = parseInt(digits.substring(6, 8), 10)
|
||||
const hours = parseInt(digits.substring(8, 10), 10)
|
||||
const minutes = parseInt(digits.substring(10, 12), 10)
|
||||
const seconds = parseInt(digits.substring(12, 14), 10)
|
||||
const date = new Date(year, month, day, hours, minutes, seconds)
|
||||
if (isNaN(date.getTime())) return 0
|
||||
return Math.floor(date.getTime() / 1000)
|
||||
}
|
||||
|
||||
export function parseDateTimeTextToSeconds(text: string): number {
|
||||
if (!text) return 0
|
||||
const digits = text.replace(/\D/g, '')
|
||||
return parseCompactDateTimeDigitsToSeconds(digits)
|
||||
}
|
||||
|
||||
export function normalizeExportDateRange(dateRange?: { start: number; end: number } | null): { start: number; end: number } | null {
|
||||
if (!dateRange) return null
|
||||
let start = normalizeTimestampSeconds(dateRange.start)
|
||||
let end = normalizeTimestampSeconds(dateRange.end)
|
||||
if (start > 0 && end > 0 && start > end) {
|
||||
const tmp = start
|
||||
start = end
|
||||
end = tmp
|
||||
}
|
||||
if (start <= 0 && end <= 0) return null
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
export function normalizeRowTimestampSeconds(value: unknown): number {
|
||||
return normalizeTimestampSeconds(value)
|
||||
}
|
||||
|
||||
export function getTimestampSecondsFromRow(row: any): number {
|
||||
if (!row) return 0
|
||||
const raw = row.create_time ?? row.createTime ?? row.CreateTime ?? row.msgCreateTime ?? row.msg_create_time ?? row.timestamp ?? row.Timestamp
|
||||
if (raw !== undefined && raw !== null) {
|
||||
return normalizeTimestampSeconds(raw)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in new issue
Block a user