Compare commits

..

No commits in common. "62cb78167d84a75567634ecf95cd1be7d404f169" and "4b2bf8f5f48decc9c6404af7ca90f52ea741b445" have entirely different histories.

12 changed files with 53 additions and 192 deletions

View File

@ -5,10 +5,7 @@ import android.util.Log;
import timber.log.Timber; import timber.log.Timber;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -20,13 +17,11 @@ public class CallBackResolution {
private static String TAG = "PTT_RX"; private static String TAG = "PTT_RX";
private static String indexGroupId = "";//用户所在群组Id private static String indexGroupId = "";//用户所在群组Id
private static String indexGroupName = "";//用户所在群组名称 private static String indexGroupName = "";//用户所在群组名称
private static final int MAX_GROUP_MEMBERS = 500; private static List<GroupMemberInfoDto> memberInfoDtos = new ArrayList<>();
private static final int MAX_GROUP_INFOS = 100;
private static final Map<String, GroupMemberInfoDto> memberInfoDtos = new LinkedHashMap<>(MAX_GROUP_MEMBERS);
private static int speckType = 1; private static int speckType = 1;
private static String talkId = ""; private static String talkId = "";
private static String talkName = ""; private static String talkName = "";
private static final Map<String, GroupInfoDto> groupInfos = new LinkedHashMap<>(MAX_GROUP_INFOS); private static ArrayList<GroupInfoDto> groupInfos = new ArrayList<>();
private static String oldLoginSate = "00"; private static String oldLoginSate = "00";
private static int logNum = 0; private static int logNum = 0;
private static long offlineTime = 0; private static long offlineTime = 0;
@ -145,7 +140,7 @@ public class CallBackResolution {
Timber.tag(TAG).d( "at_OEM_AT_cb group info groupId: " + groupId + ", groupNo: " + groupNo+", groupCount: " +groupCount); Timber.tag(TAG).d( "at_OEM_AT_cb group info groupId: " + groupId + ", groupNo: " + groupNo+", groupCount: " +groupCount);
if (1 == groupNo) groupInfos.clear(); if (1 == groupNo) groupInfos.clear();
GroupInfoDto groupInfo = new GroupInfoDto(groupId, tempTxt, groupNo, groupCount); GroupInfoDto groupInfo = new GroupInfoDto(groupId, tempTxt, groupNo, groupCount);
groupInfos.put(groupId, groupInfo); groupInfos.add(groupInfo);
} }
break; break;
@ -174,7 +169,7 @@ public class CallBackResolution {
String tempTxt = unicodeToString(groupMemberName.toString()); String tempTxt = unicodeToString(groupMemberName.toString());
GroupMemberInfoDto infoDto = new GroupMemberInfoDto(memberId, tempTxt, status, memberNo, haveVideo); GroupMemberInfoDto infoDto = new GroupMemberInfoDto(memberId, tempTxt, status, memberNo, haveVideo);
if (memberNo == 1) memberInfoDtos.clear(); if (memberNo == 1) memberInfoDtos.clear();
memberInfoDtos.put(memberId, infoDto); memberInfoDtos.add(infoDto);
} }
break; break;

View File

