feat(enums): 接入 /meta/enums 权威枚举表,删掉客户端硬编码映射
客户端此前自带 platform / mood / mode 三张硬编码标签表,会与服务端漂移—— 实测已经漂了:客户端把 `web` 显示成「Web」,权威表里是「网页」。 - data:MetaApi(GET /api/v1/meta/enums)+ MetaDto + MetaRepository, 经 ApiClientFactory 创建以复用既有 token / 401 处理 - domain/enums/EnumCatalog.kt:标签查找器,**不含任何硬编码兜底表**; 不在表内的取值一律原样返回(api-client §1.1 的未知值回落红线); platformLabelOrNull 供 deviceId 前缀这类「认识/不认识要走不同回落」的调用方用; 空 id 或空 label 一律不入表(否则角标会显示空白,或查空串凭空借到别人的标签) - core/prefs/EnumPreferences.kt:last-good 快照,baseUrl+username 指纹闸门, 换服务器/账号即作废,登出清空 - EnumCatalogViewModel + LocalEnumCatalog:在 CyreneRoot 注入(与 LocalAppPrefs 同套路), 聊天角标与管理页共读同一份 - 删除 platformLabels / personaModeLabels / moodLabels;MessageSource 与 AdminFormat 改为查表,并保留各自原有的原样回落 为什么不留硬编码兜底表:没有登录就看不到任何消息,而权威表在登录前后即会拉取, 所以 last-good 快照几乎总是存在;留着硬编码表等于把「漂移」重新引进来,而且是无声的—— 旧标签和新鲜标签长得一模一样。 不转换的:trace kind/status、memory scope、thinking.mode、WS session_busy 的 mode 集合—— 它们都不在 /meta/enums 内,且原本就已按原样回落。 测试:115 全绿(88 → +27:MetaDtoRealPayload 8 / EnumCatalog 7 / EnumPrefs 3 / MessageSource 8,AdminFormatTest 改写并 +1),其中 /meta/enums 与登录响应一样 用真实抓取的响应体做夹具。
This commit is contained in:
1 parent
9f054aad28
commit
09a3222b7b
17 files changed
+693
-48
No files matched your search
@@ -25,6 +25,7 @@ import top.yeij.cyrene.domain.admin.moodLabel
|
||||
import top.yeij.cyrene.domain.admin.orDash
|
||||
import top.yeij.cyrene.domain.admin.personaModeLabel
|
||||
import top.yeij.cyrene.domain.admin.triStateLabel
|
||||
import top.yeij.cyrene.domain.enums.LocalEnumCatalog
|
||||
import top.yeij.cyrene.ui.common.PreferenceCard
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
@@ -62,6 +63,7 @@ fun AdminPersonaScreen(viewModel: AdminViewModel, role: AdminRole, onBack: () ->
|
||||
|
||||
@Composable
|
||||
private fun PersonaCard(persona: PersonaSummaryDto) {
|
||||
val catalog = LocalEnumCatalog.current
|
||||
PreferenceCard {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -76,11 +78,14 @@ private fun PersonaCard(persona: PersonaSummaryDto) {
|
||||
}
|
||||
PersonaDetail("id", persona.id)
|
||||
PersonaDetail("character", persona.character.ifBlank { "-" })
|
||||
// mode 是开放集合:未知取值原样显示。
|
||||
PersonaDetail("mode", personaModeLabel(persona.mode).ifBlank { "-" })
|
||||
// mode 是开放集合:权威表里没有的取值原样显示。
|
||||
PersonaDetail("mode", personaModeLabel(persona.mode, catalog).ifBlank { "-" })
|
||||
PersonaDetail("记忆域", persona.memoryNamespace.ifBlank { "-" })
|
||||
// mood 为空表示「该角色暂无被跟踪的情绪」,不伪造默认值。
|
||||
PersonaDetail("情绪", persona.mood?.takeIf { it.isNotBlank() }?.let(::moodLabel) ?: "暂无")
|
||||
PersonaDetail(
|
||||
"情绪",
|
||||
persona.mood?.takeIf { it.isNotBlank() }?.let { moodLabel(it, catalog) } ?: "暂无",
|
||||
)
|
||||
PersonaDetail("思考", thinkingLabel(persona.thinking))
|
||||
persona.avatar?.let { PersonaDetail("avatar", it) }
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import top.yeij.cyrene.core.prefs.LocalAppPrefs
|
||||
import top.yeij.cyrene.data.api.dto.HistoryMessageDto
|
||||
import top.yeij.cyrene.domain.enums.LocalEnumCatalog
|
||||
import top.yeij.cyrene.domain.parse.ContentPart
|
||||
import top.yeij.cyrene.domain.parse.ParsedBubble
|
||||
import top.yeij.cyrene.domain.parse.parse
|
||||
@@ -43,13 +44,16 @@ private const val CURSOR = "▍"
|
||||
@Composable
|
||||
fun MessageBubble(message: HistoryMessageDto) {
|
||||
val isUser = message.role == "user"
|
||||
// 这里的 `chat` 是 **mode id**,与 `/meta/enums` 的 modes 表里那一条同源但用途不同:
|
||||
// 表只能给「id → 标签」,说不出哪个 mode 是「按 markup 切气泡的聊天式渲染」,
|
||||
// 所以这条字面量刻意保留为客户端渲染契约,不表驱动。
|
||||
val isChat = message.mode.isNullOrBlank() || message.mode == "chat"
|
||||
val bubbles = remember(message.content, isChat) {
|
||||
val parsed = if (isChat) parse(message.content).filterNot { it.isEmpty } else emptyList()
|
||||
parsed.ifEmpty { listOf(plainBubble(message.content)) }
|
||||
}
|
||||
|
||||
MessageGroup(isUser = isUser, footer = messageSourceLabel(message)) {
|
||||
MessageGroup(isUser = isUser, footer = messageSourceLabel(message, LocalEnumCatalog.current)) {
|
||||
message.thinking?.takeIf { it.isNotBlank() }?.let { ThinkingCard(it) }
|
||||
message.image?.takeIf { it.isNotBlank() }?.let { ChatImage(it) }
|
||||
bubbles.forEachIndexed { index, bubble ->
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
package top.yeij.cyrene.ui.chat
|
||||
|
||||
import top.yeij.cyrene.data.api.dto.HistoryMessageDto
|
||||
|
||||
private val platformLabels = mapOf(
|
||||
"web" to "Web",
|
||||
"kook" to "KOOK",
|
||||
"weixin-ilink" to "微信",
|
||||
"qq-obv11" to "QQ",
|
||||
"telegram" to "Telegram",
|
||||
"webhook" to "Webhook",
|
||||
)
|
||||
import top.yeij.cyrene.domain.enums.EnumCatalog
|
||||
|
||||
/**
|
||||
* 来源角标(S1.3):每条消息默认显示来源。
|
||||
* - 主人发的 → 「来自 X」(从哪一端发出)
|
||||
* - 昔涟发的 → 「发送至 X」(回到哪一端)
|
||||
* 优先用 `deviceId` 区分具体端,再回落到 `platform`;两者都没有则不显示。
|
||||
*
|
||||
* 平台名到标签的映射来自 `/meta/enums` 的权威表([catalog]),**不含任何硬编码表**:
|
||||
* 表里没有的取值原样显示(红线),未登录 / 尚无快照时全部原样。
|
||||
*/
|
||||
fun messageSourceLabel(message: HistoryMessageDto): String? {
|
||||
val name = sourceName(message) ?: return null
|
||||
fun messageSourceLabel(message: HistoryMessageDto, catalog: EnumCatalog): String? {
|
||||
val name = sourceName(message, catalog) ?: return null
|
||||
val verb = if (message.role == "assistant") "发送至" else "来自"
|
||||
return "$verb $name"
|
||||
}
|
||||
|
||||
private fun sourceName(message: HistoryMessageDto): String? {
|
||||
private fun sourceName(message: HistoryMessageDto, catalog: EnumCatalog): String? {
|
||||
val deviceId = message.deviceId
|
||||
if (!deviceId.isNullOrBlank()) {
|
||||
// 下面两条是 device 粒度、与平台表无关的端标识,不参与权威表映射。
|
||||
if (deviceId.startsWith("voiceassist")) return "安卓"
|
||||
if (deviceId.startsWith("web:")) {
|
||||
val browser = deviceId.removePrefix("web:").substringBefore(':')
|
||||
return "Web($browser)"
|
||||
}
|
||||
// IM 端 deviceId 形如 <平台名>:<配置id>:<uid>,只取平台名做友好显示
|
||||
platformLabels[deviceId.substringBefore(':')]?.let { return it }
|
||||
return deviceId
|
||||
// IM 端 deviceId 形如 <平台名>:<配置id>:<uid>,只取平台名做友好显示;
|
||||
// 平台名不在表里(服务端新增渠道)则整串原样显示,不丢信息也不猜。
|
||||
val platform = deviceId.substringBefore(':')
|
||||
return catalog.platformLabelOrNull(platform) ?: deviceId
|
||||
}
|
||||
|
||||
val platform = message.platform
|
||||
if (!platform.isNullOrBlank()) return platformLabels[platform] ?: platform
|
||||
if (!platform.isNullOrBlank()) return catalog.platformLabel(platform)
|
||||
return null
|
||||
}
|
||||
Reference in new issue
Block a user