feat: 新增智能体记忆管理功能

- 新增 Agent 记忆管理界面,允许用户对 Agent 的记忆进行增、删、改、查(CRUD)操作。
- 实现添加记忆的弹窗、记忆列表展示、编辑和删除功能。
- 在 Agent 个人资料页的操作菜单中添加入口,仅对自己创建的 Agent 可见。
- 集成积分系统,添加记忆需要消耗相应积分,并提供支付确认对话框。
This commit is contained in:
2025-11-12 23:29:26 +08:00
parent 2f08a7b2b6
commit 1953553277
4 changed files with 1259 additions and 50 deletions

View File

@@ -0,0 +1,806 @@
package com.aiosman.ravenow.ui.profile
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.AgentRuleEntity
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun AgentMemoryManageContent(
openId: String,
profile: AccountProfileEntity?,
viewModel: AgentMemoryManageViewModel,
onAddMemoryClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
// 编辑记忆的状态 - 存储正在编辑的记忆ID
var editingMemoryId by remember { mutableStateOf<Int?>(null) }
// 加载配额和列表数据
LaunchedEffect(Unit) {
viewModel.loadMemoryQuota()
viewModel.loadMemoryList()
}
val quota = viewModel.memoryQuota
val memoryList = viewModel.memoryList
val isLoading = viewModel.isLoadingMemory
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFFAF9FB))
) {
// 顶部栏:返回按钮 + 标题 + 加号按钮
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp)
) {
// 中间标题 - 绝对居中,不受其他组件影响
Text(
text = stringResource(R.string.group_chat_info_memory_manage),
style = TextStyle(
color = Color.Black,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold
),
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
// 左侧返回按钮
Row(
modifier = Modifier
.align(Alignment.CenterStart)
.clip(RoundedCornerShape(296.dp))
.background(Color.White)
.noRippleClickable { onDismiss() }
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
contentDescription = "返回",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(Color.Black)
)
Text(
text = "返回",
style = TextStyle(
color = Color.Black,
fontSize = 15.sp,
fontWeight = FontWeight.Normal
)
)
}
// 右侧圆形加号按钮
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.size(32.dp)
.clip(CircleShape)
.background(Color.White)
.noRippleClickable { onAddMemoryClick() },
contentAlignment = Alignment.Center
) {
Text(
text = "+",
style = TextStyle(
color = Color.Black,
fontSize = 20.sp,
fontWeight = FontWeight.Medium
)
)
}
}
// 浅黄色提示栏 - 显示真实的配额数据
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFFBF8EF))
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已付费:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.purchasedCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已使用:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.currentCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("可用上限:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.totalMaxCount ?: 50}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
// 记忆列表或空状态
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
if (isLoading) {
// 加载中状态
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(40.dp),
color = Color(0xFFFF8D28)
)
}
} else if (memoryList.isNotEmpty()) {
// 显示记忆列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(memoryList) { memory ->
AgentMemoryItem(
memory = memory,
isEditing = editingMemoryId == memory.id,
onEdit = {
editingMemoryId = memory.id
},
onCancel = {
editingMemoryId = null
},
onSave = { newText ->
viewModel.updateMemory(memory.id, newText)
editingMemoryId = null
},
onDelete = {
viewModel.deleteMemory(memory.id)
}
)
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.mipmap.group),
contentDescription = "暂无记忆",
modifier = Modifier
.height(150.dp).width(180.dp)
)
Spacer(Modifier.height(10.dp))
Text(
text = "暂无记忆",
style = TextStyle(color = Color.Black, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
)
Spacer(Modifier.height(6.dp))
Text(
text = "点击上方按钮添加 Agent 记忆",
style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal)
)
}
}
}
}
}
/**
* Agent 记忆项组件(不显示创建者信息)
*/
@Composable
fun AgentMemoryItem(
memory: AgentRuleEntity,
isEditing: Boolean = false,
onEdit: () -> Unit = {},
onCancel: () -> Unit = {},
onSave: (String) -> Unit = {},
onDelete: () -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
// 渐变边框颜色
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
// 当进入编辑模式时,重置文本
LaunchedEffect(isEditing) {
if (isEditing) {
memoryText = memory.rule
}
}
if (isEditing) {
// 编辑模式:显示编辑界面
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 文本输入框 - 带渐变边框
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
// 渐变边框层
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
// 内容层 - 白色背景通过padding形成边框效果
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.clip(RoundedCornerShape(15.dp))
.background(Color.White)
.padding(12.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 13.sp,
color = Color.Black,
lineHeight = 18.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
}
}
)
}
}
// 按钮行:取消和保存
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0x147C7480))
.noRippleClickable { onCancel() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
style = TextStyle(
fontSize = 15.sp,
color = Color.Black
)
)
}
// 保存按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0xFF110C13))
.noRippleClickable {
if (memoryText.isNotBlank() && memoryText != memory.rule) {
onSave(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
} else {
// 显示模式:显示记忆内容(不显示创建者信息)
// 格式化日期:从 "2025-10-20T10:30:00Z" 格式转换为 "2025年10月20日"
val formattedDate = try {
if (memory.createdAt.length >= 10) {
val dateStr = memory.createdAt.substring(0, 10)
val parts = dateStr.split("-")
if (parts.size == 3) {
"${parts[0]}${parts[1].toInt()}${parts[2].toInt()}"
} else {
dateStr
}
} else {
memory.createdAt
}
} catch (e: Exception) {
if (memory.createdAt.length >= 10) memory.createdAt.substring(0, 10) else memory.createdAt
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp)
) {
// 主文本 - 顶部
Text(
text = memory.rule,
style = TextStyle(
color = Color.Black,
fontSize = 13.sp,
lineHeight = 18.sp
),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 底部行:时间 + 编辑删除按钮(不显示创建者信息)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 时间信息 - 左侧
Text(
text = formattedDate,
style = TextStyle(
color = Color(0x993C3C43),
fontSize = 11.sp
)
)
// 编辑和删除图标 - 右侧
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 编辑图标
Image(
painter = painterResource(R.mipmap.icons_infor_edit),
contentDescription = "编辑",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onEdit() },
colorFilter = ColorFilter.tint(Color.Black)
)
Image(
painter = painterResource(R.mipmap.iconsdelete),
contentDescription = "删除",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onDelete() },
colorFilter = ColorFilter.tint(Color(0xFFEE2A33))
)
}
}
}
}
}
/**
* 添加 Agent 记忆对话框
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddAgentMemoryDialog(
profile: AccountProfileEntity?,
viewModel: AgentMemoryManageViewModel,
onDismiss: () -> Unit,
onRequestAddMemory: (String) -> Unit // 改为请求添加,而不是直接添加
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf("") }
val maxLength = 500
// 监听添加记忆的结果
LaunchedEffect(viewModel.addMemorySuccess) {
if (viewModel.addMemorySuccess) {
Toast.makeText(context, context.getString(R.string.group_chat_info_memory_add_success), Toast.LENGTH_SHORT).show()
memoryText = "" // 清空输入框
onDismiss()
viewModel.addMemorySuccess = false
}
}
LaunchedEffect(viewModel.addMemoryError) {
viewModel.addMemoryError?.let { error ->
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
viewModel.addMemoryError = null
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 40.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 顶部标题栏
Row(
modifier = Modifier
.fillMaxWidth()
.height(44.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Spacer(modifier = Modifier.width(24.dp))
Text(
text = stringResource(R.string.group_chat_info_add_memory),
style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Black
),
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
Image(
painter = painterResource(R.drawable.rider_pro_close),
contentDescription = "关闭",
modifier = Modifier
.size(24.dp)
.noRippleClickable { onDismiss() },
colorFilter = ColorFilter.tint(Color.Black)
)
}
Spacer(modifier = Modifier.height(10.dp))
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Agent 信息卡片
Row(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
if (profile?.avatar?.isNotEmpty() == true) {
CustomAsyncImage(
imageUrl = profile.avatar,
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(12.dp)),
contentDescription = "Agent 头像",
context = LocalContext.current
)
} else {
Box(
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(12.dp))
.background(AppColors.decentBackground),
contentAlignment = Alignment.Center
) {
Text(
text = profile?.nickName?.firstOrNull()?.toString() ?: "",
style = TextStyle(
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
)
}
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = profile?.nickName ?: "",
style = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Black
)
)
if (!profile?.bio.isNullOrEmpty()) {
Text(
text = profile?.bio ?: "",
style = TextStyle(
fontSize = 12.sp,
color = Color(0xFF3C3C43).copy(alpha = 0.6f)
),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
// 输入框卡片
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(84.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
maxLines = 6,
textStyle = TextStyle(
fontSize = 15.sp,
color = Color.Black,
lineHeight = 20.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
if (memoryText.isEmpty()) {
Text(
text = stringResource(R.string.group_chat_info_memory_input_hint),
style = TextStyle(
fontSize = 15.sp,
color = Color.Black.copy(alpha = 0.3f)
)
)
}
}
}
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Text(
text = "${memoryText.length}/$maxLength",
style = TextStyle(
fontSize = 12.sp,
color = Color(0xFF3C3C43).copy(alpha = 0.3f)
)
)
}
}
}
// 提示信息卡片
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color(0xFFFBF8EF))
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "",
fontSize = 13.sp
)
val memoryCost = viewModel.addMemoryCost
Text(
text = if (memoryCost != null && memoryCost > 0) {
"添加记忆需消耗 ${memoryCost}派币"
} else {
stringResource(R.string.group_chat_info_memory_cost)
},
style = TextStyle(
fontSize = 13.sp,
color = Color.Black
)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "🤖",
fontSize = 13.sp
)
Text(
text = stringResource(R.string.group_chat_info_memory_optimization),
style = TextStyle(
fontSize = 13.sp,
color = Color.Black
)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "✏️",
fontSize = 13.sp
)
Text(
text = stringResource(R.string.group_chat_info_memory_editable),
style = TextStyle(
fontSize = 13.sp,
color = Color.Black
)
)
}
}
// 添加记忆按钮
val isButtonEnabled = memoryText.isNotEmpty() && !viewModel.isAddingMemory
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.clip(RoundedCornerShape(1000.dp))
.then(
if (isButtonEnabled) {
Modifier.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0xFF7C45ED),
Color(0x997C57EE),
Color(0x887BD8F8)
)
)
)
} else {
Modifier.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0x337C45ED),
Color(0x337C57EE),
Color(0x337BD8F8)
)
)
)
}
)
.noRippleClickable {
if (isButtonEnabled) {
onRequestAddMemory(memoryText)
}
},
contentAlignment = Alignment.Center
) {
if (viewModel.isAddingMemory) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White
)
} else {
Text(
text = stringResource(R.string.group_chat_info_add_memory),
style = TextStyle(
fontSize = 17.sp,
color = if (isButtonEnabled) Color.White else Color.White.copy(alpha = 0.6f)
)
)
}
}
}
}
}

View File

@@ -0,0 +1,207 @@
package com.aiosman.ravenow.ui.profile
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.AgentRuleService
import com.aiosman.ravenow.data.AgentRuleServiceImpl
import com.aiosman.ravenow.data.AgentRuleEntity
import com.aiosman.ravenow.data.AgentRuleQuotaEntity
import com.aiosman.ravenow.data.PointService
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class AgentMemoryManageViewModel(
private val openId: String
) : ViewModel() {
// 记忆管理相关状态
var memoryQuota by mutableStateOf<AgentRuleQuotaEntity?>(null)
var memoryList by mutableStateOf<List<AgentRuleEntity>>(emptyList())
var isLoadingMemory by mutableStateOf(false)
var memoryError by mutableStateOf<String?>(null)
// Agent 规则服务
private val agentRuleService: AgentRuleService = AgentRuleServiceImpl()
// 添加记忆相关状态
var isAddingMemory by mutableStateOf(false)
var addMemoryError by mutableStateOf<String?>(null)
var addMemorySuccess by mutableStateOf(false)
var addMemoryCost by mutableStateOf<Int?>(null)
var pointsBalance by mutableStateOf<Int?>(null)
init {
loadMemoryCost()
loadPointsBalance()
}
/**
* 加载积分余额
*/
fun loadPointsBalance() {
viewModelScope.launch {
try {
PointService.refreshMyPointsBalance(includeStatistics = false)
val balance = PointService.pointsBalance.first()
pointsBalance = balance?.balance
} catch (e: Exception) {
Log.e("AgentMemoryManageViewModel", "加载积分余额失败: ${e.message}", e)
}
}
}
/**
* 添加 Agent 记忆
* @param memoryText 记忆内容
*/
fun addAgentMemory(memoryText: String) {
viewModelScope.launch {
try {
isAddingMemory = true
addMemoryError = null
addMemorySuccess = false
// 使用 Agent 规则接口创建记忆
agentRuleService.createAgentRuleByOpenId(
openId = openId,
rule = memoryText
)
addMemorySuccess = true
Log.d("AgentMemoryManageViewModel", "Agent 记忆添加成功")
// 刷新记忆列表和配额
loadMemoryQuota()
loadMemoryList()
} catch (e: Exception) {
addMemoryError = e.message ?: "添加 Agent 记忆失败"
Log.e("AgentMemoryManageViewModel", "添加 Agent 记忆失败: ${e.message}", e)
} finally {
isAddingMemory = false
}
}
}
/**
* 获取记忆配额信息Agent 规则配额)
*/
fun loadMemoryQuota() {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
// 使用 Agent 规则接口获取配额
memoryQuota = agentRuleService.getAgentRuleQuota(openId = openId)
} catch (e: Exception) {
memoryError = e.message ?: "获取配额信息失败"
Log.e("AgentMemoryManageViewModel", "获取配额信息失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 获取记忆列表Agent 规则列表)
*/
fun loadMemoryList(page: Int = 1, pageSize: Int = 20) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
// 使用 Agent 规则接口获取列表
val result = agentRuleService.getAgentRuleList(
openId = openId,
keyword = null,
page = page,
pageSize = pageSize
)
memoryList = result.list
} catch (e: Exception) {
memoryError = e.message ?: "获取记忆列表失败"
Log.e("AgentMemoryManageViewModel", "获取记忆列表失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 删除记忆Agent 规则)
*/
fun deleteMemory(ruleId: Int) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
// 使用 Agent 规则接口删除
agentRuleService.deleteAgentRule(ruleId)
// 刷新记忆列表和配额
loadMemoryQuota()
loadMemoryList()
} catch (e: Exception) {
memoryError = e.message ?: "删除记忆失败"
Log.e("AgentMemoryManageViewModel", "删除记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 更新记忆Agent 规则)
*/
fun updateMemory(ruleId: Int, newRuleText: String) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
// 使用 Agent 规则接口更新
agentRuleService.updateAgentRule(
id = ruleId,
rule = newRuleText,
openId = openId
)
// 刷新记忆列表和配额
loadMemoryQuota()
loadMemoryList()
} catch (e: Exception) {
memoryError = e.message ?: "更新记忆失败"
Log.e("AgentMemoryManageViewModel", "更新记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 加载添加 Agent 记忆的价格
*/
fun loadMemoryCost() {
viewModelScope.launch {
try {
// 获取积分规则中的 Agent 记忆价格
PointService.refreshPointsRules()
val rules = PointService.pointsRules.first()
val agentMemoryRule = rules?.sub?.get(PointService.PointsRuleKey.ADD_AGENT_MEMORY)
addMemoryCost = when (agentMemoryRule) {
is PointService.RuleAmount.Fixed -> agentMemoryRule.value
is PointService.RuleAmount.Range -> agentMemoryRule.min
else -> null
}
} catch (e: Exception) {
Log.e("AgentMemoryManageViewModel", "加载记忆价格失败: ${e.message}", e)
}
}
}
}

View File

@@ -57,6 +57,7 @@ fun AiProfileV3(
onFollowClick: () -> Unit = {}, onFollowClick: () -> Unit = {},
onChatClick: () -> Unit = {}, onChatClick: () -> Unit = {},
onShareClick: () -> Unit = {}, onShareClick: () -> Unit = {},
onMemoryManageClick: () -> Unit = {},
onLoadMore: () -> Unit = {}, onLoadMore: () -> Unit = {},
onComment: (MomentEntity) -> Unit = {}, onComment: (MomentEntity) -> Unit = {},
) { ) {
@@ -181,6 +182,10 @@ fun AiProfileV3(
) )
} }
}, },
onMemoryManageClick = {
showMenu = false
onMemoryManageClick()
},
profile = profile, profile = profile,
showEdit = isCreator showEdit = isCreator
) )
@@ -525,6 +530,7 @@ private fun AiProfileMenuModal(
onChatClick: () -> Unit, onChatClick: () -> Unit,
onShareClick: () -> Unit, onShareClick: () -> Unit,
onEditClick: () -> Unit, onEditClick: () -> Unit,
onMemoryManageClick: () -> Unit,
showEdit: Boolean = false, showEdit: Boolean = false,
profile: AccountProfileEntity? = null profile: AccountProfileEntity? = null
) { ) {
@@ -541,7 +547,7 @@ private fun AiProfileMenuModal(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(if (showEdit) 240.dp else 160.dp) .height(if (showEdit) 240.dp else 240.dp)
.background(appColors.background) .background(appColors.background)
.padding(vertical = 47.dp, horizontal = 20.dp) .padding(vertical = 47.dp, horizontal = 20.dp)
) { ) {
@@ -581,7 +587,7 @@ private fun AiProfileMenuModal(
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(end = if (showEdit) 16.dp else 0.dp), .padding(end = 16.dp),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@@ -609,6 +615,38 @@ private fun AiProfileMenuModal(
) )
} }
// 记忆管理选项
Column(
modifier = Modifier
.weight(1f)
.padding(end = if (showEdit) 16.dp else 0.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.clip(CircleShape)
.noRippleClickable {
onMemoryManageClick()
}
) {
Image(
painter = painterResource(id = R.drawable.ic_brain_add),
contentDescription = "",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(appColors.text)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.group_chat_info_memory_manage),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = appColors.text
)
}
// 编辑选项(仅创建者可见) // 编辑选项(仅创建者可见)
if (showEdit) { if (showEdit) {
Column( Column(

View File

@@ -1,22 +1,44 @@
package com.aiosman.ravenow.ui.profile package com.aiosman.ravenow.ui.profile
import android.util.Log import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.GuestLoginCheckOut import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.UserServiceImpl import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.exp.viewModelFactory import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.navigateToChatAi import com.aiosman.ravenow.ui.navigateToChatAi
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AiProfileWrap(id: String) { fun AiProfileWrap(id: String) {
val model: AiProfileViewModel = viewModel(factory = viewModelFactory { val model: AiProfileViewModel = viewModel(factory = viewModelFactory {
@@ -26,6 +48,26 @@ fun AiProfileWrap(id: String) {
val navController = LocalNavController.current val navController = LocalNavController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// 记忆管理相关状态
var showMemoryManageDialog by remember { mutableStateOf(false) }
var showAddMemoryDialog by remember { mutableStateOf(false) }
var showAddMemoryConfirmDialog by remember { mutableStateOf(false) }
var pendingMemoryText by remember { mutableStateOf("") }
val memoryManageSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val addMemorySheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// 获取 chatAIId (openId)
val chatAIId = model.profile?.chatAIId ?: ""
// 创建记忆管理 ViewModel
val memoryViewModel: AgentMemoryManageViewModel? = if (chatAIId.isNotEmpty()) {
viewModel(factory = viewModelFactory {
AgentMemoryManageViewModel(chatAIId)
}, key = "agentMemoryViewModel_${chatAIId}")
} else {
null
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.loadProfile(id) model.loadProfile(id)
MyProfileViewModel.loadProfile() MyProfileViewModel.loadProfile()
@@ -33,6 +75,7 @@ fun AiProfileWrap(id: String) {
val isSelf = id == MyProfileViewModel.profile?.id.toString() val isSelf = id == MyProfileViewModel.profile?.id.toString()
Box(modifier = Modifier.fillMaxSize()) {
AiProfileV3( AiProfileV3(
profile = model.profile, profile = model.profile,
moments = model.moments, moments = model.moments,
@@ -78,6 +121,11 @@ fun AiProfileWrap(id: String) {
// TODO: 实现分享逻辑 // TODO: 实现分享逻辑
Log.d("AiProfileWrap", "分享功能待实现") Log.d("AiProfileWrap", "分享功能待实现")
}, },
onMemoryManageClick = {
if (chatAIId.isNotEmpty()) {
showMemoryManageDialog = true
}
},
onLoadMore = { onLoadMore = {
Log.d("AiProfileWrap", "onLoadMore被调用") Log.d("AiProfileWrap", "onLoadMore被调用")
model.loadMoreMoment() model.loadMoreMoment()
@@ -86,5 +134,115 @@ fun AiProfileWrap(id: String) {
navController.navigateToPost(moment.id) navController.navigateToPost(moment.id)
} }
) )
// 添加记忆对话框
if (showAddMemoryDialog && memoryViewModel != null) {
ModalBottomSheet(
onDismissRequest = { showAddMemoryDialog = false },
sheetState = addMemorySheetState,
containerColor = Color(0xFFFAF9FB),
dragHandle = {
Box(
modifier = Modifier
.width(36.dp)
.height(5.dp)
.padding(top = 5.dp)
.background(
Color(0xFFCCCCCC),
RoundedCornerShape(100.dp)
)
)
},
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
) {
AddAgentMemoryDialog(
profile = model.profile,
viewModel = memoryViewModel,
onDismiss = { showAddMemoryDialog = false },
onRequestAddMemory = { memoryText ->
// 关闭添加对话框,显示确认框
showAddMemoryDialog = false
pendingMemoryText = memoryText
showAddMemoryConfirmDialog = true
}
)
}
}
// 记忆管理弹窗
if (showMemoryManageDialog && memoryViewModel != null && chatAIId.isNotEmpty()) {
ModalBottomSheet(
onDismissRequest = { showMemoryManageDialog = false },
sheetState = memoryManageSheetState,
containerColor = Color(0xFFFAF9FB),
dragHandle = {
Box(
modifier = Modifier
.width(36.dp)
.height(5.dp)
.padding(top = 5.dp)
.background(
Color(0xFFCCCCCC),
RoundedCornerShape(100.dp)
)
)
},
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
) {
// 立即展开到全屏,避免逐渐变高的动画
LaunchedEffect(Unit) {
memoryManageSheetState.expand()
}
AgentMemoryManageContent(
openId = chatAIId,
profile = model.profile,
viewModel = memoryViewModel,
onAddMemoryClick = {
showMemoryManageDialog = false
showAddMemoryDialog = true
},
onDismiss = {
showMemoryManageDialog = false
}
)
}
}
// 添加记忆确认对话框
if (showAddMemoryConfirmDialog && memoryViewModel != null) {
val cost = memoryViewModel.addMemoryCost ?: 0
val currentBalance = memoryViewModel.pointsBalance ?: 0
val balanceAfterCost = (currentBalance - cost).coerceAtLeast(0)
val isBalanceSufficient = currentBalance >= cost
// 监听添加成功,关闭确认对话框并刷新积分余额
LaunchedEffect(memoryViewModel.addMemorySuccess) {
if (memoryViewModel.addMemorySuccess) {
showAddMemoryConfirmDialog = false
pendingMemoryText = ""
memoryViewModel.addMemorySuccess = false
memoryViewModel.loadPointsBalance() // 刷新积分余额
}
}
PointsPaymentDialog(
cost = cost,
currentBalance = currentBalance,
balanceAfterCost = balanceAfterCost,
isBalanceSufficient = isBalanceSufficient,
onConfirm = {
// 确认支付,添加记忆
memoryViewModel.addAgentMemory(pendingMemoryText)
},
onCancel = {
showAddMemoryConfirmDialog = false
pendingMemoryText = ""
},
title = stringResource(R.string.group_chat_info_add_memory),
description = stringResource(R.string.group_chat_info_memory_description),
isProcessing = memoryViewModel.isAddingMemory
)
}
}
} }