codev / docs /friend /data-flow.md
chenbhao's picture
chore: docs v1.0
eeeb2b6
|
Raw History Blame
15.8 kB
# Friend ๆ•ฐๆฎๆต
ๆœฌๆ–‡ๆกฃ่ฏฆ็ป†ๆ่ฟฐ Friend ็ณป็ปŸไธญ็š„ๅ››็งๆ ธๅฟƒๆ•ฐๆฎๆต๏ผŒไปฅๅŠ SSE ไบ‹ไปถๆ ผๅผ่ง„่Œƒใ€‚
---
## 1. ๆ–‡ๅญ—ๅฏน่ฏๆต
็”จๆˆทๅœจๅ‰็ซฏ่พ“ๅ…ฅๆก†่พ“ๅ…ฅๆ–‡ๅญ—๏ผŒๆŒ‰ไธ‹ Enter ๅ‘้€ใ€‚
```
็”จๆˆท่พ“ๅ…ฅๆ–‡ๅญ—
โ”‚
โ–ผ
ChatInput.tsx โ”€โ”€ POST /plugins/friend/chat โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ { message: "ไฝ ๅฅฝ" } โ”‚
โ”‚ โ”‚
โ–ผ โ”‚
handleFriendApi() โ”€โ”€ friendService.start() โ”‚
โ”‚ friendService.sendText(message) โ”‚
โ–ผ โ”‚
FriendService.sendText() โ”‚
โ”‚ โ”‚
โ”œโ”€โ”€ ้€š็Ÿฅ inboundListeners (bridge hook turn tracking) โ”‚
โ”‚ โ”‚
โ””โ”€โ”€ messageQueueManager.enqueue() โ”‚
โ”‚ { mode: 'prompt', skipSlashCommands: true, โ”‚
โ”‚ bridgeOrigin: true, origin: { server: 'friend' }} โ”‚
โ”‚ โ”‚
โ–ผ โ”‚
AI Provider (Anthropic/NVIDIA/OpenAI) โ”‚
โ”‚ โ”‚
โ”œโ”€โ”€ ๅค„็†ๆถˆๆฏ โ”‚
โ”œโ”€โ”€ ๅฏ่ฐƒ็”จ friend_emotion ๅทฅๅ…ท่ฎพ็ฝฎ่กจๆƒ… โ”‚
โ”œโ”€โ”€ ๅฏ่ฐƒ็”จ friend_screen_observe ่ง‚ๅฏŸๅฑๅน• โ”‚
โ”‚ โ”‚
โ–ผ โ”‚
AI ๅ›žๅคๆตๅ›ž (้€š่ฟ‡ useFriendBridge / REPL) โ”‚
โ”‚ โ”‚
โ–ผ โ”‚
FriendService.broadcastResponse(text) โ”‚
โ”‚ โ”‚
โ”œโ”€โ”€ broadcastToVrm({ text }) โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ โ–ผ SSE data โ”‚
โ”‚ TextBubble ๆ”ถๅˆฐ โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€ ้‡็ฝฎๆฐ”ๆณก๏ผŒๆ˜พ็คบๆ–‡ๅญ— โ”‚
โ”‚ โ”œโ”€โ”€ ๅฏๅŠจๆ‰“ๅญ—ๆœบๆ•ˆๆžœ (้€ๅญ—็ฌฆๆ˜พ็คบ) โ”‚
โ”‚ โ”‚ - CJK: 200ms/char (TTSๅผ€ๅฏ) / 80ms (ๅ…ณ้—ญ) โ”‚
โ”‚ โ”‚ - English: 60ms/char (TTSๅผ€ๅฏ) / 30ms (ๅ…ณ้—ญ)โ”‚
โ”‚ โ”œโ”€โ”€ ๆ‰“ๅญ—ๆœบๅฎŒๆˆๅŽๆธฒๆŸ“ Markdown โ”‚
โ”‚ โ””โ”€โ”€ 1็ง’ๅŽ่งฆๅ‘ onMessage โ†’ VRMScene ่กจๆƒ…ๅŠจไฝœ โ”‚
โ”‚ โ”‚
โ”œโ”€โ”€ if TTS enabled: โ”‚
โ”‚ โ”œโ”€โ”€ generateTts(text) โ”‚
โ”‚ โ”‚ โ”œโ”€โ”€ stripForTts() ๆธ…ๆด—ๆ–‡ๆœฌ โ”‚
โ”‚ โ”‚ โ””โ”€โ”€ EdgeTTS ๆˆ– QwenTTS ็”Ÿๆˆ MP3 โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€ broadcastToVrm({ audioUrl, sendFirstTts }) โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ–ผ SSE data โ”‚
โ”‚ โ”‚ TextBubble ๅผ€ๅง‹้Ÿณ้ข‘้˜Ÿๅˆ—ๆ’ญๆ”พ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ””โ”€โ”€ LipSync.playAudio(url) โ”‚
โ”‚ โ”‚ โ”œโ”€โ”€ fetch ้Ÿณ้ข‘ๆ–‡ไปถ โ”‚
โ”‚ โ”‚ โ”œโ”€โ”€ decodeAudioData โ”‚
โ”‚ โ”‚ โ”œโ”€โ”€ ่ฟžๆŽฅๅˆฐ lipSyncNode (ๅˆ†ๆž) + gainNode (ๆ‰ฌๅฃฐๅ™จ) โ”‚
โ”‚ โ”‚ โ””โ”€โ”€ ๆ’ญๆ”พๆ—ถๅฎžๆ—ถๆ›ดๆ–ฐ VRM ๅ˜ดๅฝข โ”‚
โ”‚ โ”‚ โ”‚
โ”‚ โ””โ”€โ”€ extendMuteForTts(audioId) โ”‚
โ”‚ โ””โ”€โ”€ ็ฒพ็กฎ่ฎก็ฎ— MP3 ๆ—ถ้•ฟ๏ผŒๆ›ดๆ–ฐ้™้Ÿณๅฎšๆ—ถๅ™จ โ”‚
โ”‚ โ”‚
โ””โ”€โ”€ broadcastToVrm({ replyDone: true }) โ”‚
โ”‚ โ”‚
โ–ผ SSE data โ”‚
TextBubble ่ฐƒๅบฆๆฐ”ๆณก้š่— (2s ๅปถ่ฟŸ) โ”‚
ๆˆ–็ญ‰ๅพ…ๅŽ็ปญ appendText ๆถˆๆฏ โ”‚
```
---
## 2. ่ฏญ้Ÿณๆ•่Žทๆต (F2 ้€š่ฏๆจกๅผ)
็”จๆˆทๆŒ‰ไธ‹ F2 ่ฟ›ๅ…ฅ่ฟž็ปญ่ฏญ้Ÿณ้€š่ฏๆจกๅผ๏ผŒๅ†ๆฌกๆŒ‰ไธ‹ F2 ็ป“ๆŸ้€š่ฏใ€‚
### 2.1 ๅฏๅŠจ้€š่ฏ
```
็”จๆˆทๆŒ‰ F2
โ”‚
โ–ผ
ChatInput.startVoiceCall()
โ”‚
โ”œโ”€โ”€ setVoiceCallActive(true)
โ”‚
โ””โ”€โ”€ useServerStt.startStreaming()
โ”‚
โ””โ”€โ”€ POST /plugins/friend/voice/start
โ”‚
โ–ผ
handleFriendApi()
โ”‚
โ””โ”€โ”€ friendService.startVoiceCapture()
โ”‚
โ”œโ”€โ”€ ๆฃ€ๆต‹ STT provider (Groqโ†’Whisperโ†’Anthropicโ†’Doubao)
โ”‚
โ”œโ”€โ”€ startSttConnection() (8s ่ถ…ๆ—ถ)
โ”‚ โ”œโ”€โ”€ Groq: connectGroqStream()
โ”‚ โ”œโ”€โ”€ Local: connectLocalWhisperStream()
โ”‚ โ”œโ”€โ”€ Anthropic: connectVoiceStream()
โ”‚ โ””โ”€โ”€ Doubao: connectDoubaoStream()
โ”‚
โ”œโ”€โ”€ loadAudioCapture()
โ”‚ โ”œโ”€โ”€ ๅฐ่ฏ• arecord (ALSA)
โ”‚ โ”‚ args: -D default -r 16000 -f S16_LE -c 1 -t raw -q
โ”‚ โ”œโ”€โ”€ ๅคฑ่ดฅๅˆ™ๅฐ่ฏ• parecord (PulseAudio)
โ”‚ โ”‚ args: --raw --rate=16000 --format=s16le --channels=1
โ”‚ โ””โ”€โ”€ 500ms ้ชŒ่ฏ็ช—ๅฃ: ็กฎ่ฎคๅญ่ฟ›็จ‹่พ“ๅ‡บ้Ÿณ้ข‘ๆ•ฐๆฎ
โ”‚
โ”œโ”€โ”€ arecord ๆ•ฐๆฎๅ›ž่ฐƒ:
โ”‚ โ”œโ”€โ”€ if not muted โ†’ ่ฝฌๅ‘ๅˆฐ STT connection.send(chunk)
โ”‚ โ””โ”€โ”€ if not muted โ†’ ่ฝฌๅ‘ๅˆฐ VAD processAudio(float32)
โ”‚
โ””โ”€โ”€ vadInstance.start()
```
### 2.2 ่ฏญ้Ÿณๆฃ€ๆต‹ไธŽ่ฝฌๅฝ•
```
้บฆๅ…‹้ฃŽ้Ÿณ้ข‘ๆต (16kHz S16LE)
โ”‚
โ”œโ”€โ”€โ–บ STT Connection.send(chunk) (ๅฎžๆ—ถๆตๅผ่ฝฌๅฝ•)
โ”‚
โ””โ”€โ”€โ–บ SileroVad.processAudio(float32)
โ”‚
โ”œโ”€โ”€ RMS ้ข„่ฟ‡ๆปค (้˜ˆๅ€ผ 0.004)
โ”‚ โ”œโ”€โ”€ < ้˜ˆๅ€ผ โ†’ ๆฆ‚็އ = 0 (่ทณ่ฟ‡ๆŽจ็†)
โ”‚ โ””โ”€โ”€ >= ้˜ˆๅ€ผ โ†’ ONNX ๆŽจ็†
โ”‚
โ”œโ”€โ”€ ็Šถๆ€ๆœบ
โ”‚ โ”œโ”€โ”€ pre-speech: ้œ€่ฆ่ฟž็ปญ 10 ๅธง (~320ms) ็กฎ่ฎค่ฏด่ฏ
โ”‚ โ”œโ”€โ”€ speaking: ่ฏญ้ŸณๆŒ็ปญไธญ
โ”‚ โ””โ”€โ”€ silence redemption: ่ฟž็ปญ 20 ๅธง (~640ms) ้™้Ÿณ่งฆๅ‘ endSpeech
โ”‚
โ””โ”€โ”€ onSpeechEnd callback
โ”‚
โ–ผ
FriendService._flushVadSegment()
โ”‚
โ”œโ”€โ”€ ๅˆ›ๅปบๆ–ฐ็š„ STT ่ฟžๆŽฅ (ๆ—ง็š„่ฟžๆŽฅ็ปง็ปญๅค„็†)
โ”‚
โ”œโ”€โ”€ ็ญ‰ๅพ…ๆ—ง่ฟžๆŽฅ finalize()
โ”‚ โ””โ”€โ”€ ่Žทๅ–่ฝฌๅฝ•ๆ–‡ๆœฌๆŽจๅ…ฅ captureTranscripts
โ”‚
โ”œโ”€โ”€ if ๆœ‰่ฝฌๅฝ•ๆ–‡ๆœฌ:
โ”‚ โ”œโ”€โ”€ this.sendText(transcript)
โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ–ผ
โ”‚ โ”‚ messageQueueManager.enqueue() โ†’ AI ๅผ€ๅง‹ๅค„็†
โ”‚ โ”‚
โ”‚ โ””โ”€โ”€ this.startAiTurnMute()
โ”‚ โ”œโ”€โ”€ muted = true
โ”‚ โ”œโ”€โ”€ VAD pause
โ”‚ โ””โ”€โ”€ 30s ่ถ…ๆ—ถๅฎ‰ๅ…จๆ€งๅฎšๆ—ถๅ™จ
โ”‚
โ””โ”€โ”€ ๅพช็Žฏ็ปง็ปญ็›‘ๅฌไธ‹ไธ€ๆฎต่ฏญ้Ÿณ
```
### 2.3 AI ๅ›žๅคไธŽ้™้Ÿณ่งฃ้™ค
```
AI ๅค„็†ๅฎŒๆˆ
โ”‚
โ–ผ
FriendService.broadcastResponse(text)
โ”‚
โ”œโ”€โ”€ broadcastToVrm({ text }) // ๆ˜พ็คบๆ–‡ๅญ—
โ”‚
โ”œโ”€โ”€ if TTS enabled:
โ”‚ โ”œโ”€โ”€ generateTts(text)
โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ–ผ
โ”‚ โ”œโ”€โ”€ broadcastToVrm({ audioUrl, sendFirstTts: true })
โ”‚ โ”‚
โ”‚ โ””โ”€โ”€ extendMuteForTts(audioId)
โ”‚ โ”œโ”€โ”€ getMp3DurationMs() ็ฒพ็กฎ่ฎก็ฎ—
โ”‚ โ”œโ”€โ”€ ๅ–ๆถˆ 30s ๅฎ‰ๅ…จๆ€งๅฎšๆ—ถๅ™จ
โ”‚ โ””โ”€โ”€ ่ฎพๅฎš็ฒพ็กฎ็š„ๆ’ญๆ”พๆ—ถ้•ฟๅฎšๆ—ถๅ™จ
โ”‚
โ””โ”€โ”€ broadcastToVrm({ replyDone: true })
โ”‚
โ–ผ
ๆ’ญๆ”พๅฎŒๆˆๅŽ โ†’ unmute()
โ”œโ”€โ”€ muted = false
โ””โ”€โ”€ VAD resume (ๅฏ็ปง็ปญๆŽฅๆ”ถ่ฏญ้Ÿณ)
```
### 2.4 ็ป“ๆŸ้€š่ฏ
```
็”จๆˆทๆŒ‰ F2 (ๅ†ๆฌก)
โ”‚
โ–ผ
ChatInput.endVoiceCall()
โ”‚
โ””โ”€โ”€ useServerStt.stopStreaming()
โ”‚
โ””โ”€โ”€ POST /plugins/friend/voice/stop
โ”‚
โ–ผ
friendService.stopVoiceCapture()
โ”‚
โ”œโ”€โ”€ arecord.kill('SIGTERM') โ†’ 2s ๅŽ SIGKILL
โ”œโ”€โ”€ capturing = false
โ”œโ”€โ”€ clearMute()
โ”œโ”€โ”€ VAD reset()
โ”œโ”€โ”€ STT connection.finalize() + close()
โ”œโ”€โ”€ ๅ‘้€ๅ‰ฉไฝ™่ฝฌๅฝ•ๆ–‡ๆœฌ
โ””โ”€โ”€ ่ฟ”ๅ›žๅฎŒๆ•ด่ฝฌๅฝ•
```
---
## 3. ๆƒ…็ปช่กจๆƒ…ๆต
LLM ่ฐƒ็”จ `friend_emotion` ๅทฅๅ…ท่งฆๅ‘ๆƒ…็ปชๆ›ดๆ–ฐใ€‚
```
LLM ๅค„็†ๅฎŒๆˆ๏ผŒ่ฐƒ็”จ friend_emotion ๅทฅๅ…ท
โ”‚
โ–ผ
FriendEmotionTool.call({ emotion: 'happy', intensity: 0.8, mood_delta: 2 })
โ”‚
โ”œโ”€โ”€ broadcastToVrm({ emotion: 'happy', emotionIntensity: 0.8 })
โ”‚ โ”‚
โ”‚ โ–ผ SSE data
โ”‚ App.tsx handleVrmMessage
โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€ emotionActionMap['happy'] = 'happy'
โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€ sceneRef.current.setEmotionWithReset('happy', 5000, 0.8)
โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ–ผ
โ”‚ โ”‚ VRMScene.setEmotionWithReset (via forwardRef)
โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ””โ”€โ”€ EmoteController.setEmotionWithReset('happy', 5000, 0.8)
โ”‚ โ”‚ โ”œโ”€โ”€ setEmotion('happy', 0.8)
โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ่Žทๅ– happy ็š„ blend shapes: [{name:'happy', val:0.2}, {name:'aa', val:0.8}]
โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๅบ”็”จ intensity: aa = 0.8*0.8 = 0.64, happy = 0.2*0.8 = 0.16
โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ isTransitioning = true
โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ่ฎฐๅฝ•็›ฎๆ ‡ blendshape ๅ€ผ
โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ””โ”€โ”€ setTimeout(5000ms โ†’ setEmotion('neutral'))
โ”‚ โ”‚
โ”‚ โ””โ”€โ”€ sceneRef.current.playAction('happy')
โ”‚ โ”‚
โ”‚ โ–ผ
โ”‚ MotionController.playAction('happy')
โ”‚ โ”œโ”€โ”€ loadClip('happy.fbx')
โ”‚ โ”œโ”€โ”€ crossFadeTo(clip, 0.3s)
โ”‚ โ”œโ”€โ”€ LoopOnce + clampWhenFinished
โ”‚ โ””โ”€โ”€ ๅฎŒๆˆๅŽ crossFade ๅ›ž idle ๅŠจ็”ป
โ”‚
โ”œโ”€โ”€ ๅค„็† mood_delta
โ”‚ โ”œโ”€โ”€ ่ฏปๅ–ๅฝ“ๅ‰ moodIndex = 60
โ”‚ โ”œโ”€โ”€ newMood = clamp(60 + 2, 0, 100) = 62
โ”‚ โ”œโ”€โ”€ ๆŒไน…ๅŒ–ๅˆฐ prefs
โ”‚ โ””โ”€โ”€ broadcastToVrm({ moodDelta: 2, moodIndex: 62 })
โ”‚ โ”‚
โ”‚ โ–ผ SSE data
โ”‚ MoodIndicator ๆ”ถๅˆฐ
โ”‚ โ”œโ”€โ”€ ๆ˜พ็คบๅฟƒๆƒ…ๆ•ฐๅ€ผๅ˜ๅŒ–ๆฐ”ๆณก (+2)
โ”‚ โ”œโ”€โ”€ Canvas ๅŠจ็”ป: displayPercent ไปŽ 60 โ†’ 62 ๆธๅ˜
โ”‚ โ””โ”€โ”€ 5s ๅŽ่‡ชๅŠจ้š่—
โ”‚
โ””โ”€โ”€ ่ฟ”ๅ›ž tool result
```
### ๆฏๅธงๆ›ดๆ–ฐๅพช็Žฏ (VRMScene animate)
```
requestAnimationFrame ๅพช็Žฏ (็บฆ 60fps)
โ”‚
โ”œโ”€โ”€ 1. MotionController.update(delta)
โ”‚ โ””โ”€โ”€ AnimationMixer.update(delta)
โ”‚
โ”œโ”€โ”€ 2. ๅบ”็”จ Relaxed Hand Pose (้ž่ˆž่นˆ็Šถๆ€)
โ”‚ โ””โ”€โ”€ ๆ‰‹ๆŒ‡่‡ช็„ถๅผฏๆ›ฒ + ๅพฎๅฆ™้ขคๅŠจ
โ”‚
โ”œโ”€โ”€ 3. Humanoid.update()
โ”‚
โ”œโ”€โ”€ 4. ็œผ็ƒ่ฟฝ่ธช (camera ๆจกๅผ)
โ”‚ โ””โ”€โ”€ lookAtTarget = camera.position
โ”‚
โ”œโ”€โ”€ 5. LookAt.update(delta)
โ”‚
โ”œโ”€โ”€ 6. Eye Saccades Controller.update()
โ”‚ โ””โ”€โ”€ ๆฏ้š” 400-1200ms ๆทปๅŠ ้šๆœบ็œผ็ƒๅพฎๅŠจๅ็งป
โ”‚
โ”œโ”€โ”€ 7. Blink State Machine.update()
โ”‚ โ””โ”€โ”€ ้šๆœบ็œจ็œผ (้—ด้š” 1-6s, ๆ—ถ้•ฟ 150ms, sin ๆ›ฒ็บฟ)
โ”‚
โ”œโ”€โ”€ 8. EmoteController.update(delta)
โ”‚ โ””โ”€โ”€ cubic ease ่ฟ‡ๆธกๅˆฐ็›ฎๆ ‡ blendshape ๅ€ผ
โ”‚
โ”œโ”€โ”€ 9. LipSync.update(vrm, delta)
โ”‚ โ”œโ”€โ”€ ่ฏปๅ– wlipsync ้Ÿณ้ข‘ๅˆ†ๆž่Š‚็‚น็š„้Ÿณ็ด ๆƒ้‡
โ”‚ โ”œโ”€โ”€ ้€‰ๆ‹ฉ่ƒœ่€…/ไบšๅ†›้Ÿณ็ด 
โ”‚ โ”œโ”€โ”€ Attack/Release ๅนณๆป‘ (50/30)
โ”‚ โ””โ”€โ”€ ่ฎพ็ฝฎ VRM ๅ˜ดๅฝข blendshapes (aa, ee, ih, oh, ou)
โ”‚
โ”œโ”€โ”€ 10. ExpressionManager.update()
โ”‚
โ””โ”€โ”€ 11. SpringBoneManager.update(delta)
โ””โ”€โ”€ ็‰ฉ็†ๅคดๅ‘/่กฃๆœ/้ฅฐๅ“ๆจกๆ‹Ÿ
```
---
## 4. SSE ไบ‹ไปถๆ ผๅผ
ๆ‰€ๆœ‰ๅ‰็ซฏ SSE ไบ‹ไปถ้€š่ฟ‡ `GET /plugins/friend/events` ๆŽฅๆ”ถ๏ผŒๆ ผๅผไธบๆ ‡ๅ‡† SSE (`data: {json}\n\n`)ใ€‚
### 4.1 VrmBroadcastPayload ๅญ—ๆฎต่ฏดๆ˜Ž
| ๅญ—ๆฎต | ็ฑปๅž‹ | ๅฟ…ๅกซ | ่ฏดๆ˜Ž |
|------|------|------|------|
| `text` | string | ๅฆ | AI ๅ›žๅคๆ–‡ๅญ—๏ผŒTextBubble ๆ˜พ็คบๅนถๅฏๅŠจๆ‰“ๅญ—ๆœบๆ•ˆๆžœ |
| `emotion` | string | ๅฆ | VRM ่กจๆƒ…ๅ๏ผŒ่งฆๅ‘ EmoteController ๅˆ‡ๆข blend shapes |
| `emotionIntensity` | number | ๅฆ | ่กจๆƒ…ๅผบๅบฆ 0-1๏ผŒ้ป˜่ฎค 1 |
| `audioUrl` | string | ๅฆ | TTS ้Ÿณ้ข‘ URL๏ผŒTextBubble ่งฆๅ‘ LipSync ๆ’ญๆ”พ |
| `audioIndex` | number | ๅฆ | ้Ÿณ้ข‘ๆ’ญๆ”พ้กบๅบ็ดขๅผ•๏ผŒ็”จไบŽๅคšๅฅๆŽ’ๅบ |
| `clearText` | boolean | ๅฆ | ๆธ…็ฉบๆฐ”ๆณกๆ–‡ๅญ—ๅ’Œ้Ÿณ้ข‘้˜Ÿๅˆ— |
| `imageUrl` | string | ๅฆ | ๅ›พ็‰‡ URL๏ผŒๅœจๆฐ”ๆณกไธญๆ˜พ็คบ |
| `moodDelta` | number | ๅฆ | ๅฟƒๆƒ…ๅ˜ๅŒ–้‡๏ผŒMoodIndicator ๆ˜พ็คบๆตฎๅŠจๆฐ”ๆณก |
| `moodIndex` | number | ๅฆ | ๅฝ“ๅ‰ๅฟƒๆƒ…ๆŒ‡ๆ•ฐ 0-100๏ผŒMoodIndicator Canvas ๆ›ดๆ–ฐ |
| `sendFirstTts` | boolean | ๅฆ | ๅผ€ๅง‹ TTS ๆ’ญๆ”พ็š„ไฟกๅท๏ผŒ้‡็ฝฎ้Ÿณ้ข‘้˜Ÿๅˆ— |
| `appendText` | boolean | ๅฆ | ่ฟฝๅŠ ๆ–‡ๅญ—ๆจกๅผ๏ผŒๅŽ็ปญๅฅๅญ็š„ๆ–‡ๅญ—ๅ’Œ้Ÿณ้ข‘้…ๅฏน |
| `replyDone` | boolean | ๅฆ | ๅ›žๅคๅฎŒๆˆไฟกๅท๏ผŒTextBubble ่ฐƒๅบฆๆฐ”ๆณก้š่— |
### 4.2 ๅ…ธๅž‹ๅ›žๅคๅบๅˆ—
```
1. { text: "ไฝ ๅฅฝ๏ผไปŠๅคฉๅฟƒๆƒ…ไธ้”™ๅ•Š๏ผ", replyDone: true }
โ†’ ๆ˜พ็คบๆ–‡ๅญ—๏ผŒๆ‰“ๅญ—ๆœบๆ•ˆๆžœ๏ผŒ1s ๅŽ่งฆๅ‘่กจๆƒ…
2. { text: "ไธ€่ตท็Žฉๅง๏ผ" }
{ audioUrl: "http://127.0.0.1:3456/plugins/friend/audio/123-1", sendFirstTts: true }
{ replyDone: true }
โ†’ ๆ˜พ็คบๆ–‡ๅญ— + ๆ’ญๆ”พ TTS + ่ฏญ้Ÿณ็ป“ๆŸๅŽ้š่—ๆฐ”ๆณก
3. { text: "ไปŠๅคฉๅคฉๆฐ”็œŸๅฅฝใ€‚" }
{ audioUrl: "...", sendFirstTts: true, emotion: "happy", emotionIntensity: 0.8 }
{ appendText: true, text: "่ฆไธ่ฆๅ‡บๅŽป่ตฐ่ตฐ๏ผŸ", audioUrl: "...", audioIndex: 1 }
{ appendText: true, text: "ๆˆ‘็Ÿฅ้“ไธ€ไธชๅฅฝๅœฐๆ–นใ€‚", audioUrl: "...", audioIndex: 2 }
{ replyDone: true }
โ†’ ๅคšๅฅๅญๅ›žๅค๏ผŒๆฏๅฅ็‹ฌ็ซ‹้Ÿณ้ข‘๏ผŒๆŒ‰็ดขๅผ•้กบๅบๆ’ญๆ”พ
โ†’ ็ฌฌไธ€ๅฅๅ‘้€ๆ—ถ่งฆๅ‘ happy ่กจๆƒ…
4. { emotion: "think", emotionIntensity: 0.7 }
โ†’ ๆ€่€ƒ้˜ถๆฎต็š„่กจๆƒ…ๆ›ดๆ–ฐ๏ผˆLLM ๅค„็†ไธญ๏ผ‰
5. { clearText: true }
โ†’ ๆธ…็ฉบๆฐ”ๆณก๏ผˆๆ–ฐไผš่ฏ๏ผ‰
6. { moodDelta: 3, moodIndex: 63 }
โ†’ ๅฟƒๆƒ…ๆ›ดๆ–ฐ๏ผŒๆ˜พ็คบ +3 ๆตฎๅŠจๆฐ”ๆณก๏ผŒCanvas ๆถฒๆ€ๅกซๅ……ๅ˜ๅŒ–
```