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])
}