Skip to content

Commit 0a1bcca

Browse files
committed
1.10.0
1 parent 5fb8b48 commit 0a1bcca

4 files changed

Lines changed: 139 additions & 65 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ android {
5555
applicationId = "com.ai.assistance.operit"
5656
minSdk = 26
5757
targetSdk = 34
58-
versionCode = 39
59-
versionName = "1.9.1+19"
58+
versionCode = 40
59+
versionName = "1.10.0"
6060

6161
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
6262
vectorDrawables {

app/src/main/java/com/ai/assistance/operit/api/chat/EnhancedAIService.kt

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import com.ai.assistance.operit.R
4545
import java.util.concurrent.ConcurrentHashMap
4646
import java.util.concurrent.atomic.AtomicBoolean
4747
import java.util.concurrent.atomic.AtomicInteger
48+
import kotlinx.coroutines.CancellationException
4849
import kotlinx.coroutines.CoroutineScope
4950
import kotlinx.coroutines.Dispatchers
5051
import kotlinx.coroutines.Job
@@ -346,13 +347,42 @@ class EnhancedAIService private constructor(private val context: Context) {
346347

347348
// Execution context for a single sendMessage call to achieve concurrency
348349
private data class MessageExecutionContext(
350+
val executionId: Int,
349351
val streamBuffer: StringBuilder = StringBuilder(),
350352
val roundManager: ConversationRoundManager = ConversationRoundManager(),
351353
val isConversationActive: AtomicBoolean = AtomicBoolean(true),
352354
val conversationHistory: MutableList<Pair<String, String>>,
353355
val eventChannel: MutableSharedStream<TextStreamEvent>,
354356
)
355357

358+
private val activeExecutionContexts = ConcurrentHashMap<Int, MessageExecutionContext>()
359+
private val nextExecutionContextId = AtomicInteger(0)
360+
361+
private fun registerExecutionContext(context: MessageExecutionContext) {
362+
activeExecutionContexts[context.executionId] = context
363+
}
364+
365+
private fun unregisterExecutionContext(context: MessageExecutionContext) {
366+
activeExecutionContexts.remove(context.executionId, context)
367+
}
368+
369+
private fun invalidateExecutionContext(context: MessageExecutionContext, reason: String) {
370+
if (context.isConversationActive.compareAndSet(true, false)) {
371+
AppLogger.d(TAG, "执行上下文已失效: id=${context.executionId}, reason=$reason")
372+
}
373+
}
374+
375+
private fun invalidateAllExecutionContexts(reason: String) {
376+
activeExecutionContexts.values.forEach { context ->
377+
invalidateExecutionContext(context, reason)
378+
}
379+
}
380+
381+
private fun isExecutionContextActive(context: MessageExecutionContext): Boolean {
382+
return context.isConversationActive.get() &&
383+
activeExecutionContexts[context.executionId] === context
384+
}
385+
356386
private suspend fun startAssistantResponseRound(context: MessageExecutionContext) {
357387
context.roundManager.startNewRound()
358388
context.streamBuffer.clear()
@@ -551,9 +581,11 @@ class EnhancedAIService private constructor(private val context: Context) {
551581
val wrappedStream = stream {
552582
val execContext =
553583
MessageExecutionContext(
584+
executionId = nextExecutionContextId.incrementAndGet(),
554585
conversationHistory = chatHistory.toMutableList(),
555586
eventChannel = eventChannel
556587
)
588+
registerExecutionContext(execContext)
557589
var hadFatalError = false
558590
try {
559591
// 确保所有操作都在IO线程上执行
@@ -808,16 +840,18 @@ class EnhancedAIService private constructor(private val context: Context) {
808840
"流收集完成,总计 $totalChars 字符,耗时: ${System.currentTimeMillis() - streamStartTime}ms"
809841
)
810842
}
843+
} catch (e: CancellationException) {
844+
invalidateExecutionContext(execContext, "sendMessage.collect.cancelled")
845+
AppLogger.d(TAG, "sendMessage流被取消")
846+
throw e
811847
} catch (e: Exception) {
812-
// 对于协程取消异常,这是正常流程,应当向上抛出以停止流
813-
if (e is kotlinx.coroutines.CancellationException) {
814-
AppLogger.d(TAG, "sendMessage流被取消")
815-
throw e
816-
}
817-
818848
// 用户取消导致的 Socket closed 是预期行为,不应作为错误处理
819849
if (e.message?.contains("Socket closed", ignoreCase = true) == true) {
820-
AppLogger.d(TAG, "Stream was cancelled by the user (Socket closed).")
850+
if (isExecutionContextActive(execContext)) {
851+
AppLogger.d(TAG, "Stream was cancelled by the user (Socket closed).")
852+
} else {
853+
AppLogger.d(TAG, "Stream closed after execution context was invalidated.")
854+
}
821855
} else {
822856
hadFatalError = true
823857
// Handle any exceptions
@@ -833,32 +867,41 @@ class EnhancedAIService private constructor(private val context: Context) {
833867
if (!isSubTask) stopAiService()
834868
}
835869
} finally {
836-
// 确保流处理完成后调用
837-
if (!hadFatalError) {
838-
val collector = this
839-
withContext(Dispatchers.IO) {
840-
processStreamCompletion(
841-
execContext,
842-
functionType,
843-
collector,
844-
enableThinking,
845-
enableMemoryQuery,
846-
onNonFatalError,
847-
onTokenLimitExceeded,
848-
maxTokens,
849-
tokenUsageThreshold,
850-
isSubTask,
851-
characterName,
852-
avatarUri,
853-
roleCardId,
854-
chatId,
855-
onToolInvocation,
856-
chatModelConfigIdOverride,
857-
chatModelIndexOverride,
858-
stream,
859-
enableGroupOrchestrationHint
870+
try {
871+
// 确保流处理完成后调用;如果本轮已被取消,则不能再继续跑完成逻辑。
872+
if (!hadFatalError && isExecutionContextActive(execContext)) {
873+
val collector = this
874+
withContext(Dispatchers.IO) {
875+
processStreamCompletion(
876+
execContext,
877+
functionType,
878+
collector,
879+
enableThinking,
880+
enableMemoryQuery,
881+
onNonFatalError,
882+
onTokenLimitExceeded,
883+
maxTokens,
884+
tokenUsageThreshold,
885+
isSubTask,
886+
characterName,
887+
avatarUri,
888+
roleCardId,
889+
chatId,
890+
onToolInvocation,
891+
chatModelConfigIdOverride,
892+
chatModelIndexOverride,
893+
stream,
894+
enableGroupOrchestrationHint
895+
)
896+
}
897+
} else if (!hadFatalError) {
898+
AppLogger.d(
899+
TAG,
900+
"跳过流完成处理:执行上下文已失效, id=${execContext.executionId}"
860901
)
861902
}
903+
} finally {
904+
unregisterExecutionContext(execContext)
862905
}
863906
}
864907
}
@@ -1956,6 +1999,9 @@ class EnhancedAIService private constructor(private val context: Context) {
19561999
stream,
19572000
enableGroupOrchestrationHint
19582001
)
2002+
} catch (e: CancellationException) {
2003+
AppLogger.d(TAG, "处理工具执行结果被取消")
2004+
throw e
19592005
} catch (e: Exception) {
19602006
AppLogger.e(TAG, "处理工具执行结果时出错", e)
19612007
withContext(Dispatchers.Main) {
@@ -2156,6 +2202,8 @@ class EnhancedAIService private constructor(private val context: Context) {
21562202

21572203
/** Cancel the current conversation */
21582204
fun cancelConversation() {
2205+
invalidateAllExecutionContexts("cancelConversation")
2206+
21592207
// Set conversation inactive
21602208
// isConversationActive.set(false) // This is now per-context, can't set a global one
21612209

app/src/main/java/com/ai/assistance/operit/services/core/MessageProcessingDelegate.kt

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -233,24 +233,50 @@ class MessageProcessingDelegate(
233233
return chatRuntimes[chatKey(chatId)]?.responseStream
234234
}
235235

236+
private fun resolveFinalContent(aiMessage: ChatMessage): String {
237+
val sharedStream = aiMessage.contentStream as? SharedStream<String>
238+
val replayChunks = sharedStream?.replayCache
239+
val eventCarrier = aiMessage.contentStream as? TextStreamEventCarrier
240+
241+
return if (eventCarrier?.eventChannel?.replayCache?.isNotEmpty() == true) {
242+
aiMessage.content
243+
} else if (!replayChunks.isNullOrEmpty()) {
244+
replayChunks.joinToString(separator = "")
245+
} else {
246+
aiMessage.content
247+
}
248+
}
249+
250+
private suspend fun detachStreamingAiMessage(chatId: String) {
251+
val streamingMessage =
252+
getChatHistory(chatId).lastOrNull { it.sender == "ai" && it.contentStream != null }
253+
?: return
254+
val finalContent = resolveFinalContent(streamingMessage)
255+
streamingMessage.content = finalContent
256+
val finalMessage = streamingMessage.copy(content = finalContent, contentStream = null)
257+
withContext(Dispatchers.Main) {
258+
addMessageToChat(chatId, finalMessage)
259+
}
260+
}
261+
236262
fun cancelMessage(chatId: String) {
237263
coroutineScope.launch {
238-
setChatInputProcessingState(chatId, EnhancedInputProcessingState.Idle)
239-
240264
val chatRuntime = runtimeFor(chatId)
241-
chatRuntime.streamCollectionJob?.cancel()
242-
chatRuntime.streamCollectionJob = null
243265
chatRuntime.stateCollectionJob?.cancel()
244266
chatRuntime.stateCollectionJob = null
245-
chatRuntime.isLoading.value = false
267+
chatRuntime.streamCollectionJob?.cancel()
268+
chatRuntime.streamCollectionJob = null
269+
clearCurrentTurnToolInvocationCount(chatId)
270+
271+
AIMessageManager.cancelOperation(chatId)
272+
detachStreamingAiMessage(chatId)
273+
246274
chatRuntime.responseStream = null
275+
chatRuntime.isLoading.value = false
247276
updateGlobalLoadingState()
248-
clearCurrentTurnToolInvocationCount(chatId)
277+
setChatInputProcessingState(chatId, EnhancedInputProcessingState.Idle)
249278

250-
withContext(Dispatchers.IO) {
251-
AIMessageManager.cancelOperation(chatId)
252-
saveCurrentChat()
253-
}
279+
withContext(Dispatchers.IO) { saveCurrentChat() }
254280
}
255281
}
256282

@@ -935,18 +961,8 @@ class MessageProcessingDelegate(
935961
// 修改为使用 try-catch 来检查变量是否已初始化,而不是使用 ::var.isInitialized
936962
try {
937963
val aiMessage = aiMessageProvider()
938-
val sharedStream = aiMessage.contentStream as? SharedStream<String>
939-
val replayChunks = sharedStream?.replayCache
940-
val eventCarrier = aiMessage.contentStream as? TextStreamEventCarrier
941964
// 优先使用共享流的全量重放缓存重建最终文本,避免完成信号早于收集协程处理尾部字符时丢字。
942-
val finalContent =
943-
if (eventCarrier?.eventChannel?.replayCache?.isNotEmpty() == true) {
944-
aiMessage.content
945-
} else if (!replayChunks.isNullOrEmpty()) {
946-
replayChunks.joinToString(separator = "")
947-
} else {
948-
aiMessage.content
949-
}
965+
val finalContent = resolveFinalContent(aiMessage)
950966
aiMessage.content = finalContent
951967

952968
var deferTurnCompleteToAsyncJob = false
@@ -1080,7 +1096,7 @@ class MessageProcessingDelegate(
10801096
try {
10811097
val aiMessage = aiMessageProvider()
10821098
val finalContent = aiMessage.content
1083-
val finalMessage = aiMessage.copy(content = finalContent)
1099+
val finalMessage = aiMessage.copy(content = finalContent, contentStream = null)
10841100
withContext(Dispatchers.Main) {
10851101
if (chatId != null) {
10861102
addMessageToChat(chatId, finalMessage)

app/src/main/java/com/ai/assistance/operit/ui/features/chat/components/part/CustomXmlRenderer.kt

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,25 +210,35 @@ class CustomXmlRenderer(
210210
/** 从XML内容中提取纯文本内容 */
211211
private fun extractContentFromXml(content: String, tagName: String? = null): String {
212212
val rawTagName = extractRawTagName(content) ?: return content
213+
val normalizedRawTagName = ChatMarkupRegex.normalizeToolLikeTagName(rawTagName)
213214
val effectiveTagName =
214-
if (tagName != null && ChatMarkupRegex.normalizeToolLikeTagName(rawTagName) != tagName) {
215+
if (tagName != null && normalizedRawTagName != tagName) {
215216
tagName
216217
} else {
217218
rawTagName
218219
}
219220
val startTag = "<$effectiveTagName"
220-
val endTag = "</$effectiveTagName>"
221+
val startTagIndex = content.indexOf(startTag)
222+
if (startTagIndex < 0) {
223+
return content
224+
}
221225

222-
// 找到开始标签的结束位置
223-
val startTagEnd = content.indexOf('>', content.indexOf(startTag)) + 1
226+
// 起始标签本身必须完整;如果连 '>' 都没有,保留原始内容以等待后续片段。
227+
val startTagEnd = content.indexOf('>', startTagIndex)
228+
if (startTagEnd < 0) {
229+
return content
230+
}
231+
232+
val endTag = "</$effectiveTagName>"
224233
val endIndex = content.lastIndexOf(endTag)
234+
val contentEndExclusive =
235+
if (endIndex > startTagEnd) {
236+
endIndex
237+
} else {
238+
content.length
239+
}
225240

226-
return if (startTagEnd > 0 && endIndex > startTagEnd) {
227-
content.substring(startTagEnd, endIndex).trim()
228-
} else {
229-
// 如果无法正确提取,返回原始内容
230-
content
231-
}
241+
return content.substring(startTagEnd + 1, contentEndExclusive).trim()
232242
}
233243

234244
/** 从工具调用XML提取参数内容 */

0 commit comments

Comments
 (0)