/** * HTTP API 鏈嶅姟 * 鎻愪緵 ChatLab 鏍囧噯鍖栨牸寮忕殑娑堟伅鏌ヨ API */ import * as http from 'http' import * as fs from 'fs' import * as path from 'path' import { URL } from 'url' import { chatService, Message } from './chatService' import { wcdbService } from './wcdbService' import { ConfigService } from './config' import { videoService } from './videoService' // ChatLab 鏍煎紡瀹氫箟 interface ChatLabHeader { version: string exportedAt: number generator: string description?: string } interface ChatLabMeta { name: string platform: string type: 'group' | 'private' groupId?: string groupAvatar?: string ownerId?: string } interface ChatLabMember { platformId: string accountName: string groupNickname?: string aliases?: string[] avatar?: string } interface ChatLabMessage { sender: string accountName: string groupNickname?: string timestamp: number type: number content: string | null platformMessageId?: string replyToMessageId?: string mediaPath?: string } interface ChatLabData { chatlab: ChatLabHeader meta: ChatLabMeta members: ChatLabMember[] messages: ChatLabMessage[] } interface ApiMediaOptions { enabled: boolean exportImages: boolean exportVoices: boolean exportVideos: boolean exportEmojis: boolean } type MediaKind = 'image' | 'voice' | 'video' | 'emoji' interface ApiExportedMedia { kind: MediaKind fileName: string fullPath: string } // ChatLab 娑堟伅绫诲瀷鏄犲皠 const ChatLabType = { TEXT: 0, IMAGE: 1, VOICE: 2, VIDEO: 3, FILE: 4, EMOJI: 5, LINK: 7, LOCATION: 8, RED_PACKET: 20, TRANSFER: 21, POKE: 22, CALL: 23, SHARE: 24, REPLY: 25, FORWARD: 26, CONTACT: 27, SYSTEM: 80, RECALL: 81, OTHER: 99 } as const class HttpService { private server: http.Server | null = null private configService: ConfigService private port: number = 5031 private running: boolean = false private connections: Set = new Set() constructor() { this.configService = ConfigService.getInstance() } /** * 鍚姩 HTTP 鏈嶅姟 */ async start(port: number = 5031): Promise<{ success: boolean; port?: number; error?: string }> { if (this.running && this.server) { return { success: true, port: this.port } } this.port = port return new Promise((resolve) => { this.server = http.createServer((req, res) => this.handleRequest(req, res)) // 璺熻釜鎵€鏈夎繛鎺ワ紝浠ヤ究鍏抽棴鏃惰兘寮哄埗鏂紑 this.server.on('connection', (socket) => { this.connections.add(socket) socket.on('close', () => { this.connections.delete(socket) }) }) this.server.on('error', (err: NodeJS.ErrnoException) => { if (err.code === 'EADDRINUSE') { console.error(`[HttpService] Port ${this.port} is already in use`) resolve({ success: false, error: `Port ${this.port} is already in use` }) } else { console.error('[HttpService] Server error:', err) resolve({ success: false, error: err.message }) } }) this.server.listen(this.port, '127.0.0.1', () => { this.running = true console.log(`[HttpService] HTTP API server started on http://127.0.0.1:${this.port}`) resolve({ success: true, port: this.port }) }) }) } /** * 鍋滄 HTTP 鏈嶅姟 */ async stop(): Promise { return new Promise((resolve) => { if (this.server) { // 寮哄埗鍏抽棴鎵€鏈夋椿鍔ㄨ繛鎺? for (const socket of this.connections) { socket.destroy() } this.connections.clear() this.server.close(() => { this.running = false this.server = null console.log('[HttpService] HTTP API server stopped') resolve() }) } else { this.running = false resolve() } }) } /** * 妫€鏌ユ湇鍔℃槸鍚﹁繍琛? */ isRunning(): boolean { return this.running } /** * 鑾峰彇褰撳墠绔彛 */ getPort(): number { return this.port } getDefaultMediaExportPath(): string { return this.getApiMediaExportPath() } /** * 澶勭悊 HTTP 璇锋眰 */ private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { // 璁剧疆 CORS 澶? res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type') if (req.method === 'OPTIONS') { res.writeHead(204) res.end() return } const url = new URL(req.url || '/', `http://127.0.0.1:${this.port}`) const pathname = url.pathname try { // 璺敱澶勭悊 if (pathname === '/health' || pathname === '/api/v1/health') { this.sendJson(res, { status: 'ok' }) } else if (pathname === '/api/v1/messages') { await this.handleMessages(url, res) } else if (pathname === '/api/v1/sessions') { await this.handleSessions(url, res) } else if (pathname === '/api/v1/contacts') { await this.handleContacts(url, res) } else { this.sendError(res, 404, 'Not Found') } } catch (error) { console.error('[HttpService] Request error:', error) this.sendError(res, 500, String(error)) } } /** * 鎵归噺鑾峰彇娑堟伅锛堝惊鐜父鏍囩洿鍒版弧瓒?limit锛? * 缁曡繃 chatService 鐨勫崟 batch 闄愬埗锛岀洿鎺ユ搷浣?wcdbService 娓告爣 */ private async fetchMessagesBatch( talker: string, offset: number, limit: number, startTime: number, endTime: number, ascending: boolean ): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> { try { // 浣跨敤鍥哄畾 batch 澶у皬锛堜笌 limit 鐩稿悓鎴栨渶澶?500锛夋潵鍑忓皯寰幆娆℃暟 const batchSize = Math.min(limit, 500) const beginTimestamp = startTime > 10000000000 ? Math.floor(startTime / 1000) : startTime const endTimestamp = endTime > 10000000000 ? Math.floor(endTime / 1000) : endTime const cursorResult = await wcdbService.openMessageCursor(talker, batchSize, ascending, beginTimestamp, endTimestamp) if (!cursorResult.success || !cursorResult.cursor) { return { success: false, error: cursorResult.error || '鎵撳紑娑堟伅娓告爣澶辫触' } } const cursor = cursorResult.cursor try { const allRows: Record[] = [] let hasMore = true let skipped = 0 // 寰幆鑾峰彇娑堟伅锛屽鐞?offset 璺宠繃 + limit 绱Н while (allRows.length < limit && hasMore) { const batch = await wcdbService.fetchMessageBatch(cursor) if (!batch.success || !batch.rows || batch.rows.length === 0) { hasMore = false break } let rows = batch.rows hasMore = batch.hasMore === true // 澶勭悊 offset: 璺宠繃鍓?N 鏉? if (skipped < offset) { const remaining = offset - skipped if (remaining >= rows.length) { skipped += rows.length continue } rows = rows.slice(remaining) skipped = offset } allRows.push(...rows) } const trimmedRows = allRows.slice(0, limit) const finalHasMore = hasMore || allRows.length > limit const messages = chatService.mapRowsToMessagesForApi(trimmedRows) return { success: true, messages, hasMore: finalHasMore } } finally { await wcdbService.closeMessageCursor(cursor) } } catch (e) { console.error('[HttpService] fetchMessagesBatch error:', e) return { success: false, error: String(e) } } } /** * Query param helpers. */ private parseIntParam(value: string | null, defaultValue: number, min: number, max: number): number { const parsed = parseInt(value || '', 10) if (!Number.isFinite(parsed)) return defaultValue return Math.min(Math.max(parsed, min), max) } private parseBooleanParam(url: URL, keys: string[], defaultValue: boolean = false): boolean { for (const key of keys) { const raw = url.searchParams.get(key) if (raw === null) continue const normalized = raw.trim().toLowerCase() if (['1', 'true', 'yes', 'on'].includes(normalized)) return true if (['0', 'false', 'no', 'off'].includes(normalized)) return false } return defaultValue } private parseMediaOptions(url: URL): ApiMediaOptions { const mediaEnabled = this.parseBooleanParam(url, ['media', 'meiti'], false) if (!mediaEnabled) { return { enabled: false, exportImages: false, exportVoices: false, exportVideos: false, exportEmojis: false } } return { enabled: true, exportImages: this.parseBooleanParam(url, ['image', 'tupian'], true), exportVoices: this.parseBooleanParam(url, ['voice', 'vioce'], true), exportVideos: this.parseBooleanParam(url, ['video'], true), exportEmojis: this.parseBooleanParam(url, ['emoji'], true) } } private async handleMessages(url: URL, res: http.ServerResponse): Promise { const talker = (url.searchParams.get('talker') || '').trim() const limit = this.parseIntParam(url.searchParams.get('limit'), 100, 1, 10000) const offset = this.parseIntParam(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER) const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase() const startParam = url.searchParams.get('start') const endParam = url.searchParams.get('end') const chatlab = this.parseBooleanParam(url, ['chatlab'], false) const formatParam = (url.searchParams.get('format') || '').trim().toLowerCase() const format = formatParam || (chatlab ? 'chatlab' : 'json') const mediaOptions = this.parseMediaOptions(url) if (!talker) { this.sendError(res, 400, 'Missing required parameter: talker') return } if (format !== 'json' && format !== 'chatlab') { this.sendError(res, 400, 'Invalid format, supported: json/chatlab') return } const startTime = this.parseTimeParam(startParam) const endTime = this.parseTimeParam(endParam, true) const queryOffset = keyword ? 0 : offset const queryLimit = keyword ? 10000 : limit const result = await this.fetchMessagesBatch(talker, queryOffset, queryLimit, startTime, endTime, true) if (!result.success || !result.messages) { this.sendError(res, 500, result.error || 'Failed to get messages') return } let messages = result.messages let hasMore = result.hasMore === true if (keyword) { const filtered = messages.filter((msg) => { const content = (msg.parsedContent || msg.rawContent || '').toLowerCase() return content.includes(keyword) }) const endIndex = offset + limit hasMore = filtered.length > endIndex messages = filtered.slice(offset, endIndex) } const mediaMap = mediaOptions.enabled ? await this.exportMediaForMessages(messages, talker, mediaOptions) : new Map() const displayNames = await this.getDisplayNames([talker]) const talkerName = displayNames[talker] || talker if (format === 'chatlab') { const chatLabData = await this.convertToChatLab(messages, talker, talkerName, mediaMap) this.sendJson(res, { ...chatLabData, media: { enabled: mediaOptions.enabled, exportPath: this.getApiMediaExportPath(), count: mediaMap.size } }) return } const apiMessages = messages.map((msg) => this.toApiMessage(msg, mediaMap.get(msg.localId))) this.sendJson(res, { success: true, talker, count: apiMessages.length, hasMore, media: { enabled: mediaOptions.enabled, exportPath: this.getApiMediaExportPath(), count: mediaMap.size }, messages: apiMessages }) } /** * 澶勭悊浼氳瘽鍒楄〃鏌ヨ * GET /api/v1/sessions?keyword=xxx&limit=100 */ private async handleSessions(url: URL, res: http.ServerResponse): Promise { const keyword = (url.searchParams.get('keyword') || '').trim() const limit = this.parseIntParam(url.searchParams.get('limit'), 100, 1, 10000) try { const sessions = await chatService.getSessions() if (!sessions.success || !sessions.sessions) { this.sendError(res, 500, sessions.error || 'Failed to get sessions') return } let filteredSessions = sessions.sessions if (keyword) { const lowerKeyword = keyword.toLowerCase() filteredSessions = sessions.sessions.filter(s => s.username.toLowerCase().includes(lowerKeyword) || (s.displayName && s.displayName.toLowerCase().includes(lowerKeyword)) ) } // 搴旂敤 limit const limitedSessions = filteredSessions.slice(0, limit) this.sendJson(res, { success: true, count: limitedSessions.length, sessions: limitedSessions.map(s => ({ username: s.username, displayName: s.displayName, type: s.type, lastTimestamp: s.lastTimestamp, unreadCount: s.unreadCount })) }) } catch (error) { this.sendError(res, 500, String(error)) } } /** * 澶勭悊鑱旂郴浜烘煡璇? * GET /api/v1/contacts?keyword=xxx&limit=100 */ private async handleContacts(url: URL, res: http.ServerResponse): Promise { const keyword = (url.searchParams.get('keyword') || '').trim() const limit = this.parseIntParam(url.searchParams.get('limit'), 100, 1, 10000) try { const contacts = await chatService.getContacts() if (!contacts.success || !contacts.contacts) { this.sendError(res, 500, contacts.error || 'Failed to get contacts') return } let filteredContacts = contacts.contacts if (keyword) { const lowerKeyword = keyword.toLowerCase() filteredContacts = contacts.contacts.filter(c => c.username.toLowerCase().includes(lowerKeyword) || (c.nickname && c.nickname.toLowerCase().includes(lowerKeyword)) || (c.remark && c.remark.toLowerCase().includes(lowerKeyword)) || (c.displayName && c.displayName.toLowerCase().includes(lowerKeyword)) ) } const limited = filteredContacts.slice(0, limit) this.sendJson(res, { success: true, count: limited.length, contacts: limited }) } catch (error) { this.sendError(res, 500, String(error)) } } private getApiMediaExportPath(): string { return path.join(this.configService.getCacheBasePath(), 'api-media') } private sanitizeFileName(value: string, fallback: string): string { const safe = (value || '') .trim() .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') .replace(/\.+$/g, '') return safe || fallback } private ensureDir(dirPath: string): void { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }) } } private detectImageExt(buffer: Buffer): string { if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return '.jpg' if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return '.png' if (buffer.length >= 6) { const sig6 = buffer.subarray(0, 6).toString('ascii') if (sig6 === 'GIF87a' || sig6 === 'GIF89a') return '.gif' } if (buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') return '.webp' if (buffer.length >= 2 && buffer[0] === 0x42 && buffer[1] === 0x4d) return '.bmp' return '.jpg' } private async exportMediaForMessages( messages: Message[], talker: string, options: ApiMediaOptions ): Promise> { const mediaMap = new Map() if (!options.enabled || messages.length === 0) { return mediaMap } const sessionDir = path.join(this.getApiMediaExportPath(), this.sanitizeFileName(talker, 'session')) this.ensureDir(sessionDir) for (const msg of messages) { const exported = await this.exportMediaForMessage(msg, talker, sessionDir, options) if (exported) { mediaMap.set(msg.localId, exported) } } return mediaMap } private async exportMediaForMessage( msg: Message, talker: string, sessionDir: string, options: ApiMediaOptions ): Promise { try { if (msg.localType === 3 && options.exportImages) { const result = await chatService.getImageData(talker, String(msg.localId)) if (result.success && result.data) { const imageBuffer = Buffer.from(result.data, 'base64') const ext = this.detectImageExt(imageBuffer) const fileBase = this.sanitizeFileName(msg.imageMd5 || msg.imageDatName || `image_${msg.localId}`, `image_${msg.localId}`) const fileName = `${fileBase}${ext}` const targetDir = path.join(sessionDir, 'images') const fullPath = path.join(targetDir, fileName) this.ensureDir(targetDir) if (!fs.existsSync(fullPath)) { fs.writeFileSync(fullPath, imageBuffer) } return { kind: 'image', fileName, fullPath } } } if (msg.localType === 34 && options.exportVoices) { const result = await chatService.getVoiceData( talker, String(msg.localId), msg.createTime || undefined, msg.serverId || undefined ) if (result.success && result.data) { const fileName = `voice_${msg.localId}.wav` const targetDir = path.join(sessionDir, 'voices') const fullPath = path.join(targetDir, fileName) this.ensureDir(targetDir) if (!fs.existsSync(fullPath)) { fs.writeFileSync(fullPath, Buffer.from(result.data, 'base64')) } return { kind: 'voice', fileName, fullPath } } } if (msg.localType === 43 && options.exportVideos && msg.videoMd5) { const info = await videoService.getVideoInfo(msg.videoMd5) if (info.exists && info.videoUrl && fs.existsSync(info.videoUrl)) { const ext = path.extname(info.videoUrl) || '.mp4' const fileName = `${this.sanitizeFileName(msg.videoMd5, `video_${msg.localId}`)}${ext}` const targetDir = path.join(sessionDir, 'videos') const fullPath = path.join(targetDir, fileName) this.ensureDir(targetDir) if (!fs.existsSync(fullPath)) { fs.copyFileSync(info.videoUrl, fullPath) } return { kind: 'video', fileName, fullPath } } } if (msg.localType === 47 && options.exportEmojis && msg.emojiCdnUrl) { const result = await chatService.downloadEmoji(msg.emojiCdnUrl, msg.emojiMd5) if (result.success && result.localPath && fs.existsSync(result.localPath)) { const sourceExt = path.extname(result.localPath) || '.gif' const fileName = `${this.sanitizeFileName(msg.emojiMd5 || `emoji_${msg.localId}`, `emoji_${msg.localId}`)}${sourceExt}` const targetDir = path.join(sessionDir, 'emojis') const fullPath = path.join(targetDir, fileName) this.ensureDir(targetDir) if (!fs.existsSync(fullPath)) { fs.copyFileSync(result.localPath, fullPath) } return { kind: 'emoji', fileName, fullPath } } } } catch (e) { console.warn('[HttpService] exportMediaForMessage failed:', e) } return null } private toApiMessage(msg: Message, media?: ApiExportedMedia): Record { return { localId: msg.localId, serverId: msg.serverId, localType: msg.localType, createTime: msg.createTime, sortSeq: msg.sortSeq, isSend: msg.isSend, senderUsername: msg.senderUsername, content: this.getMessageContent(msg), rawContent: msg.rawContent, parsedContent: msg.parsedContent, mediaType: media?.kind, mediaFileName: media?.fileName, mediaPath: media?.fullPath } } /** * 瑙f瀽鏃堕棿鍙傛暟 * 鏀寔 YYYYMMDD 鏍煎紡锛岃繑鍥炵绾ф椂闂存埑 */ private parseTimeParam(param: string | null, isEnd: boolean = false): number { if (!param) return 0 // 绾暟瀛椾笖闀垮害涓?锛岃涓?YYYYMMDD if (/^\d{8}$/.test(param)) { const year = parseInt(param.slice(0, 4), 10) const month = parseInt(param.slice(4, 6), 10) - 1 const day = parseInt(param.slice(6, 8), 10) const date = new Date(year, month, day) if (isEnd) { // 缁撴潫鏃堕棿璁句负褰撳ぉ 23:59:59 date.setHours(23, 59, 59, 999) } return Math.floor(date.getTime() / 1000) } // 绾暟瀛楋紝瑙嗕负鏃堕棿鎴? if (/^\d+$/.test(param)) { const ts = parseInt(param, 10) // 濡傛灉鏄绉掔骇鏃堕棿鎴筹紝杞负绉掔骇 return ts > 10000000000 ? Math.floor(ts / 1000) : ts } return 0 } /** * 鑾峰彇鏄剧ず鍚嶇О */ private async getDisplayNames(usernames: string[]): Promise> { try { const result = await wcdbService.getDisplayNames(usernames) if (result.success && result.map) { return result.map } } catch (e) { console.error('[HttpService] Failed to get display names:', e) } // 杩斿洖绌哄璞★紝璋冪敤鏂逛細浣跨敤 username 浣滀负澶囩敤 return {} } /** * 杞崲涓?ChatLab 鏍煎紡 */ private async convertToChatLab( messages: Message[], talkerId: string, talkerName: string, mediaMap: Map = new Map() ): Promise { const isGroup = talkerId.endsWith('@chatroom') const myWxid = this.configService.get('myWxid') || '' // 鏀堕泦鎵€鏈夊彂閫佽€? const senderSet = new Set() for (const msg of messages) { if (msg.senderUsername) { senderSet.add(msg.senderUsername) } } // 鑾峰彇鍙戦€佽€呮樉绀哄悕 const senderNames = await this.getDisplayNames(Array.from(senderSet)) // 鑾峰彇缇ゆ樀绉帮紙濡傛灉鏄兢鑱婏級 let groupNicknamesMap = new Map() if (isGroup) { try { const result = await wcdbService.getGroupNicknames(talkerId) if (result.success && result.nicknames) { groupNicknamesMap = new Map(Object.entries(result.nicknames)) } } catch (e) { console.error('[HttpService] Failed to get group nicknames:', e) } } // 鏋勫缓鎴愬憳鍒楄〃 const memberMap = new Map() for (const msg of messages) { const sender = msg.senderUsername || '' if (sender && !memberMap.has(sender)) { const displayName = senderNames[sender] || sender const isSelf = sender === myWxid || sender.toLowerCase() === myWxid.toLowerCase() // 鑾峰彇缇ゆ樀绉帮紙灏濊瘯澶氱鏂瑰紡锛? const groupNickname = isGroup ? (groupNicknamesMap.get(sender) || groupNicknamesMap.get(sender.toLowerCase()) || '') : '' memberMap.set(sender, { platformId: sender, accountName: isSelf ? '鎴? : displayName, groupNickname: groupNickname || undefined }) } } // 杞崲娑堟伅 const chatLabMessages: ChatLabMessage[] = messages.map(msg => { const sender = msg.senderUsername || '' const isSelf = msg.isSend === 1 || sender === myWxid const accountName = isSelf ? '鎴? : (senderNames[sender] || sender) // 鑾峰彇璇ュ彂閫佽€呯殑缇ゆ樀绉? const groupNickname = isGroup ? (groupNicknamesMap.get(sender) || groupNicknamesMap.get(sender.toLowerCase()) || '') : '' return { sender, accountName, groupNickname: groupNickname || undefined, timestamp: msg.createTime, type: this.mapMessageType(msg.localType, msg), content: this.getMessageContent(msg), platformMessageId: msg.serverId ? String(msg.serverId) : undefined, mediaPath: mediaMap.get(msg.localId)?.fullPath } }) return { chatlab: { version: '0.0.2', exportedAt: Math.floor(Date.now() / 1000), generator: 'WeFlow' }, meta: { name: talkerName, platform: 'wechat', type: isGroup ? 'group' : 'private', groupId: isGroup ? talkerId : undefined, ownerId: myWxid || undefined }, members: Array.from(memberMap.values()), messages: chatLabMessages } } /** * 鏄犲皠 WeChat 娑堟伅绫诲瀷鍒?ChatLab 绫诲瀷 */ private mapMessageType(localType: number, msg: Message): number { switch (localType) { case 1: // 鏂囨湰 return ChatLabType.TEXT case 3: // 鍥剧墖 return ChatLabType.IMAGE case 34: // 璇煶 return ChatLabType.VOICE case 43: // 瑙嗛 return ChatLabType.VIDEO case 47: // 鍔ㄧ敾琛ㄦ儏 return ChatLabType.EMOJI case 48: // 浣嶇疆 return ChatLabType.LOCATION case 42: // 鍚嶇墖 return ChatLabType.CONTACT case 50: // 璇煶/瑙嗛閫氳瘽 return ChatLabType.CALL case 10000: // 绯荤粺娑堟伅 return ChatLabType.SYSTEM case 49: // 澶嶅悎娑堟伅 return this.mapType49(msg) case 244813135921: // 寮曠敤娑堟伅 return ChatLabType.REPLY case 266287972401: // 鎷嶄竴鎷? return ChatLabType.POKE case 8594229559345: // 绾㈠寘 return ChatLabType.RED_PACKET case 8589934592049: // 杞处 return ChatLabType.TRANSFER default: return ChatLabType.OTHER } } /** * 鏄犲皠 Type 49 瀛愮被鍨? */ private mapType49(msg: Message): number { const xmlType = msg.xmlType switch (xmlType) { case '5': // 閾炬帴 case '49': return ChatLabType.LINK case '6': // 鏂囦欢 return ChatLabType.FILE case '19': // 鑱婂ぉ璁板綍 return ChatLabType.FORWARD case '33': // 灏忕▼搴? case '36': return ChatLabType.SHARE case '57': // 寮曠敤娑堟伅 return ChatLabType.REPLY case '2000': // 杞处 return ChatLabType.TRANSFER case '2001': // 绾㈠寘 return ChatLabType.RED_PACKET default: return ChatLabType.OTHER } } /** * 鑾峰彇娑堟伅鍐呭 */ private getMessageContent(msg: Message): string | null { // 浼樺厛浣跨敤宸茶В鏋愮殑鍐呭 if (msg.parsedContent) { return msg.parsedContent } // 鏍规嵁绫诲瀷杩斿洖鍗犱綅绗? switch (msg.localType) { case 1: return msg.rawContent || null case 3: return '[图片]' case 34: return '[语音]' case 43: return '[视频]' case 47: return '[表情]' case 42: return msg.cardNickname || '[名片]' case 48: return '[位置]' case 49: return msg.linkTitle || msg.fileName || '[消息]' default: return msg.rawContent || null } } /** * 鍙戦€?JSON 鍝嶅簲 */ private sendJson(res: http.ServerResponse, data: any): void { res.setHeader('Content-Type', 'application/json; charset=utf-8') res.writeHead(200) res.end(JSON.stringify(data, null, 2)) } /** * 鍙戦€侀敊璇搷搴? */ private sendError(res: http.ServerResponse, code: number, message: string): void { res.setHeader('Content-Type', 'application/json; charset=utf-8') res.writeHead(code) res.end(JSON.stringify({ error: message })) } } export const httpService = new HttpService()