Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ sealed class MessageSendStreamStartResult {
data class Failed(val result: ToolResult) : MessageSendStreamStartResult()
}

/**
* 定向发送时,决定悬浮窗应跟随哪个对话显示。
*
* FLOATING runtime 的 currentChatId 由 ChatRuntimeHolder 从主界面同步而来,只用 chatIdOverride
* 发送会让消息进到目标对话、窗口却停在前台对话上。MAIN runtime 不做切换,否则后台发送会把用户
* 正在看的对话顶掉。
*/
internal object FloatingDisplayChatSelector {
fun select(
runtimeSlot: ChatRuntimeSlot,
targetChatId: String?,
displayedChatId: String?
): String? {
if (runtimeSlot != ChatRuntimeSlot.FLOATING) return null
if (targetChatId.isNullOrBlank()) return null
if (targetChatId == displayedChatId) return null
return targetChatId
}
}

/**
* 对话管理工具
* 负责管理对话、浮窗服务,以及按指定 runtime 发送消息
Expand Down Expand Up @@ -1733,7 +1753,8 @@ class StandardChatManagerTool(private val context: Context) {
)
}

val core = chatRuntimeHolder.getCore(runtimeSlot ?: ChatRuntimeSlot.FLOATING)
val effectiveSlot = runtimeSlot ?: ChatRuntimeSlot.FLOATING
val core = chatRuntimeHolder.getCore(effectiveSlot)

val message = tool.parameters.find { it.name == "message" }?.value
if (message.isNullOrBlank()) {
Expand Down Expand Up @@ -1892,7 +1913,18 @@ class StandardChatManagerTool(private val context: Context) {
}

if (hasTargetChat) {
// 后台发送到指定对话,不切换 UI
// 悬浮窗跟随目标对话:chatIdOverride 只路由消息,FLOATING runtime 的
// currentChatId 仍停在前台对话上,窗口会显示错误的会话(#555)。
// switchChatLocal 只改悬浮窗本地状态,不写回全局,主界面不受影响。
FloatingDisplayChatSelector.select(
runtimeSlot = effectiveSlot,
targetChatId = targetChatId,
displayedChatId = core.currentChatId.value
)?.let { chatToFollow ->
core.switchChatLocal(chatToFollow)
}

// 后台发送到指定对话,不切换主界面
core.sendUserMessage(
promptFunctionType = PromptFunctionType.CHAT,
roleCardIdOverride = roleCardId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.ai.assistance.operit.core.tools.defaultTool.standard

import com.ai.assistance.operit.api.chat.ChatRuntimeSlot
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

class FloatingDisplayChatSelectorTest {

@Test
fun select_floatingRuntimeFollowsWorkflowTargetChatInsteadOfForegroundChat() {
// #555: a workflow sends to chat B while the user is looking at chat A.
val result =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.FLOATING,
targetChatId = "chat-b",
displayedChatId = "chat-a"
)

assertEquals("chat-b", result)
}

@Test
fun select_floatingRuntimeFollowsTargetWhenNothingIsDisplayedYet() {
val result =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.FLOATING,
targetChatId = "chat-b",
displayedChatId = null
)

assertEquals("chat-b", result)
}

@Test
fun select_mainRuntimeNeverSwitchesAwayFromForegroundChat() {
val result =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.MAIN,
targetChatId = "chat-b",
displayedChatId = "chat-a"
)

assertNull(result)
}

@Test
fun select_returnsNullWhenTargetAlreadyDisplayed() {
val result =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.FLOATING,
targetChatId = "chat-a",
displayedChatId = "chat-a"
)

assertNull(result)
}

@Test
fun select_returnsNullWhenNoTargetChatIsGiven() {
val floating =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.FLOATING,
targetChatId = null,
displayedChatId = "chat-a"
)
val main =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.MAIN,
targetChatId = null,
displayedChatId = "chat-a"
)

assertNull(floating)
assertNull(main)
}

@Test
fun select_returnsNullWhenTargetChatIsBlank() {
val blank =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.FLOATING,
targetChatId = " ",
displayedChatId = "chat-a"
)
val empty =
FloatingDisplayChatSelector.select(
runtimeSlot = ChatRuntimeSlot.FLOATING,
targetChatId = "",
displayedChatId = "chat-a"
)

assertNull(blank)
assertNull(empty)
}
}
1 change: 1 addition & 0 deletions docs/doc-src/package-dev/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ findChat({ query, match?, index? }): Promise<ChatFindResultData>
其中:

- `runtime` 用于指定本次消息发送到哪个 chat runtime,未指定时默认走 `floating`
- 传入 `chatId` 时消息只投递到该对话,走 `floating` 时悬浮窗会一并切到该对话显示,走 `main` 时不切换用户正在看的对话
- `timeout_ms` 用于控制本次发送的最长等待时间,单位毫秒

### `call(options)`
Expand Down