@ -1,14 +1,9 @@
package com.example.kingway.ptt; package com.example.kingway.ptt;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import timber.log.Timber; import timber.log.Timber;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**指令返回类 /**指令返回类
* 说明操作UI请在主线程进行 * 说明操作UI请在主线程进行
@ -16,45 +11,6 @@ import java.util.concurrent.ConcurrentHashMap;
public class CallBackUtil { public class CallBackUtil {
private static String TAG = "TYT_CallBackUtil"; private static String TAG = "TYT_CallBackUtil";
private static final long THROTTLE_INTERVAL_MS = 100L;
private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
private static final ConcurrentHashMap<String, Long> lastPushAt = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Runnable> pendingRunnables = new ConcurrentHashMap<>();
private static void pushJsThrottled(String eventName, String script) {
long now = SystemClock.uptimeMillis();
Long last = lastPushAt.get(eventName);
if (last == null || now - last >= THROTTLE_INTERVAL_MS) {
lastPushAt.put(eventName, now);
cancelPending(eventName);
com.stand.standapp.MainActivity.executeJs(script);
return;
}
Runnable prev = pendingRunnables.get(eventName);
if (prev != null) {
MAIN_HANDLER.removeCallbacks(prev);
}
final long scheduledAt = now;
Runnable task = () -> {
lastPushAt.put(eventName, SystemClock.uptimeMillis());
pendingRunnables.remove(eventName);
com.stand.standapp.MainActivity.executeJs(script);
};
pendingRunnables.put(eventName, task);
MAIN_HANDLER.postDelayed(task, THROTTLE_INTERVAL_MS - (now - last));
}
private static void cancelPending(String eventName) {
Runnable r = pendingRunnables.remove(eventName);
if (r != null) {
MAIN_HANDLER.removeCallbacks(r);
}
}
private static void pushJsImmediate(String script) {
com.stand.standapp.MainActivity.executeJs(script);
}
/** /**
* 返回登录 * 返回登录
@ -112,11 +68,11 @@ public class CallBackUtil {
* 查询群组成员返回 * 查询群组成员返回
* state true-成功 false-失败 * state true-成功 false-失败
*/ */
public static void callBackOnMemberSuccess(boolean state, java.util.Map<String, GroupMemberInfoDto> dtos) { public static void callBackOnMemberSuccess(boolean state, List<GroupMemberInfoDto> dtos) {
try { try {
org.json.JSONArray arr = new org.json.JSONArray(); org.json.JSONArray arr = new org.json.JSONArray();
if (dtos != null && state) { if (dtos != null && state) {
for (GroupMemberInfoDto dto : dtos.values()) { for (GroupMemberInfoDto dto : dtos) {
org.json.JSONObject obj = new org.json.JSONObject(); org.json.JSONObject obj = new org.json.JSONObject();
obj.put("memberId", dto.getMemberId()); obj.put("memberId", dto.getMemberId());
obj.put("memberName", dto.getGmemberName()); obj.put("memberName", dto.getGmemberName());
@ -124,7 +80,7 @@ public class CallBackUtil {
arr.put(obj); arr.put(obj);
} }
} }
pushJsThrottled("MemberList", "if(window.onPttEvent){window.onPttEvent('MemberList', " + arr.toString() + ");}"); com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('MemberList', " + arr.toString() + ");}");
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); } } catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
} }
@ -172,17 +128,17 @@ public class CallBackUtil {
* 返回群组集合信息 * 返回群组集合信息
* GroupInfoDto 群组信息 * GroupInfoDto 群组信息
* */ * */
public static void callBackGroupInfo (java.util.Map<String, GroupInfoDto> groupInfoDtos) { public static void callBackGroupInfo (ArrayList < GroupInfoDto > groupInfoDtos) {
try { try {
org.json.JSONArray arr = new org.json.JSONArray(); org.json.JSONArray arr = new org.json.JSONArray();
for (GroupInfoDto dto : groupInfoDtos.values()) { for (GroupInfoDto dto : groupInfoDtos) {
org.json.JSONObject obj = new org.json.JSONObject(); org.json.JSONObject obj = new org.json.JSONObject();
obj.put("groupId", dto.getGroupId()); obj.put("groupId", dto.getGroupId());
obj.put("groupName", dto.getGroupName()); obj.put("groupName", dto.getGroupName());
obj.put("groupNo", dto.getGroupNo()); obj.put("groupNo", dto.getGroupNo());
arr.put(obj); arr.put(obj);
} }
pushJsThrottled("GroupList", "if(window.onPttEvent){window.onPttEvent('GroupList', " + arr.toString() + ");}"); com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('GroupList', " + arr.toString() + ");}");
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); } } catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
} }
@ -201,7 +157,7 @@ public class CallBackUtil {
obj.put("groupName", groupName); obj.put("groupName", groupName);
obj.put("talkId", talkId); obj.put("talkId", talkId);
obj.put("talkName", talkName); obj.put("talkName", talkName);
pushJsThrottled("SpeakerUpdate", "if(window.onPttEvent){window.onPttEvent('SpeakerUpdate', " + obj.toString() + ");}"); com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('SpeakerUpdate', " + obj.toString() + ");}");
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); } } catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
} }
@ -215,7 +171,7 @@ public class CallBackUtil {
obj.put("groupName", groupName); obj.put("groupName", groupName);
obj.put("talkId", talkId); obj.put("talkId", talkId);
obj.put("talkName", talkName); obj.put("talkName", talkName);
pushJsThrottled("PlayStatus", "if(window.onPttEvent){window.onPttEvent('PlayStatus', " + obj.toString() + ");}"); com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('PlayStatus', " + obj.toString() + ");}");
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); } } catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
} }
@ -255,7 +211,7 @@ public class CallBackUtil {
obj.put("latitude", latitude); obj.put("latitude", latitude);
obj.put("longitude", longitude); obj.put("longitude", longitude);
obj.put("time", time); obj.put("time", time);
pushJsThrottled("LocationStatus", "if(window.onPttEvent){window.onPttEvent('LocationStatus', " + obj.toString() + ");}"); com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('LocationStatus', " + obj.toString() + ");}");
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); } } catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
} }

