mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-27 15:07:55 +00:00
Merge branch 'dev' of https://github.com/hicccc77/WeFlow into dev
This commit is contained in:
@@ -2199,6 +2199,13 @@ function registerIpcHandlers() {
|
||||
return groupAnalyticsService.getGroupMediaStats(chatroomId, startTime, endTime)
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'groupAnalytics:getGroupMemberAnalytics',
|
||||
async (_, chatroomId: string, memberUsername: string, startTime?: number, endTime?: number) => {
|
||||
return groupAnalyticsService.getGroupMemberAnalytics(chatroomId, memberUsername, startTime, endTime)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'groupAnalytics:getGroupMemberMessages',
|
||||
async (
|
||||
@@ -2647,8 +2654,9 @@ function registerIpcHandlers() {
|
||||
})
|
||||
|
||||
// HTTP API 服务
|
||||
ipcMain.handle('http:start', async (_, port?: number) => {
|
||||
return httpService.start(port || 5031)
|
||||
ipcMain.handle('http:start', async (_, port?: number, host?: string) => {
|
||||
const bindHost = typeof host === 'string' && host.trim() ? host.trim() : '127.0.0.1'
|
||||
return httpService.start(port || 5031, bindHost)
|
||||
})
|
||||
|
||||
ipcMain.handle('http:stop', async () => {
|
||||
@@ -2867,6 +2875,8 @@ app.whenReady().then(async () => {
|
||||
// 启动时检测更新(不阻塞启动)
|
||||
checkForUpdatesOnStartup()
|
||||
|
||||
await httpService.autoStart()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
mainWindow = createWindow()
|
||||
|
||||
@@ -297,6 +297,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getGroupMessageRanking: (chatroomId: string, limit?: number, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMessageRanking', chatroomId, limit, startTime, endTime),
|
||||
getGroupActiveHours: (chatroomId: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupActiveHours', chatroomId, startTime, endTime),
|
||||
getGroupMediaStats: (chatroomId: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMediaStats', chatroomId, startTime, endTime),
|
||||
getGroupMemberAnalytics: (chatroomId: string, memberUsername: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMemberAnalytics', chatroomId, memberUsername, startTime, endTime),
|
||||
getGroupMemberMessages: (
|
||||
chatroomId: string,
|
||||
memberUsername: string,
|
||||
@@ -422,7 +423,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
// HTTP API 服务
|
||||
http: {
|
||||
start: (port?: number) => ipcRenderer.invoke('http:start', port),
|
||||
start: (port?: number, host?: string) => ipcRenderer.invoke('http:start', port, host),
|
||||
stop: () => ipcRenderer.invoke('http:stop'),
|
||||
status: () => ipcRenderer.invoke('http:status')
|
||||
}
|
||||
|
||||
@@ -52,13 +52,17 @@ interface ConfigSchema {
|
||||
notificationFilterMode: 'all' | 'whitelist' | 'blacklist'
|
||||
notificationFilterList: string[]
|
||||
messagePushEnabled: boolean
|
||||
httpApiEnabled: boolean
|
||||
httpApiPort: number
|
||||
httpApiHost: string
|
||||
httpApiToken: string
|
||||
windowCloseBehavior: 'ask' | 'tray' | 'quit'
|
||||
quoteLayout: 'quote-top' | 'quote-bottom'
|
||||
wordCloudExcludeWords: string[]
|
||||
}
|
||||
|
||||
// 需要 safeStorage 加密的字段(普通模式)
|
||||
const ENCRYPTED_STRING_KEYS: Set<string> = new Set(['decryptKey', 'imageAesKey', 'authPassword'])
|
||||
const ENCRYPTED_STRING_KEYS: Set<string> = new Set(['decryptKey', 'imageAesKey', 'authPassword', 'httpApiToken'])
|
||||
const ENCRYPTED_BOOL_KEYS: Set<string> = new Set(['authEnabled', 'authUseHello'])
|
||||
const ENCRYPTED_NUMBER_KEYS: Set<string> = new Set(['imageXorKey'])
|
||||
|
||||
@@ -119,6 +123,10 @@ export class ConfigService {
|
||||
notificationPosition: 'top-right',
|
||||
notificationFilterMode: 'all',
|
||||
notificationFilterList: [],
|
||||
httpApiToken: '',
|
||||
httpApiEnabled: false,
|
||||
httpApiPort: 5031,
|
||||
httpApiHost: '127.0.0.1',
|
||||
messagePushEnabled: false,
|
||||
windowCloseBehavior: 'ask',
|
||||
quoteLayout: 'quote-top',
|
||||
@@ -662,11 +670,9 @@ export class ConfigService {
|
||||
|
||||
// 即使 authEnabled 被删除/篡改,如果密钥是 lock: 格式,说明曾开启过应用锁
|
||||
const rawDecryptKey: any = this.store.get('decryptKey')
|
||||
if (typeof rawDecryptKey === 'string' && rawDecryptKey.startsWith(LOCK_PREFIX)) {
|
||||
return true
|
||||
}
|
||||
return typeof rawDecryptKey === 'string' && rawDecryptKey.startsWith(LOCK_PREFIX);
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// === 工具方法 ===
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ConfigService } from './config'
|
||||
import { wcdbService } from './wcdbService'
|
||||
import { chatService } from './chatService'
|
||||
import type { Message } from './chatService'
|
||||
import type { ChatStatistics } from './analyticsService'
|
||||
|
||||
export interface GroupChatInfo {
|
||||
username: string
|
||||
@@ -49,6 +50,13 @@ export interface GroupMediaStats {
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface GroupMemberAnalytics {
|
||||
statistics: ChatStatistics
|
||||
timeDistribution: Record<number, number>
|
||||
commonPhrases?: Array<{ phrase: string; count: number }>
|
||||
commonEmojis?: Array<{ emoji: string; count: number }>
|
||||
}
|
||||
|
||||
export interface GroupMemberMessagesPage {
|
||||
messages: Message[]
|
||||
hasMore: boolean
|
||||
@@ -797,7 +805,12 @@ class GroupAnalyticsService {
|
||||
return normalized > 10000000000 ? Math.floor(normalized / 1000) : normalized
|
||||
}
|
||||
|
||||
private extractRowSenderUsername(row: Record<string, any>): string {
|
||||
private extractRowSenderUsername(row: Record<string, any>, myWxid?: string): string {
|
||||
const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? row.WCDB_CT_is_send
|
||||
if (isSendRaw != null && parseInt(isSendRaw, 10) === 1 && myWxid) {
|
||||
return myWxid
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
row.sender_username,
|
||||
row.senderUsername,
|
||||
@@ -820,13 +833,33 @@ class GroupAnalyticsService {
|
||||
if (normalizedValue) return normalizedValue
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: fast extract from raw content to avoid full parse
|
||||
const rawContent = String(row.StrContent || row.message_content || row.content || row.msg_content || '').trim()
|
||||
if (rawContent) {
|
||||
const match = /^\s*([a-zA-Z0-9_@-]{4,}):(?!\/\/)\s*(?:\r?\n|<br\s*\/?>)/i.exec(rawContent)
|
||||
if (match && match[1]) {
|
||||
return match[1].trim()
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private parseSingleMessageRow(row: Record<string, any>): Message | null {
|
||||
try {
|
||||
const mapped = chatService.mapRowsToMessagesForApi([row])
|
||||
return Array.isArray(mapped) && mapped.length > 0 ? mapped[0] : null
|
||||
if (Array.isArray(mapped) && mapped.length > 0) {
|
||||
const msg = mapped[0]
|
||||
if (!msg.localType) {
|
||||
msg.localType = parseInt(row.Type || row.type || row.local_type || row.msg_type || '0', 10)
|
||||
}
|
||||
if (!msg.createTime) {
|
||||
msg.createTime = parseInt(row.CreateTime || row.create_time || row.createTime || row.msg_time || '0', 10)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -881,7 +914,7 @@ class GroupAnalyticsService {
|
||||
if (rows.length === 0) break
|
||||
|
||||
for (const row of rows) {
|
||||
const senderFromRow = this.extractRowSenderUsername(row)
|
||||
const senderFromRow = this.extractRowSenderUsername(row, String(this.configService.get('myWxid') || '').trim())
|
||||
if (senderFromRow && !matchesTargetSender(senderFromRow)) {
|
||||
continue
|
||||
}
|
||||
@@ -987,7 +1020,7 @@ class GroupAnalyticsService {
|
||||
const row = rows[index]
|
||||
consumedRows += 1
|
||||
|
||||
const senderFromRow = this.extractRowSenderUsername(row)
|
||||
const senderFromRow = this.extractRowSenderUsername(row, String(this.configService.get('myWxid') || '').trim())
|
||||
if (senderFromRow && !matchesTargetSender(senderFromRow)) {
|
||||
continue
|
||||
}
|
||||
@@ -1467,6 +1500,154 @@ class GroupAnalyticsService {
|
||||
}
|
||||
}
|
||||
|
||||
async getGroupMemberAnalytics(
|
||||
chatroomId: string,
|
||||
memberUsername: string,
|
||||
startTime?: number,
|
||||
endTime?: number
|
||||
): Promise<{ success: boolean; data?: GroupMemberAnalytics; error?: string }> {
|
||||
try {
|
||||
const conn = await this.ensureConnected()
|
||||
if (!conn.success) return { success: false, error: conn.error }
|
||||
|
||||
const normalizedChatroomId = String(chatroomId || '').trim()
|
||||
const normalizedMemberUsername = String(memberUsername || '').trim()
|
||||
|
||||
const batchSize = 10000
|
||||
const senderMatchCache = new Map<string, boolean>()
|
||||
const matchesTargetSender = (sender: string | null | undefined): boolean => {
|
||||
const key = String(sender || '').trim().toLowerCase()
|
||||
if (!key) return false
|
||||
const cached = senderMatchCache.get(key)
|
||||
if (typeof cached === 'boolean') return cached
|
||||
const matched = this.isSameAccountIdentity(normalizedMemberUsername, sender)
|
||||
senderMatchCache.set(key, matched)
|
||||
return matched
|
||||
}
|
||||
|
||||
const cursorResult = await this.openMemberMessageCursor(normalizedChatroomId, batchSize, true, startTime || 0, endTime || 0)
|
||||
if (!cursorResult.success || !cursorResult.cursor) {
|
||||
return { success: false, error: cursorResult.error || '创建游标失败' }
|
||||
}
|
||||
|
||||
const cursor = cursorResult.cursor
|
||||
const stats: ChatStatistics = {
|
||||
totalMessages: 0,
|
||||
textMessages: 0,
|
||||
imageMessages: 0,
|
||||
voiceMessages: 0,
|
||||
videoMessages: 0,
|
||||
emojiMessages: 0,
|
||||
otherMessages: 0,
|
||||
sentMessages: 0, // In group, we only fetch messages of this member, so sentMessages = totalMessages
|
||||
receivedMessages: 0, // No meaning here
|
||||
firstMessageTime: null,
|
||||
lastMessageTime: null,
|
||||
activeDays: 0,
|
||||
messageTypeCounts: {}
|
||||
}
|
||||
|
||||
const hourlyDistribution: Record<number, number> = {}
|
||||
for (let i = 0; i < 24; i++) hourlyDistribution[i] = 0
|
||||
const dailySet = new Set<string>()
|
||||
const textTypes = [1, 244813135921]
|
||||
|
||||
const phraseCounts = new Map<string, number>()
|
||||
const emojiCounts = new Map<string, number>()
|
||||
|
||||
const myWxid = String(this.configService.get('myWxid') || '').trim()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const batch = await wcdbService.fetchMessageBatch(cursor)
|
||||
if (!batch.success) {
|
||||
return { success: false, error: batch.error || '获取分析数据失败' }
|
||||
}
|
||||
const rows = Array.isArray(batch.rows) ? batch.rows as Record<string, any>[] : []
|
||||
if (rows.length === 0) break
|
||||
|
||||
for (const row of rows) {
|
||||
let senderFromRow = this.extractRowSenderUsername(row, myWxid)
|
||||
|
||||
const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? row.WCDB_CT_is_send
|
||||
const isSend = isSendRaw != null ? parseInt(isSendRaw, 10) === 1 : false
|
||||
|
||||
if (isSend) {
|
||||
senderFromRow = myWxid
|
||||
}
|
||||
|
||||
if (!senderFromRow || !matchesTargetSender(senderFromRow)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const msgType = parseInt(row.Type || row.type || row.local_type || row.msg_type || '0', 10)
|
||||
const createTime = parseInt(row.CreateTime || row.create_time || row.createTime || row.msg_time || '0', 10)
|
||||
|
||||
let content = String(row.StrContent || row.message_content || row.content || row.msg_content || '')
|
||||
if (content) {
|
||||
content = content.replace(/^\s*([a-zA-Z0-9_@-]{4,}):(?!\/\/)\s*(?:\r?\n|<br\s*\/?>)/i, '')
|
||||
}
|
||||
|
||||
stats.totalMessages++
|
||||
if (textTypes.includes(msgType)) {
|
||||
stats.textMessages++
|
||||
if (content) {
|
||||
const text = content.trim()
|
||||
if (text && text.length <= 20) {
|
||||
phraseCounts.set(text, (phraseCounts.get(text) || 0) + 1)
|
||||
}
|
||||
const emojiMatches = text.match(/\[.*?\]/g)
|
||||
if (emojiMatches) {
|
||||
for (const em of emojiMatches) {
|
||||
emojiCounts.set(em, (emojiCounts.get(em) || 0) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (msgType === 3) stats.imageMessages++
|
||||
else if (msgType === 34) stats.voiceMessages++
|
||||
else if (msgType === 43) stats.videoMessages++
|
||||
else if (msgType === 47) stats.emojiMessages++
|
||||
else stats.otherMessages++
|
||||
|
||||
stats.sentMessages++
|
||||
|
||||
stats.messageTypeCounts[msgType] = (stats.messageTypeCounts[msgType] || 0) + 1
|
||||
|
||||
if (createTime > 0) {
|
||||
if (stats.firstMessageTime === null || createTime < stats.firstMessageTime) stats.firstMessageTime = createTime
|
||||
if (stats.lastMessageTime === null || createTime > stats.lastMessageTime) stats.lastMessageTime = createTime
|
||||
|
||||
const d = new Date(createTime * 1000)
|
||||
const hour = d.getHours()
|
||||
hourlyDistribution[hour]++
|
||||
dailySet.add(`${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`)
|
||||
}
|
||||
}
|
||||
if (!batch.hasMore) break
|
||||
}
|
||||
} finally {
|
||||
await wcdbService.closeMessageCursor(cursor)
|
||||
}
|
||||
|
||||
stats.activeDays = dailySet.size
|
||||
|
||||
const commonPhrases = Array.from(phraseCounts.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
.map(([phrase, count]) => ({ phrase, count }))
|
||||
|
||||
const commonEmojis = Array.from(emojiCounts.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
.map(([emoji, count]) => ({ emoji, count }))
|
||||
|
||||
return { success: true, data: { statistics: stats, timeDistribution: hourlyDistribution, commonPhrases, commonEmojis } }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async exportGroupMemberMessages(
|
||||
chatroomId: string,
|
||||
memberUsername: string,
|
||||
|
||||
@@ -101,6 +101,7 @@ class HttpService {
|
||||
private server: http.Server | null = null
|
||||
private configService: ConfigService
|
||||
private port: number = 5031
|
||||
private host: string = '127.0.0.1'
|
||||
private running: boolean = false
|
||||
private connections: Set<import('net').Socket> = new Set()
|
||||
private messagePushClients: Set<http.ServerResponse> = new Set()
|
||||
@@ -114,12 +115,13 @@ class HttpService {
|
||||
/**
|
||||
* 启动 HTTP 服务
|
||||
*/
|
||||
async start(port: number = 5031): Promise<{ success: boolean; port?: number; error?: string }> {
|
||||
async start(port: number = 5031, host: string = '127.0.0.1'): Promise<{ success: boolean; port?: number; error?: string }> {
|
||||
if (this.running && this.server) {
|
||||
return { success: true, port: this.port }
|
||||
}
|
||||
|
||||
this.port = port
|
||||
this.host = host
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.server = http.createServer((req, res) => this.handleRequest(req, res))
|
||||
@@ -153,10 +155,10 @@ class HttpService {
|
||||
}
|
||||
})
|
||||
|
||||
this.server.listen(this.port, '127.0.0.1', () => {
|
||||
this.server.listen(this.port, this.host, () => {
|
||||
this.running = true
|
||||
this.startMessagePushHeartbeat()
|
||||
console.log(`[HttpService] HTTP API server started on http://127.0.0.1:${this.port}`)
|
||||
console.log(`[HttpService] HTTP API server started on http://${this.host}:${this.port}`)
|
||||
resolve({ success: true, port: this.port })
|
||||
})
|
||||
})
|
||||
@@ -225,7 +227,7 @@ class HttpService {
|
||||
}
|
||||
|
||||
getMessagePushStreamUrl(): string {
|
||||
return `http://127.0.0.1:${this.port}/api/v1/push/messages`
|
||||
return `http://${this.host}:${this.port}/api/v1/push/messages`
|
||||
}
|
||||
|
||||
broadcastMessagePush(payload: Record<string, unknown>): void {
|
||||
@@ -246,49 +248,116 @@ class HttpService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 请求
|
||||
*/
|
||||
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
// 设置 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/push/messages') {
|
||||
this.handleMessagePushStream(req, res)
|
||||
} 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 if (pathname === '/api/v1/group-members') {
|
||||
await this.handleGroupMembers(url, res)
|
||||
} else if (pathname.startsWith('/api/v1/media/')) {
|
||||
this.handleMediaRequest(pathname, res)
|
||||
} else {
|
||||
this.sendError(res, 404, 'Not Found')
|
||||
async autoStart(): Promise<void> {
|
||||
const enabled = this.configService.get('httpApiEnabled')
|
||||
if (enabled) {
|
||||
const port = Number(this.configService.get('httpApiPort')) || 5031
|
||||
const host = String(this.configService.get('httpApiHost') || '127.0.0.1').trim() || '127.0.0.1'
|
||||
try {
|
||||
await this.start(port, host)
|
||||
console.log(`[HttpService] Auto-started on port ${port}`)
|
||||
} catch (err) {
|
||||
console.error('[HttpService] Auto-start failed:', err)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HttpService] Request error:', error)
|
||||
this.sendError(res, 500, String(error))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 POST 请求的 JSON Body
|
||||
*/
|
||||
private async parseBody(req: http.IncomingMessage): Promise<Record<string, any>> {
|
||||
if (req.method !== 'POST') return {}
|
||||
return new Promise((resolve) => {
|
||||
let body = ''
|
||||
req.on('data', chunk => { body += chunk.toString() })
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(body))
|
||||
} catch {
|
||||
resolve({})
|
||||
}
|
||||
})
|
||||
req.on('error', () => resolve({}))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 鉴权拦截器
|
||||
*/
|
||||
private verifyToken(req: http.IncomingMessage, url: URL, body: Record<string, any>): boolean {
|
||||
const expectedToken = String(this.configService.get('httpApiToken') || '').trim()
|
||||
if (!expectedToken) return true
|
||||
|
||||
const authHeader = req.headers.authorization
|
||||
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
|
||||
const token = authHeader.substring(7).trim()
|
||||
if (token === expectedToken) return true
|
||||
}
|
||||
|
||||
const queryToken = url.searchParams.get('access_token')
|
||||
if (queryToken && queryToken.trim() === expectedToken) return true
|
||||
|
||||
const bodyToken = body['access_token']
|
||||
return !!(bodyToken && String(bodyToken).trim() === expectedToken);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 请求 (重构后)
|
||||
*/
|
||||
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(req.url || '/', `http://${this.host}:${this.port}`)
|
||||
const pathname = url.pathname
|
||||
|
||||
try {
|
||||
const bodyParams = await this.parseBody(req)
|
||||
|
||||
for (const [key, value] of Object.entries(bodyParams)) {
|
||||
if (!url.searchParams.has(key)) {
|
||||
url.searchParams.set(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname !== '/health' && pathname !== '/api/v1/health') {
|
||||
if (!this.verifyToken(req, url, bodyParams)) {
|
||||
this.sendError(res, 401, 'Unauthorized: Invalid or missing access_token')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/health' || pathname === '/api/v1/health') {
|
||||
this.sendJson(res, { status: 'ok' })
|
||||
} else if (pathname === '/api/v1/push/messages') {
|
||||
this.handleMessagePushStream(req, res)
|
||||
} 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 if (pathname === '/api/v1/group-members') {
|
||||
await this.handleGroupMembers(url, res)
|
||||
} else if (pathname.startsWith('/api/v1/media/')) {
|
||||
this.handleMediaRequest(pathname, res)
|
||||
} else {
|
||||
this.sendError(res, 404, 'Not Found')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HttpService] Request error:', error)
|
||||
this.sendError(res, 500, String(error))
|
||||
}
|
||||
}
|
||||
private startMessagePushHeartbeat(): void {
|
||||
if (this.messagePushHeartbeatTimer) return
|
||||
this.messagePushHeartbeatTimer = setInterval(() => {
|
||||
@@ -895,7 +964,7 @@ class HttpService {
|
||||
parsedContent: msg.parsedContent,
|
||||
mediaType: media?.kind,
|
||||
mediaFileName: media?.fileName,
|
||||
mediaUrl: media ? `http://127.0.0.1:${this.port}/api/v1/media/${media.relativePath}` : undefined,
|
||||
mediaUrl: media ? `http://${this.host}:${this.port}/api/v1/media/${media.relativePath}` : undefined,
|
||||
mediaLocalPath: media?.fullPath
|
||||
}
|
||||
}
|
||||
@@ -1165,7 +1234,7 @@ class HttpService {
|
||||
type: this.mapMessageType(msg.localType, msg),
|
||||
content: this.getMessageContent(msg),
|
||||
platformMessageId: msg.serverId ? String(msg.serverId) : undefined,
|
||||
mediaPath: mediaMap.get(msg.localId) ? `http://127.0.0.1:${this.port}/api/v1/media/${mediaMap.get(msg.localId)!.relativePath}` : undefined
|
||||
mediaPath: mediaMap.get(msg.localId) ? `http://${this.host}:${this.port}/api/v1/media/${mediaMap.get(msg.localId)!.relativePath}` : undefined
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -75,6 +75,14 @@ export class VoiceTranscribeService {
|
||||
if (candidates.length === 0) {
|
||||
console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`)
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
// Windows: 把 sherpa-onnx DLL 所在目录加到 PATH,否则 native module 找不到依赖
|
||||
const existing = env['PATH'] || ''
|
||||
const merged = [...candidates, ...existing.split(';').filter(Boolean)]
|
||||
env['PATH'] = Array.from(new Set(merged)).join(';')
|
||||
if (candidates.length === 0) {
|
||||
console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`)
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
|
||||
Reference in New Issue
Block a user