mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-10-26 01:54:43 +08:00
196 lines
7.1 KiB
TypeScript
196 lines
7.1 KiB
TypeScript
import * as fs from 'fs'
|
||
import * as path from 'path'
|
||
import { chatService } from './chatService'
|
||
|
||
interface ContactExportOptions {
|
||
format: 'json' | 'csv' | 'vcf'
|
||
exportAvatars: boolean
|
||
contactTypes: {
|
||
friends: boolean
|
||
groups: boolean
|
||
officials: boolean
|
||
blocked?: boolean
|
||
}
|
||
selectedUsernames?: string[]
|
||
}
|
||
|
||
/**
|
||
* 联系人导出服务
|
||
*/
|
||
class ContactExportService {
|
||
/**
|
||
* 导出联系人
|
||
*/
|
||
async exportContacts(
|
||
outputDir: string,
|
||
options: ContactExportOptions
|
||
): Promise<{ success: boolean; successCount?: number; error?: string }> {
|
||
try {
|
||
// 获取所有联系人
|
||
const contactsResult = await chatService.getContacts()
|
||
if (!contactsResult.success || !contactsResult.contacts) {
|
||
return { success: false, error: contactsResult.error || '获取联系人失败' }
|
||
}
|
||
|
||
let contacts = contactsResult.contacts
|
||
|
||
// 根据类型过滤
|
||
contacts = contacts.filter(c => {
|
||
if (c.type === 'friend' && !options.contactTypes.friends) return false
|
||
if (c.type === 'group' && !options.contactTypes.groups) return false
|
||
if (c.type === 'official' && !options.contactTypes.officials) return false
|
||
if (c.type === 'blocked' && !options.contactTypes.blocked) return false
|
||
return true
|
||
})
|
||
|
||
if (Array.isArray(options.selectedUsernames) && options.selectedUsernames.length > 0) {
|
||
const selectedSet = new Set(options.selectedUsernames)
|
||
contacts = contacts.filter(c => selectedSet.has(c.username))
|
||
}
|
||
|
||
if (contacts.length === 0) {
|
||
return { success: false, error: '没有符合条件的联系人' }
|
||
}
|
||
|
||
// 确保输出目录存在
|
||
if (!fs.existsSync(outputDir)) {
|
||
fs.mkdirSync(outputDir, { recursive: true })
|
||
}
|
||
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
||
let outputPath: string
|
||
|
||
switch (options.format) {
|
||
case 'json':
|
||
outputPath = path.join(outputDir, `contacts_${timestamp}.json`)
|
||
await this.exportToJSON(contacts, outputPath)
|
||
break
|
||
case 'csv':
|
||
outputPath = path.join(outputDir, `contacts_${timestamp}.csv`)
|
||
await this.exportToCSV(contacts, outputPath)
|
||
break
|
||
case 'vcf':
|
||
outputPath = path.join(outputDir, `contacts_${timestamp}.vcf`)
|
||
await this.exportToVCF(contacts, outputPath)
|
||
break
|
||
default:
|
||
return { success: false, error: '不支持的导出格式' }
|
||
}
|
||
|
||
return { success: true, successCount: contacts.length }
|
||
} catch (e) {
|
||
return { success: false, error: String(e) }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 导出为JSON格式
|
||
*/
|
||
private async exportToJSON(contacts: any[], outputPath: string): Promise<void> {
|
||
const data = {
|
||
exportedAt: new Date().toISOString(),
|
||
count: contacts.length,
|
||
contacts: contacts.map(c => ({
|
||
username: c.username,
|
||
displayName: c.displayName,
|
||
remark: c.remark,
|
||
nickname: c.nickname,
|
||
alias: c.alias,
|
||
labels: Array.isArray(c.labels) ? c.labels : [],
|
||
description: c.description,
|
||
signature: c.detailDescription,
|
||
detailDescription: c.detailDescription,
|
||
region: c.region,
|
||
type: c.type,
|
||
officialAccountKind: c.officialAccountKind,
|
||
officialAccountType: c.officialAccountType,
|
||
typeLabel: this.getTypeLabel(c)
|
||
}))
|
||
}
|
||
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf-8')
|
||
}
|
||
|
||
/**
|
||
* 导出为CSV格式
|
||
*/
|
||
private async exportToCSV(contacts: any[], outputPath: string): Promise<void> {
|
||
const headers = ['用户名', '显示名称', '备注', '昵称', '账号号', '标签', '描述', '个性签名', '地区', '类型']
|
||
const rows = contacts.map(c => [
|
||
c.username || '',
|
||
c.displayName || '',
|
||
c.remark || '',
|
||
c.nickname || '',
|
||
c.alias || '',
|
||
Array.isArray(c.labels) ? c.labels.join(' | ') : '',
|
||
c.description || '',
|
||
c.detailDescription || '',
|
||
c.region || '',
|
||
this.getTypeLabel(c)
|
||
])
|
||
const escapeCell = (value: unknown) => `"${String(value ?? '').replace(/"/g, '""')}"`
|
||
|
||
const csvContent = [
|
||
headers.join(','),
|
||
...rows.map(row => row.map(escapeCell).join(','))
|
||
].join('\n')
|
||
|
||
fs.writeFileSync(outputPath, '\uFEFF' + csvContent, 'utf-8') // 添加BOM以支持Excel
|
||
}
|
||
|
||
/**
|
||
* 导出为VCF格式(vCard)
|
||
*/
|
||
private async exportToVCF(contacts: any[], outputPath: string): Promise<void> {
|
||
const vcards = contacts
|
||
.filter(c => c.type === 'friend') // VCF通常只用于个人联系人
|
||
.map(c => {
|
||
const lines = ['BEGIN:VCARD', 'VERSION:3.0']
|
||
|
||
// 全名
|
||
lines.push(`FN:${c.displayName || c.username}`)
|
||
|
||
// 昵称
|
||
if (c.nickname) {
|
||
lines.push(`NICKNAME:${c.nickname}`)
|
||
}
|
||
|
||
const noteParts = [
|
||
c.remark ? String(c.remark) : '',
|
||
Array.isArray(c.labels) && c.labels.length > 0 ? `标签: ${c.labels.join(', ')}` : '',
|
||
c.description ? `描述: ${c.description}` : '',
|
||
c.detailDescription ? `个性签名: ${c.detailDescription}` : '',
|
||
c.region ? `地区: ${c.region}` : ''
|
||
].filter(Boolean)
|
||
if (noteParts.length > 0) {
|
||
lines.push(`NOTE:${noteParts.join('\\n')}`)
|
||
}
|
||
|
||
// 账号ID
|
||
lines.push(`X-ACCOUNT-ID:${c.username}`)
|
||
|
||
lines.push('END:VCARD')
|
||
return lines.join('\r\n')
|
||
})
|
||
|
||
fs.writeFileSync(outputPath, vcards.join('\r\n\r\n'), 'utf-8')
|
||
}
|
||
|
||
private getTypeLabel(contactOrType: any): string {
|
||
const type = typeof contactOrType === 'string' ? contactOrType : String(contactOrType?.type || '')
|
||
if (type === 'official' && typeof contactOrType !== 'string') {
|
||
if (contactOrType.officialAccountKind === 'service') return '服务号'
|
||
if (contactOrType.officialAccountKind === 'enterprise') return '企业号'
|
||
return '公众号'
|
||
}
|
||
switch (type) {
|
||
case 'friend': return '好友'
|
||
case 'group': return '群聊'
|
||
case 'official': return '公众号'
|
||
case 'blocked': return '黑名单'
|
||
default: return '其他'
|
||
}
|
||
}
|
||
}
|
||
|
||
export const contactExportService = new ContactExportService()
|