View File

@ -9,7 +9,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.hjq.permissions.Permission import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
import com.stand.standapp.net.NetworkModule
import com.stand.standapp.printer.PrinterManager import com.stand.standapp.printer.PrinterManager
import okhttp3.* import okhttp3.*
import org.json.JSONObject import org.json.JSONObject
@ -18,7 +17,7 @@ import java.io.IOException
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private val client = NetworkModule.defaultClient private val client = OkHttpClient()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -489,19 +489,10 @@ class MainActivity : AppCompatActivity() {
geckoView.releaseSession() geckoView.releaseSession()
geckoSession?.close() geckoSession?.close()
geckoSession = null geckoSession = null
clearCompanionState()
instance = null instance = null
super.onDestroy() super.onDestroy()
} }
private fun clearCompanionState() {
synchronized(jsLock) {
pendingJsCode = null
}
pendingDialog = null
isConflictDialogShowing = false
}
private fun startTimeoutTimer() { private fun startTimeoutTimer() {
cancelTimeoutTimer() cancelTimeoutTimer()
mHandler.postDelayed(timeoutRunnable, TIMEOUT_MS) mHandler.postDelayed(timeoutRunnable, TIMEOUT_MS)

View File

@ -114,9 +114,11 @@ class TempCallActivity : AppCompatActivity() {
pulseAnimator?.cancel() pulseAnimator?.cancel()
if (instance == this) { if (instance == this) {
instance = null instance = null
// 如果不是最小化状态,说明是真正结束,清除所有状态
if (!isMinimized) { if (!isMinimized) {
currentGroupName = "临时会话" currentGroupName = "临时会话"
} }
// 通知 web 页面状态变化
MainActivity.executeJs("onTempCallStateChanged('" + if (isMinimized) "minimized" else "destroyed" + "')") MainActivity.executeJs("onTempCallStateChanged('" + if (isMinimized) "minimized" else "destroyed" + "')")
} }
} }

View File

@ -15,7 +15,6 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.stand.standapp.net.NetworkModule
import okhttp3.* import okhttp3.*
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
@ -59,7 +58,7 @@ class UpdateManager(private val context: Context) {
val url = "$serverUrl/api/app-version/check-update?versionCode=$currentVersionCode" val url = "$serverUrl/api/app-version/check-update?versionCode=$currentVersionCode"
val client = NetworkModule.defaultClient val client = OkHttpClient()
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
@ -140,7 +139,7 @@ class UpdateManager(private val context: Context) {
private fun startManualDownload(url: String) { private fun startManualDownload(url: String) {
showProgressDialog() showProgressDialog()
val client = NetworkModule.defaultClient val client = OkHttpClient()
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.addHeader("User-Agent", "Mozilla/5.0 (Android)") .addHeader("User-Agent", "Mozilla/5.0 (Android)")

View File

@ -1,25 +0,0 @@
package com.stand.standapp.net
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
object NetworkModule {
private const val DEFAULT_CONNECT_TIMEOUT_SEC = 15L
private const val DEFAULT_READ_TIMEOUT_SEC = 30L
private const val DEFAULT_WRITE_TIMEOUT_SEC = 30L
val defaultClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(DEFAULT_CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS)
.readTimeout(DEFAULT_READ_TIMEOUT_SEC, TimeUnit.SECONDS)
.writeTimeout(DEFAULT_WRITE_TIMEOUT_SEC, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
}
fun streamingClient(): OkHttpClient {
return defaultClient.newBuilder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.build()
}
}

View File

@ -13,16 +13,11 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.util.UUID import java.util.UUID
private const val STREAM_FLUSH_INTERVAL_MS = 50L
class ChatViewModel(application: Application) : AndroidViewModel(application) { class ChatViewModel(application: Application) : AndroidViewModel(application) {
private val repository = AiChatRepository() private val repository = AiChatRepository()
@ -35,49 +30,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList()) private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow() val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow()
private val _streamingContent = MutableStateFlow<Map<String, String>>(emptyMap()) // 👈 声明一个正在加载的状态,保证上一个问题没结束前无法再次发送提问!
val streamingContent: StateFlow<Map<String, String>> = _streamingContent.asStateFlow()
private val _isLoading = MutableStateFlow(false) private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow() val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// 👈 额外声明一个当前协程控制作业,用于物理取消/中止 SSE 对话!
private var activeChatJob: kotlinx.coroutines.Job? = null private var activeChatJob: kotlinx.coroutines.Job? = null
private val prefs = application.getSharedPreferences("ai_chat_cache", Context.MODE_PRIVATE) private val prefs = application.getSharedPreferences("ai_chat_cache", Context.MODE_PRIVATE)
private val chunkBuffer = java.util.concurrent.ConcurrentHashMap<String, StringBuilder>()
private val lastFlushAt = java.util.concurrent.ConcurrentHashMap<String, Long>()
private val flushScope = kotlinx.coroutines.CoroutineScope(
kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.Main.immediate
)
private val flushMutex = kotlinx.coroutines.sync.Mutex()
private fun scheduleFlush(messageId: String) {
val now = android.os.SystemClock.uptimeMillis()
val last = lastFlushAt[messageId] ?: 0
val delta = now - last
if (delta >= STREAM_FLUSH_INTERVAL_MS) {
flushStreamingMessage(messageId)
} else {
flushScope.launch {
kotlinx.coroutines.delay(STREAM_FLUSH_INTERVAL_MS - delta)
flushStreamingMessage(messageId)
}
}
}
private fun flushStreamingMessage(messageId: String) {
flushScope.launch {
flushMutex.withLock {
val buffer = chunkBuffer.remove(messageId) ?: return@withLock
val pending = buffer.toString()
if (pending.isEmpty()) return@withLock
_streamingContent.update { current ->
current + (messageId to (current[messageId].orEmpty() + pending))
}
lastFlushAt[messageId] = android.os.SystemClock.uptimeMillis()
}
}
}
init { init {
// 👈 将数据加载移到IO线程避免阻塞主线程导致UI卡顿 // 👈 将数据加载移到IO线程避免阻塞主线程导致UI卡顿
@ -172,21 +132,18 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 👈 用户手动中断当前正在生成的 AI 回答的方法! // 👈 用户手动中断当前正在生成的 AI 回答的方法!
fun cancelActiveStreaming() { fun cancelActiveStreaming() {
activeChatJob?.cancel() activeChatJob?.cancel() // 物理取消协程作业,断开 SSE OkHttp 连接并关闭流!
activeChatJob = null activeChatJob = null
// 将当前正在流式输出的 AI 消息状态重置为完成,防止气泡卡死在 streaming 样式
_messages.value = _messages.value.map { msg -> _messages.value = _messages.value.map { msg ->
if (msg.isStreaming) { if (msg.isStreaming) {
val finalContent = _streamingContent.value[msg.id] ?: msg.content msg.copy(isStreaming = false)
msg.copy(content = finalContent, isStreaming = false)
} else msg } else msg
} }
_streamingContent.value = emptyMap()
chunkBuffer.clear()
lastFlushAt.clear()
_isLoading.value = false _isLoading.value = false // 释放锁,允许用户立刻开始下一次提问!
saveCacheToLocal() saveCacheToLocal() // 强制存盘归档
} }
fun createNewSession() { fun createNewSession() {
@ -275,21 +232,20 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
private fun appendAiMessageChunk(messageId: String, chunk: String) { private fun appendAiMessageChunk(messageId: String, chunk: String) {
chunkBuffer.computeIfAbsent(messageId) { StringBuilder() }.append(chunk) _messages.value = _messages.value.map { msg ->
scheduleFlush(messageId) if (msg.id == messageId) {
msg.copy(content = msg.content + chunk)
} else msg
}
} }
private fun finalizeAiMessage(messageId: String) { private fun finalizeAiMessage(messageId: String) {
val finalResidue = chunkBuffer.remove(messageId)?.toString() ?: ""
_messages.value = _messages.value.map { msg -> _messages.value = _messages.value.map { msg ->
if (msg.id == messageId) { if (msg.id == messageId) {
val finalContent = (_streamingContent.value[messageId] ?: msg.content) + finalResidue msg.copy(isStreaming = false)
_streamingContent.update { it - messageId }
msg.copy(content = finalContent, isStreaming = false)
} else msg } else msg
} }
lastFlushAt.remove(messageId) saveCacheToLocal() // 👈 AI 回复完毕后,状态变更为非 streaming 并归档存盘
saveCacheToLocal()
} }
private fun updateSessionTitleIfFirstMessage(sessionId: String, content: String) { private fun updateSessionTitleIfFirstMessage(sessionId: String, content: String) {

View File

@ -162,18 +162,6 @@ fun ChatArea(
@Composable @Composable
fun MarkwonRenderer(markdown: String, modifier: Modifier) { fun MarkwonRenderer(markdown: String, modifier: Modifier) {
val context = androidx.compose.ui.platform.LocalContext.current
val markwon = remember(context) {
io.noties.markwon.Markwon.builder(context)
.usePlugin(io.noties.markwon.core.CorePlugin.create())
.usePlugin(io.noties.markwon.ext.tables.TablePlugin.create(context))
.usePlugin(io.noties.markwon.html.HtmlPlugin.create())
.usePlugin(io.noties.markwon.linkify.LinkifyPlugin.create())
.usePlugin(io.noties.markwon.ext.strikethrough.StrikethroughPlugin.create())
.usePlugin(io.noties.markwon.ext.tasklist.TaskListPlugin.create(context))
.build()
}
androidx.compose.ui.viewinterop.AndroidView( androidx.compose.ui.viewinterop.AndroidView(
factory = { ctx -> factory = { ctx ->
val textView = SafeSelectableTextView(ctx).apply { val textView = SafeSelectableTextView(ctx).apply {
@ -185,14 +173,23 @@ fun MarkwonRenderer(markdown: String, modifier: Modifier) {
setPadding(0, 0, 0, 0) setPadding(0, 0, 0, 0)
} }
val markwon = io.noties.markwon.Markwon.builder(ctx)
.usePlugin(io.noties.markwon.core.CorePlugin.create())
.usePlugin(io.noties.markwon.ext.tables.TablePlugin.create(ctx))
.usePlugin(io.noties.markwon.html.HtmlPlugin.create())
.usePlugin(io.noties.markwon.linkify.LinkifyPlugin.create())
.usePlugin(io.noties.markwon.ext.strikethrough.StrikethroughPlugin.create())
.usePlugin(io.noties.markwon.ext.tasklist.TaskListPlugin.create(ctx))
.build()
textView.tag = markwon textView.tag = markwon
textView textView
}, },
update = { view -> update = { view ->
try { try {
val currentMarkwon = view.tag as? io.noties.markwon.Markwon val markwon = view.tag as? io.noties.markwon.Markwon
if (currentMarkwon != null) { if (markwon != null) {
currentMarkwon.setMarkdown(view, markdown) markwon.setMarkdown(view, markdown)
} else { } else {
view.text = markdown view.text = markdown
} }

View File

@ -31,19 +31,9 @@ fun ChatScaffold(
val sessions by viewModel.sessions.collectAsState() val sessions by viewModel.sessions.collectAsState()
val currentSessionId by viewModel.currentSessionId.collectAsState() val currentSessionId by viewModel.currentSessionId.collectAsState()
val messages by viewModel.messages.collectAsState() val messages by viewModel.messages.collectAsState()
val streamingContent by viewModel.streamingContent.collectAsState()
var showClearDialog by remember { mutableStateOf(false) } var showClearDialog by remember { mutableStateOf(false) }
val currentMessages = remember(messages, streamingContent, currentSessionId) { val currentMessages = messages.filter { it.sessionId == currentSessionId }
val streaming = streamingContent
messages.asSequence()
.filter { it.sessionId == currentSessionId }
.map { msg ->
val live = streaming[msg.id]
if (msg.isStreaming && live != null) msg.copy(content = live) else msg
}
.toList()
}
// 清除确认对话框 // 清除确认对话框
if (showClearDialog) { if (showClearDialog) {

View File

@ -2,7 +2,6 @@ package com.stand.standapp.ui.chat.repo
import com.stand.standapp.AppConfig import com.stand.standapp.AppConfig
import android.content.Context import android.content.Context
import com.stand.standapp.net.NetworkModule
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -16,9 +15,12 @@ import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber import timber.log.Timber
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.concurrent.TimeUnit
class AiChatRepository { class AiChatRepository {
private val client: OkHttpClient = NetworkModule.streamingClient() private val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // 禁用读取超时以适配 SSE 流
.build()
fun streamChat(context: Context, message: String, token: String, historyJson: String): Flow<String> = callbackFlow { fun streamChat(context: Context, message: String, token: String, historyJson: String): Flow<String> = callbackFlow {
// 1. 从 AppConfig 中动态读取当前服务器基准地址,完美适配真实网关 // 1. 从 AppConfig 中动态读取当前服务器基准地址,完美适配真实网关

View File

@ -2,7 +2,6 @@ package com.stand.standapp.utils
import android.content.Context import android.content.Context
import com.stand.standapp.AppConfig import com.stand.standapp.AppConfig
import com.stand.standapp.net.NetworkModule
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
@ -18,7 +17,7 @@ import kotlin.system.exitProcess
object LogManager { object LogManager {
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
private const val MAX_DAYS = 15 private const val MAX_DAYS = 15
private val client = NetworkModule.defaultClient private val client = OkHttpClient()
private var logDir: File? = null private var logDir: File? = null
fun init(context: Context) { fun init(context: Context) {