File size: 4,826 Bytes
73bc263 44e0492 73bc263 44e0492 73bc263 44e0492 73bc263 44e0492 1caee20 588f433 1caee20 44e0492 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | /**
* useFeishuBridge — Feishu inbound/outbound bridge hook.
*
* - Auto-starts Feishu service if credentials are saved.
* - Tracks AI response turns for Feishu-originated messages.
* - Sends generated responses back to the originating Feishu chat.
*/
import { useEffect, useRef } from 'react'
import { feishuService } from '../services/feishu/FeishuService.js'
import { getFeishuConfig } from '../services/feishu/feishuConfig.js'
import { getContentText } from '../utils/messages.js'
import type { Message } from '../types/message.js'
type Props = {
messages: Message[]
isLoading: boolean
}
type ActiveFeishuTurn = {
chatId: string
responseParts: string[]
}
const FEISHU_CHANNEL_SERVER = 'feishu'
export function useFeishuBridge({ messages, isLoading }: Props): void {
const pendingInboundRef = useRef<{ chatId: string }[]>([])
const activeTurnRef = useRef<ActiveFeishuTurn | null>(null)
const lastProcessedMessageCountRef = useRef(messages.length)
const previousLoadingRef = useRef(isLoading)
// Subscribe to inbound events (stores chatId for turn tracking)
useEffect(() => {
return feishuService.subscribeToInbound(event => {
pendingInboundRef.current.push({ chatId: event.chatId })
})
}, [])
// Process new messages — detect Feishu-originated user messages and collect
// assistant responses
useEffect(() => {
const newMessages = messages.slice(lastProcessedMessageCountRef.current)
for (const message of newMessages) {
if (
message.type === 'user' &&
typeof message.origin === 'object' &&
message.origin !== null &&
(message.origin as Record<string, unknown>).kind === 'channel' &&
(message.origin as Record<string, unknown>).server === FEISHU_CHANNEL_SERVER
) {
const inbound = pendingInboundRef.current.shift()
if (inbound) {
activeTurnRef.current = {
chatId: inbound.chatId,
responseParts: [],
}
}
continue
}
if (message.type === 'assistant' && activeTurnRef.current) {
const text = getContentText(message.message.content)
if (text) {
activeTurnRef.current.responseParts.push(text)
}
continue
}
if (
message.type === 'system' &&
message.subtype === 'local_command' &&
activeTurnRef.current
) {
const text = message.content
if (text) {
activeTurnRef.current.responseParts.push(text)
}
}
}
lastProcessedMessageCountRef.current = messages.length
}, [messages])
// When loading completes, send accumulated response back to Feishu
useEffect(() => {
const wasLoading = previousLoadingRef.current
previousLoadingRef.current = isLoading
if (!wasLoading || isLoading || !activeTurnRef.current) return
const completedTurn = activeTurnRef.current
activeTurnRef.current = null
void (async () => {
try {
const reply =
completedTurn.responseParts.join('\n\n').trim() ||
'这一轮没有可回传的文本结果,请查看本地终端会话。'
await feishuService.sendMarkdown(completedTurn.chatId, reply)
// Also send TTS voice if enabled
const config = getFeishuConfig()
if (config.ttsEnabled) {
const rawText = completedTurn.responseParts.join('\n')
// Deep-clean text before sending to TTS
let plainText = rawText
// 去掉代码块标记 ``` ,但保留里面内容
plainText = plainText.replace(/```(\w*)\n?([\s\S]*?)```/g, '$2')
// 去掉行内代码标记 ` ,但保留内容
plainText = plainText.replace(/`([^`]+)`/g, '$1')
// 去掉 markdown 语法符号(# * _ ~ > | 等)
plainText = plainText.replace(/[#*_~>|^=\-\[\]()>|\\]/g, '')
// 去掉花括号内容
plainText = plainText.replace(/\{[^}]*\}/g, '')
// 去掉 emoji
plainText = plainText.replace(
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2300}-\u{23FF}\u{2B50}\u{FE0F}]/gu,
'',
)
// 合并多个换行/空行为单个换行
plainText = plainText.replace(/\n{2,}/g, '\n')
// 去掉行首尾空白
plainText = plainText.split('\n').map(l => l.trim()).join('\n')
plainText = plainText.trim()
if (plainText) {
await feishuService.sendVoice(completedTurn.chatId, plainText)
}
}
} catch (error) {
console.warn(
'[feishu] failed to send outbound reply:',
error instanceof Error ? error.message : String(error),
)
}
})()
}, [isLoading])
}
|