Merge branch 'feat/pr-20251104-154907' of https://github.com/Kevinlinpr/rider-pro-android-app into feat/pr-20251104-154907

This commit is contained in:
2025-11-07 14:42:17 +08:00
28 changed files with 2738 additions and 158 deletions

15
.vscode/launch.json vendored
View File

@@ -1,15 +0,0 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "针对 localhost 启动 Chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@@ -2,8 +2,16 @@ package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.AgentRule
import com.aiosman.ravenow.data.api.AgentRuleListResponse
import com.aiosman.ravenow.data.api.AgentRuleQuota
import com.aiosman.ravenow.data.api.AgentRuleAgent
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.InsufficientBalanceError
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.ProfileEntity
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
data class Agent(
@@ -83,9 +91,16 @@ data class Profile(
)
}
}
/**
* 智能体服务
*/
interface AgentService {
/**
* 获取智能体列表
* @param pageNumber 页码
* @param pageSize 每页数量,默认 20
* @param authorId 作者ID可选参数用于筛选特定作者的智能体
* @return 智能体列表容器,包含分页信息和智能体列表,失败时返回 null
*/
suspend fun getAgent(
pageNumber: Int,
@@ -96,3 +111,206 @@ interface AgentService {
}
// ========== Agent 规则 - 领域实体 ==========
data class AgentRuleAgentInfo(
val id: Int,
val title: String,
val avatar: String,
)
data class AgentRuleEntity(
val id: Int,
val rule: String,
val creator: String,
val creatorType: String,
val scope: String,
val agent: AgentRuleAgentInfo,
val createdAt: String,
val updatedAt: String,
)
data class AgentRuleListResult(
val page: Int,
val pageSize: Int,
val total: Int,
val list: List<AgentRuleEntity>,
)
data class AgentRuleQuotaEntity(
val agentId: Int,
val agentTitle: String,
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double,
)
// ========== Agent 规则 - Service 接口 ==========
/**
* Agent 规则服务
*/
interface AgentRuleService {
/**
* 根据 OpenId 创建 Agent 规则
* @param openId Agent 的 OpenId
* @param rule 规则内容,不能为空
* @throws ServiceException 创建失败时抛出异常(包括余额不足等情况)
*/
suspend fun createAgentRuleByOpenId(openId: String, rule: String)
/**
* 修改 Agent 规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @param openId Agent 的 OpenId可选参数
* @throws ServiceException 修改失败时抛出异常(包括余额不足等情况)
*/
suspend fun updateAgentRule(id: Int, rule: String, openId: String? = null)
/**
* 删除 Agent 规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteAgentRule(id: Int)
/**
* 查询 Agent 规则列表
* @param openId Agent 的 OpenId
* @param keyword 关键词搜索,可选参数
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleList(
openId: String,
keyword: String? = null,
page: Int = 1,
pageSize: Int = 10
): AgentRuleListResult
/**
* 查询 Agent 规则配额使用情况
* @param openId Agent 的 OpenId
* @return 规则配额信息,包含基础配额、已购买配额、当前使用量等
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity
}
class AgentRuleServiceImpl : AgentRuleService {
private val gson = Gson()
override suspend fun createAgentRuleByOpenId(openId: String, rule: String) {
val body = CreateAgentRuleRequestBody(
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.createAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("创建 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun updateAgentRule(id: Int, rule: String, openId: String?) {
val body = UpdateAgentRuleRequestBody(
id = id,
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.updateAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("更新 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun deleteAgentRule(id: Int) {
val resp = ApiClient.api.deleteAgentRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun getAgentRuleList(
openId: String,
keyword: String?,
page: Int,
pageSize: Int
): AgentRuleListResult {
val resp = ApiClient.api.getAgentRuleList(
promptId = openId,
rule = keyword,
page = page,
pageSize = pageSize
)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则列表失败")
return data.toEntity()
}
override suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity {
val resp = ApiClient.api.getAgentRuleQuota(openId)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则配额失败")
return data.toEntity()
}
private fun AgentRuleListResponse.toEntity(): AgentRuleListResult = AgentRuleListResult(
page = page,
pageSize = pageSize,
total = total,
list = list.map { it.toEntity() }
)
private fun AgentRule.toEntity(): AgentRuleEntity = AgentRuleEntity(
id = id,
rule = rule,
creator = creator,
creatorType = creatorType,
scope = scope,
agent = prompt.toEntity(),
createdAt = createdAt,
updatedAt = updatedAt,
)
private fun AgentRuleAgent.toEntity(): AgentRuleAgentInfo = AgentRuleAgentInfo(
id = id,
title = title,
avatar = avatar,
)
private fun AgentRuleQuota.toEntity(): AgentRuleQuotaEntity = AgentRuleQuotaEntity(
agentId = promptId,
agentTitle = promptTitle,
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent,
)
}

View File

@@ -9,7 +9,7 @@ import com.google.gson.annotations.SerializedName
data class ListContainer<T>(
// 总数
@SerializedName("total")
val total: Int,
val total: Long,
// 当前页
@SerializedName("page")
val page: Int,

View File

@@ -1,11 +1,16 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.data.api.CreateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.UpdateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.RoomRuleQuota
import com.aiosman.ravenow.data.api.RoomRule
import com.aiosman.ravenow.data.api.RoomRuleCreator
import com.aiosman.ravenow.entity.CreatorEntity
import com.aiosman.ravenow.entity.ProfileEntity
import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.entity.RoomRuleEntity
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
import com.aiosman.ravenow.entity.RoomRuleQuotaEntity
import com.aiosman.ravenow.entity.UsersEntity
import com.google.gson.annotations.SerializedName
@@ -106,6 +111,190 @@ data class Users(
}
}
/**
* 房间规则相关服务
*/
interface RoomService {
/**
* 创建房间规则
* @param rule 规则内容,不能为空
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @throws ServiceException 创建失败时抛出异常
*/
suspend fun createRoomRule(
rule: String,
roomId: Int? = null,
trtcId: String? = null
)
/**
* 修改房间规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @throws ServiceException 修改失败时抛出异常
*/
suspend fun updateRoomRule(
id: Int,
rule: String
)
/**
* 删除房间规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteRoomRule(id: Int)
/**
* 查询房间规则列表
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleList(
roomId: Int? = null,
trtcId: String? = null,
page: Int = 1,
pageSize: Int = 10
): ListContainer<RoomRuleEntity>
/**
* 查询规则配额使用情况
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @return 规则配额信息
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleQuota(
roomId: Int? = null,
trtcId: String? = null
): RoomRuleQuotaEntity
}
/**
* 房间规则服务实现类
*/
class RoomServiceImpl : RoomService {
override suspend fun createRoomRule(
rule: String,
roomId: Int?,
trtcId: String?
) {
val resp = ApiClient.api.createRoomRule(
CreateRoomRuleRequestBody(
rule = rule,
roomId = roomId,
trtcId = trtcId
)
)
if (!resp.isSuccessful) {
throw ServiceException("创建房间规则失败")
}
}
override suspend fun updateRoomRule(
id: Int,
rule: String
) {
val resp = ApiClient.api.updateRoomRule(
UpdateRoomRuleRequestBody(
id = id,
rule = rule
)
)
if (!resp.isSuccessful) {
throw ServiceException("修改房间规则失败")
}
}
override suspend fun deleteRoomRule(id: Int) {
val resp = ApiClient.api.deleteRoomRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除房间规则失败")
}
}
override suspend fun getRoomRuleList(
roomId: Int?,
trtcId: String?,
page: Int,
pageSize: Int
): ListContainer<RoomRuleEntity> {
val resp = ApiClient.api.getRoomRuleList(
roomId = roomId,
trtcId = trtcId,
page = page,
pageSize = pageSize
)
val body = resp.body() ?: throw ServiceException("获取房间规则列表失败")
return ListContainer(
list = body.list.map { it.toRoomRuleEntity() },
page = body.page,
total = body.total,
pageSize = body.pageSize
)
}
override suspend fun getRoomRuleQuota(
roomId: Int?,
trtcId: String?
): RoomRuleQuotaEntity {
val resp = ApiClient.api.getRoomRuleQuota(
roomId = roomId,
trtcId = trtcId
)
val body = resp.body() ?: throw ServiceException("获取规则配额信息失败")
val data = body.data ?: throw ServiceException("规则配额数据为空")
return data.toRoomRuleQuotaEntity()
}
}
/**
* RoomRule 扩展函数,转换为 RoomRuleEntity
*/
fun RoomRule.toRoomRuleEntity(): RoomRuleEntity {
return RoomRuleEntity(
id = id,
rule = rule,
creator = creator?.toRoomRuleCreatorEntity(),
creatorType = creatorType,
roomId = roomId,
createdAt = createdAt,
updatedAt = updatedAt
)
}
/**
* RoomRuleCreator 扩展函数,转换为 RoomRuleCreatorEntity
*/
fun RoomRuleCreator.toRoomRuleCreatorEntity(): RoomRuleCreatorEntity {
return RoomRuleCreatorEntity(
id = id,
nickname = nickname,
avatar = avatar
)
}
/**
* RoomRuleQuota 扩展函数,转换为 RoomRuleQuotaEntity
*/
fun RoomRuleQuota.toRoomRuleQuotaEntity(): RoomRuleQuotaEntity {
return RoomRuleQuotaEntity(
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent
)
}

View File

@@ -489,7 +489,7 @@ data class CategoryListResponse(
val list: List<CategoryTemplate>
)
// ========== Prompt Rule 相关数据类 ==========
// ========== Agent Rule 相关数据类 ==========
/**
* 创建规则请求体
@@ -497,7 +497,7 @@ data class CategoryListResponse(
* @param promptId 智能体ID与 openId 二选一promptId 优先
* @param openId 智能体的 OpenIDUUID格式与 promptId 二选一
*/
data class CreatePromptRuleRequestBody(
data class CreateAgentRuleRequestBody(
@SerializedName("rule")
val rule: String,
@SerializedName("promptId")
@@ -513,7 +513,7 @@ data class CreatePromptRuleRequestBody(
* @param promptId 要更改关联的智能体ID可选
* @param openId 要更改关联的智能体 OpenID可选
*/
data class UpdatePromptRuleRequestBody(
data class UpdateAgentRuleRequestBody(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
@@ -530,7 +530,7 @@ data class UpdatePromptRuleRequestBody(
* @param title 智能体标题
* @param avatar 智能体头像URL
*/
data class PromptRuleAgent(
data class AgentRuleAgent(
@SerializedName("id")
val id: Int,
@SerializedName("title")
@@ -550,7 +550,7 @@ data class PromptRuleAgent(
* @param createdAt 创建时间ISO 8601 格式)
* @param updatedAt 更新时间ISO 8601 格式)
*/
data class PromptRule(
data class AgentRule(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
@@ -562,7 +562,7 @@ data class PromptRule(
@SerializedName("scope")
val scope: String,
@SerializedName("prompt")
val prompt: PromptRuleAgent,
val prompt: AgentRuleAgent,
@SerializedName("created_at")
val createdAt: String,
@SerializedName("updated_at")
@@ -576,7 +576,7 @@ data class PromptRule(
* @param total 总记录数
* @param list 规则列表
*/
data class PromptRuleListResponse(
data class AgentRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
@@ -584,7 +584,7 @@ data class PromptRuleListResponse(
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<PromptRule>
val list: List<AgentRule>
)
/**
@@ -598,7 +598,7 @@ data class PromptRuleListResponse(
* @param remainingCount 剩余可用条数
* @param usagePercent 使用百分比0-100
*/
data class PromptRuleQuota(
data class AgentRuleQuota(
@SerializedName("promptId")
val promptId: Int,
@SerializedName("promptTitle")
@@ -617,6 +617,150 @@ data class PromptRuleQuota(
val usagePercent: Double
)
// ========== Room Rule 相关数据类 ==========
/**
* 创建房间规则请求体
* @param rule 规则内容,不能为空
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID字符串格式与 roomId 二选一
*/
data class CreateRoomRuleRequestBody(
@SerializedName("rule")
val rule: String,
@SerializedName("roomId")
val roomId: Int? = null,
@SerializedName("trtcId")
val trtcId: String? = null
)
/**
* 修改房间规则请求体
* @param id 规则ID必填
* @param rule 新的规则内容,不能为空
*/
data class UpdateRoomRuleRequestBody(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String
)
/**
* 房间规则创建者信息
* @param id 创建者ID
* @param nickname 创建者昵称
* @param avatar 创建者头像文件名
*/
data class RoomRuleCreator(
@SerializedName("id")
val id: Int,
@SerializedName("nickname")
val nickname: String,
@SerializedName("avatar")
val avatar: String
)
/**
* 房间规则详情
* @param id 规则ID
* @param rule 规则内容
* @param creator 创建者信息(可能为 null
* @param creatorType 创建者类型(如 "user"
* @param roomId 所属房间ID
* @param createdAt 创建时间ISO 8601 格式)
* @param updatedAt 更新时间ISO 8601 格式)
*/
data class RoomRule(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String,
@SerializedName("creator")
val creator: RoomRuleCreator?,
@SerializedName("creatorType")
val creatorType: String,
@SerializedName("roomId")
val roomId: Int,
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String
)
/**
* 房间规则列表响应
* @param page 当前页码
* @param pageSize 每页数量
* @param total 总记录数
* @param list 规则列表
*/
data class RoomRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Long,
@SerializedName("list")
val list: List<RoomRule>
)
/**
* 房间规则配额信息
* @param baseMaxCount 基础条数限制(系统配置的免费配额)
* @param purchasedCount 用户购买的额外条数(通过自动扩容机制)
* @param totalMaxCount 总可用条数baseMaxCount + purchasedCount
* @param currentCount 当前已创建的规则条数(该用户在该房间)
* @param remainingCount 剩余可用条数totalMaxCount - currentCount
* @param usagePercent 使用百分比0-100
*/
data class RoomRuleQuota(
@SerializedName("baseMaxCount")
val baseMaxCount: Int,
@SerializedName("purchasedCount")
val purchasedCount: Int,
@SerializedName("totalMaxCount")
val totalMaxCount: Int,
@SerializedName("currentCount")
val currentCount: Int,
@SerializedName("remainingCount")
val remainingCount: Int,
@SerializedName("usagePercent")
val usagePercent: Double
)
/**
* 积分不足错误响应
*
* 当创建房间规则时积分不足,服务器会返回此特殊错误格式。
* 包含当前余额和所需金额信息,便于客户端提示用户。
*
* @param success 是否成功,固定为 false
* @param code 错误码,固定为 35600积分不足
* @param message 错误消息,包含所需积分和当前余额的详细说明
* @param err 错误类型,固定为 "InsufficientBalance"
* @param currentBalance 当前积分余额
* @param requiredAmount 所需积分数量
* @param traceId 追踪ID可选
*/
data class InsufficientBalanceError(
@SerializedName("success")
val success: Boolean,
@SerializedName("code")
val code: Int,
@SerializedName("message")
val message: String,
@SerializedName("err")
val err: String,
@SerializedName("currentBalance")
val currentBalance: Int,
@SerializedName("requiredAmount")
val requiredAmount: Int,
@SerializedName("traceId")
val traceId: String?
)
interface RaveNowAPI {
@GET("membership/config")
@retrofit2.http.Headers("X-Requires-Auth: true")
@@ -992,7 +1136,7 @@ interface RaveNowAPI {
@Query("pageSize") pageSize: Int? = null
): Response<ListContainer<Agent>>
// ========== Prompt Rule API ==========
// ========== Agent Rule API ==========
/**
* 创建智能体规则
@@ -1020,8 +1164,8 @@ interface RaveNowAPI {
* ```
*/
@POST("outside/prompt/rule")
suspend fun createPromptRule(
@Body body: CreatePromptRuleRequestBody
suspend fun createAgentRule(
@Body body: CreateAgentRuleRequestBody
): Response<Unit>
/**
@@ -1054,8 +1198,8 @@ interface RaveNowAPI {
* ```
*/
@retrofit2.http.PUT("outside/prompt/rule")
suspend fun updatePromptRule(
@Body body: UpdatePromptRuleRequestBody
suspend fun updateAgentRule(
@Body body: UpdateAgentRuleRequestBody
): Response<Unit>
/**
@@ -1079,7 +1223,7 @@ interface RaveNowAPI {
* ```
*/
@DELETE("outside/prompt/rule/{id}")
suspend fun deletePromptRule(
suspend fun deleteAgentRule(
@Path("id") id: Int
): Response<Unit>
@@ -1134,12 +1278,12 @@ interface RaveNowAPI {
* ```
*/
@GET("outside/prompt/{promptId}/rule/list")
suspend fun getPromptRuleList(
suspend fun getAgentRuleList(
@Path("promptId") promptId: String,
@Query("rule") rule: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<DataContainer<PromptRuleListResponse>>
): Response<DataContainer<AgentRuleListResponse>>
/**
* 查询智能体规则配额信息
@@ -1189,9 +1333,264 @@ interface RaveNowAPI {
* ```
*/
@GET("outside/prompt/{promptId}/rule/count")
suspend fun getPromptRuleQuota(
suspend fun getAgentRuleQuota(
@Path("promptId") promptId: String
): Response<DataContainer<PromptRuleQuota>>
): Response<DataContainer<AgentRuleQuota>>
// ========== Room Rule API ==========
/**
* 创建房间规则
*
* 功能说明:
* - 为指定的房间创建一条新规则
* - 规则会被添加到AI对话上下文中用于约束房间内的行为和交互方式
* - 必须是房间成员或房间创建者才能创建规则
* - 每个用户在每个房间有基础条数限制,达到限制后会自动扣费扩容
* - 如果积分不足,会返回特殊的错误响应(错误码 35600
*
* @param body 创建规则请求体
* - rule: 规则内容,不能为空
* - roomId: 房间ID与 trtcId 二选一)
* - trtcId: TRTC房间ID与 roomId 二选一)
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 400 (40002): 规则内容为空
* - 400 (40003): 规则内容超出字数限制
* - 400 (35600): 积分不足(自动扩容失败),返回 InsufficientBalanceError 格式
* - 401 (40101): 未登录
* - 403 (40301): 无权限为该房间创建规则
* - 400 (40005): 房间不存在
*
* 示例:
* ```kotlin
* // 使用 roomId 创建规则
* val request1 = CreateRoomRuleRequestBody(
* rule = "禁止在房间内讨论政治话题",
* roomId = 123
* )
* val response1 = api.createRoomRule(request1)
*
* // 使用 trtcId 创建规则
* val request2 = CreateRoomRuleRequestBody(
* rule = "请使用文明用语",
* trtcId = "room_trtc_123"
* )
* val response2 = api.createRoomRule(request2)
*
* // 处理积分不足错误
* if (!response1.isSuccessful && response1.code() == 400) {
* val errorBody = response1.errorBody()?.string()
* // 解析为 InsufficientBalanceError 获取余额信息
* }
* ```
*/
@POST("outside/room/rule")
suspend fun createRoomRule(
@Body body: CreateRoomRuleRequestBody
): Response<Unit>
/**
* 修改房间规则
*
* 功能说明:
* - 修改指定的房间规则内容
* - 只能修改自己创建的规则creator_type 为 "user"
* - 修改不会增加规则条数,因此不会触发扣费
*
* @param body 修改规则请求体
* - id: 规则ID必填
* - rule: 新的规则内容,不能为空
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者
* - 规则必须是用户类型creator_type = "user"
*
* 错误响应说明:
* - 400 (40002): 规则内容为空
* - 400 (40003): 规则内容超出字数限制
* - 401 (40101): 未登录
* - 403 (40301): 无权限修改该规则
* - 404 (40401): 规则不存在
*
* 示例:
* ```kotlin
* val request = UpdateRoomRuleRequestBody(
* id = 456,
* rule = "请保持友善交流"
* )
* val response = api.updateRoomRule(request)
* ```
*/
@retrofit2.http.PUT("outside/room/rule")
suspend fun updateRoomRule(
@Body body: UpdateRoomRuleRequestBody
): Response<Unit>
/**
* 删除房间规则
*
* 功能说明:
* - 删除指定的房间规则
* - 只能删除自己创建的规则creator_type 为 "user"
* - 删除操作不可恢复,请谨慎操作
* - 删除规则不会退还已扣除的积分
*
* @param id 规则ID
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者
* - 规则必须是用户类型creator_type = "user"
*
* 错误响应说明:
* - 400 (40006): 无效的规则ID
* - 401 (40101): 未登录
* - 403 (40301): 无权限删除该规则
*
* 示例:
* ```kotlin
* val response = api.deleteRoomRule(456)
* ```
*/
@DELETE("outside/room/rule/{id}")
suspend fun deleteRoomRule(
@Path("id") id: Int
): Response<Unit>
/**
* 查询房间规则列表
*
* 功能说明:
* - 查询指定房间的规则列表,支持分页
* - 返回该房间所有用户创建的规则,包含创建者信息
* - 必须是房间成员或房间创建者才能查询
*
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
*
* @return 返回分页的规则列表,包含规则详情和创建者信息
*
* 响应数据说明:
* - page: 当前页码
* - pageSize: 每页数量
* - total: 总记录数
* - list: 规则列表
* - id: 规则ID
* - rule: 规则内容
* - creator: 创建者信息id, nickname, avatar
* - creatorType: 创建者类型("user" 等)
* - roomId: 所属房间ID
* - createdAt: 创建时间ISO 8601格式
* - updatedAt: 更新时间ISO 8601格式
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 401 (40101): 未登录
* - 403 (40301): 无权限查看该房间的规则
*
* 示例:
* ```kotlin
* // 使用 roomId 查询
* val response1 = api.getRoomRuleList(
* roomId = 123,
* page = 1,
* pageSize = 10
* )
*
* // 使用 trtcId 查询
* val response2 = api.getRoomRuleList(
* trtcId = "room_trtc_123",
* page = 1,
* pageSize = 10
* )
*
* // 处理响应
* response1.body()?.let { result ->
* println("共 ${result.total} 条规则,当前第 ${result.page} 页")
* result.list.forEach { rule ->
* println("规则: ${rule.rule}, 创建者: ${rule.creator?.nickname}")
* }
* }
* ```
*/
@GET("outside/room/rule")
suspend fun getRoomRuleList(
@Query("roomId") roomId: Int? = null,
@Query("trtcId") trtcId: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<RoomRuleListResponse>
/**
* 查询规则额度使用情况
*
* 功能说明:
* - 查询当前用户在指定房间的规则额度使用情况
* - 包括基础条数、已购买条数、当前使用数等完整信息
* - 用于判断用户是否还能创建新规则
* - 必须是房间成员或房间创建者才能查询
*
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
*
* @return 返回配额详细信息
*
* 响应数据说明:
* - baseMaxCount: 基础条数限制(系统配置的免费配额)
* - purchasedCount: 用户购买的额外条数(通过自动扩容机制)
* - totalMaxCount: 总可用条数baseMaxCount + purchasedCount
* - currentCount: 当前已创建的规则条数(该用户在该房间)
* - remainingCount: 剩余可用条数totalMaxCount - currentCount
* - usagePercent: 使用百分比0-100currentCount / totalMaxCount * 100
*
* 使用场景:
* 1. 创建规则前检查是否有足够配额
* 2. 展示规则使用情况统计
* 3. 提示用户即将达到配额上限
*
* 错误响应说明:
* - 400 (40001): 缺少房间标识(必须提供 roomId 或 trtcId
* - 401 (40101): 未登录
* - 403 (40301): 无权限查看该房间的规则条数信息
*
* 示例:
* ```kotlin
* // 使用 roomId 查询
* val response1 = api.getRoomRuleQuota(roomId = 123)
*
* // 使用 trtcId 查询
* val response2 = api.getRoomRuleQuota(trtcId = "room_trtc_123")
*
* // 处理响应
* response1.body()?.data?.let { quota ->
* if (quota.remainingCount > 0) {
* // 可以创建新规则
* println("还可以创建 ${quota.remainingCount} 条规则")
* println("使用率: ${quota.usagePercent}%")
* } else {
* // 配额已用完,需要扩容
* println("规则配额已用完,已使用 ${quota.currentCount}/${quota.totalMaxCount}")
* println("创建新规则将自动扣除积分进行扩容")
* }
* }
* ```
*/
@GET("outside/room/rule/length-info")
suspend fun getRoomRuleQuota(
@Query("roomId") roomId: Int? = null,
@Query("trtcId") trtcId: String? = null
): Response<DataContainer<RoomRuleQuota>>
}

View File

@@ -7,18 +7,13 @@ import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.AgentService
import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.http.Part
import java.io.File
import java.io.IOException
@@ -160,7 +155,7 @@ class AgentBackend {
return if (authorId != null) {
// getAgent 返回 DataContainer<ListContainer<Agent>>
val dataContainer =
body as com.aiosman.ravenow.data.DataContainer<com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>>
body as DataContainer<ListContainer<Agent>>
val listContainer = dataContainer.data
ListContainer(
total = listContainer.total,
@@ -171,7 +166,7 @@ class AgentBackend {
} else {
// getMyAgent 返回 ListContainer<Agent>
val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>
body as ListContainer<Agent>
ListContainer(
total = listContainer.total,
page = pageNumber,
@@ -251,7 +246,7 @@ class AgentLoader : DataLoader<AgentEntity, AgentLoaderExtraArgs>() {
} else {
// getMyAgent 返回 ListContainer<Agent>
val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>
body as ListContainer<Agent>
ListContainer(
list = listContainer.list.map { it.toAgentEntity() },
total = listContainer.total,

View File

@@ -9,7 +9,7 @@ import com.aiosman.ravenow.data.ListContainer
abstract class DataLoader<T,ET> {
var list: MutableList<T> = mutableListOf()
var page by mutableStateOf(1)
var total by mutableStateOf(0)
var total by mutableStateOf(0L)
var pageSize by mutableStateOf(10)
var hasNext by mutableStateOf(true)
var isLoading by mutableStateOf(false)

View File

@@ -3,10 +3,6 @@ package com.aiosman.ravenow.entity
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
/**
* 群聊房间
@@ -56,6 +52,40 @@ data class ProfileEntity(
val aiAccount: Boolean,
)
/**
* 房间规则创建者信息
*/
data class RoomRuleCreatorEntity(
val id: Int,
val nickname: String,
val avatar: String
)
/**
* 房间规则详情
*/
data class RoomRuleEntity(
val id: Int,
val rule: String,
val creator: RoomRuleCreatorEntity?,
val creatorType: String,
val roomId: Int,
val createdAt: String,
val updatedAt: String
)
/**
* 房间规则配额信息
*/
data class RoomRuleQuotaEntity(
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double
)
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
override suspend fun fetchData(
page: Int,

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.ui.group
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -8,6 +9,10 @@ import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CreatePromptRuleRequestBody
import com.aiosman.ravenow.data.api.PromptRule
import com.aiosman.ravenow.data.api.PromptRuleQuota
import com.aiosman.ravenow.data.parseErrorResponse
import com.aiosman.ravenow.entity.ChatNotification
import com.aiosman.ravenow.entity.GroupInfo
import com.aiosman.ravenow.entity.GroupMember
@@ -22,9 +27,36 @@ class GroupChatInfoViewModel(
var isLoading by mutableStateOf(false)
var error by mutableStateOf<String?>(null)
var chatNotification by mutableStateOf<ChatNotification?>(null)
var isAddingMemory by mutableStateOf(false)
var addMemoryError by mutableStateOf<String?>(null)
var addMemorySuccess by mutableStateOf(false)
val notificationStrategy get() = chatNotification?.strategy ?: "default"
// 记忆管理相关状态
var memoryQuota by mutableStateOf<PromptRuleQuota?>(null)
var memoryList by mutableStateOf<List<PromptRule>>(emptyList())
var isLoadingMemory by mutableStateOf(false)
var memoryError by mutableStateOf<String?>(null)
var promptOpenId by mutableStateOf<String?>(null)
init {
loadGroupInfo()
loadPromptOpenId()
}
/**
* 获取群聊中智能体的 OpenID
*/
private fun loadPromptOpenId() {
viewModelScope.launch {
try {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
promptOpenId = prompts?.firstOrNull()?.openId
} catch (e: Exception) {
Log.e("GroupChatInfoViewModel", "获取智能体OpenID失败: ${e.message}", e)
}
}
}
suspend fun updateNotificationStrategy(strategy: String) {
val result = ChatState.updateChatNotification(groupId.hashCode(), strategy)
@@ -71,4 +103,218 @@ class GroupChatInfoViewModel(
}
}
}
/**
* 添加群记忆
* @param memoryText 记忆内容
* @param promptOpenId 智能体的 OpenID可选如果不提供则从群聊信息中获取
*/
fun addGroupMemory(memoryText: String, promptOpenId: String? = null) {
viewModelScope.launch {
try {
isAddingMemory = true
addMemoryError = null
addMemorySuccess = false
// 如果没有提供 promptOpenId需要先获取群聊的智能体信息
val openId = promptOpenId ?: run {
// 通过 createGroupChatAi 接口获取群聊详细信息(包含 prompts
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
if (prompts.isNullOrEmpty()) {
throw Exception("群聊中没有找到智能体,无法添加记忆")
}
// 使用第一个智能体的 openId
prompts.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
}
if (openId.isBlank()) {
throw Exception("智能体ID不能为空")
}
// 创建智能体规则(群记忆)
val requestBody = CreatePromptRuleRequestBody(
rule = memoryText,
openId = openId
)
val response = ApiClient.api.createPromptRule(requestBody)
if (response.isSuccessful) {
addMemorySuccess = true
Log.d("GroupChatInfoViewModel", "群记忆添加成功")
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "添加群记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
addMemoryError = e.message ?: "添加群记忆失败"
Log.e("GroupChatInfoViewModel", "添加群记忆失败: ${e.message}", e)
} finally {
isAddingMemory = false
}
}
}
/**
* 获取记忆配额信息
*/
fun loadMemoryQuota(openId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
val quotaResponse = ApiClient.api.getPromptRuleQuota(fetchedOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
} else {
val quotaResponse = ApiClient.api.getPromptRuleQuota(targetOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取配额信息失败"
Log.e("GroupChatInfoViewModel", "获取配额信息失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 获取记忆列表
*/
fun loadMemoryList(openId: String? = null, page: Int = 1, pageSize: Int = 20) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
val listResponse = ApiClient.api.getPromptRuleList(fetchedOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList()
} else {
throw Exception("获取记忆列表失败: ${listResponse.code()}")
}
} else {
val listResponse = ApiClient.api.getPromptRuleList(targetOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList()
} else {
throw Exception("获取记忆列表失败: ${listResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取记忆列表失败"
Log.e("GroupChatInfoViewModel", "获取记忆列表失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 删除记忆
*/
fun deleteMemory(ruleId: Int) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val response = ApiClient.api.deletePromptRule(ruleId)
if (response.isSuccessful) {
// 刷新记忆列表和配额
promptOpenId?.let { openId ->
loadMemoryQuota(openId)
loadMemoryList(openId)
}
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "删除记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "删除记忆失败"
Log.e("GroupChatInfoViewModel", "删除记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 更新记忆
*/
fun updateMemory(ruleId: Int, newRuleText: String, targetOpenId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val openId = targetOpenId ?: promptOpenId
?: throw Exception("无法获取智能体ID")
val requestBody = com.aiosman.ravenow.data.api.UpdatePromptRuleRequestBody(
id = ruleId,
rule = newRuleText,
openId = openId
)
val response = ApiClient.api.updatePromptRule(requestBody)
if (response.isSuccessful) {
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "更新记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "更新记忆失败"
Log.e("GroupChatInfoViewModel", "更新记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
}

View File

@@ -0,0 +1,625 @@
package com.aiosman.ravenow.ui.group
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
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 com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
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 androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import android.widget.Toast
import androidx.compose.ui.graphics.Brush
@Composable
fun GroupMemoryManageContent(
groupId: String,
viewModel: GroupChatInfoViewModel,
onAddMemoryClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.95f
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
.fillMaxWidth()
.height(sheetHeight)
.background(Color(0xFFFAF9FB))
) {
// 顶部栏:返回按钮 + 标题 + 加号按钮
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp)
) {
// 中间标题 - 绝对居中,不受其他组件影响
Text(
text = "记忆管理",
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(
"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 ->
MemoryItem(
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 = "点击上方按钮添加群记忆",
style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal)
)
}
}
}
}
}
/**
* 编辑记忆对话框
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditGroupMemoryDialog(
memory: com.aiosman.ravenow.data.api.PromptRule,
viewModel: GroupChatInfoViewModel,
onDismiss: () -> Unit,
onUpdateMemory: (String) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
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)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
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 { onDismiss() },
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) {
onUpdateMemory(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
}
}
/**
* 记忆项组件
*/
@Composable
fun MemoryItem(
memory: com.aiosman.ravenow.data.api.PromptRule,
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.Bottom
) {
// 日期文本 - 左侧
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))
)
}
}
}
}
}

View File

@@ -126,7 +126,7 @@ fun ProfileV3(
onLike: (MomentEntity) -> Unit = {},
onComment: (MomentEntity) -> Unit = {},
onAgentClick: (AgentEntity) -> Unit = {},
postCount: Int? = null, // 新增参数用于传递帖子总数
postCount: Long? = null, // 新增参数用于传递帖子总数
) {
val model = MyProfileViewModel
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
@@ -370,7 +370,7 @@ fun ProfileV3(
profile?.let {
UserItem(
accountProfileEntity = it,
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size.toLong()
)
}
}

View File

@@ -33,7 +33,7 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun UserItem(
accountProfileEntity: AccountProfileEntity,
postCount: Int = 0
postCount: Long = 0
) {
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

View File

@@ -262,5 +262,38 @@
<string name="follow_system">フォローアップシステム</string>
<string name="message_notification">メッセージ通知</string>
<string name="logout_confirm">ログアウト</string>
<!-- Group Chat Info -->
<string name="group_chat_info_title">グループチャット情報</string>
<string name="group_chat_info_add_member">メンバーを追加</string>
<string name="group_chat_info_notification">通知</string>
<string name="group_chat_info_share">共有</string>
<string name="group_chat_info_unlock_extension">グループ拡張機能を解除</string>
<string name="group_chat_info_group_memory">グループメモリ</string>
<string name="group_chat_info_memory_description">AIは記憶に基づいてグループチャットであなたをより理解します</string>
<string name="group_chat_info_add_memory">メモリを追加</string>
<string name="group_chat_info_memory_manage">メモリ管理</string>
<string name="group_chat_info_group_settings">グループ設定</string>
<string name="group_chat_info_group_visibility">グループの可視性</string>
<string name="group_chat_info_locked">ロック中</string>
<string name="group_chat_info_member_manage">メンバー管理</string>
<string name="group_chat_info_wallpaper">グループチャット壁紙</string>
<string name="group_chat_info_dissolve">グループチャットを解散</string>
<string name="group_chat_info_add_group_memory">グループメモリを追加</string>
<string name="group_chat_info_member_count">%d人のメンバー</string>
<string name="group_chat_info_memory_input_hint">グループの記憶内容を入力、例えば、グループのメンバーは科学技術と設計を議論するのが好きである....</string>
<string name="group_chat_info_memory_cost">メモリを追加すると20コインを消費します</string>
<string name="group_chat_info_memory_optimization">AIは記憶に基づいて返信を最適化します</string>
<string name="group_chat_info_memory_editable">いつでも編集または削除できます</string>
<string name="group_chat_info_memory_add_success">グループメモリが正常に追加されました</string>
<string name="group_chat_info_permission_settings">グループ権限設定</string>
<string name="group_chat_info_public_group">公開グループ</string>
<string name="group_chat_info_public_group_desc">誰でも検索して参加できます</string>
<string name="group_chat_info_private_group">プライベートグループ</string>
<string name="group_chat_info_private_group_desc">招待のみ</string>
<string name="group_chat_info_private_group_cost">50コイン</string>
<string name="group_chat_info_balance">残高: %1$dコイン</string>
<string name="group_chat_info_unlock_cost">アンロック費用: %1$dコイン</string>
<string name="group_chat_info_done">完了</string>
<string name="group_chat_info_recharge_hint">チャージしてより多くのコインを獲得できます</string>
</resources>

View File

@@ -251,6 +251,40 @@
<string name="select_apply_to_use_theme">选择"应用"可选中这个主题</string>
<string name="tap_cancel_to_preview_other_themes">轻触"取消"可预览其他主题</string>
<!-- Group Chat Info -->
<string name="group_chat_info_title">群聊信息</string>
<string name="group_chat_info_add_member">添加成员</string>
<string name="group_chat_info_notification">通知</string>
<string name="group_chat_info_share">分享</string>
<string name="group_chat_info_unlock_extension">解锁群扩展</string>
<string name="group_chat_info_group_memory">群记忆</string>
<string name="group_chat_info_memory_description">AI 会根据记忆在群聊里更懂你</string>
<string name="group_chat_info_add_memory">添加记忆</string>
<string name="group_chat_info_memory_manage">记忆管理</string>
<string name="group_chat_info_group_settings">群资料设置</string>
<string name="group_chat_info_group_visibility">群可见性</string>
<string name="group_chat_info_locked">待解锁</string>
<string name="group_chat_info_member_manage">成员管理</string>
<string name="group_chat_info_wallpaper">群聊壁纸</string>
<string name="group_chat_info_dissolve">解散群聊</string>
<string name="group_chat_info_add_group_memory">添加群记忆</string>
<string name="group_chat_info_member_count">%d 位成员</string>
<string name="group_chat_info_memory_input_hint">输入群记忆内容,例如:群成员喜欢讨论科技和设计....</string>
<string name="group_chat_info_memory_cost">添加记忆需消耗 20 派币</string>
<string name="group_chat_info_memory_optimization">AI 将基于记忆优化回复</string>
<string name="group_chat_info_memory_editable">可随时编辑或删除</string>
<string name="group_chat_info_memory_add_success">群记忆添加成功</string>
<string name="group_chat_info_permission_settings">群权限设置</string>
<string name="group_chat_info_public_group">公开群组</string>
<string name="group_chat_info_public_group_desc">任何人都可搜索并加入</string>
<string name="group_chat_info_private_group">私密群组</string>
<string name="group_chat_info_private_group_desc">仅限邀请加入</string>
<string name="group_chat_info_private_group_cost">50 派币</string>
<string name="group_chat_info_balance">余额: %1$d 派币</string>
<string name="group_chat_info_unlock_cost">解锁费用: %1$d 派币</string>
<string name="group_chat_info_done">完成</string>
<string name="group_chat_info_recharge_hint">可通过充值获得更多派币</string>
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI 类型</string>
<string name="zodiac">星座</string>

View File

@@ -246,7 +246,39 @@
<string name="select_apply_to_use_theme">Select "Apply" to use this theme</string>
<string name="tap_cancel_to_preview_other_themes">Tap "Cancel" to preview other themes</string>
<!-- Group Chat Info -->
<string name="group_chat_info_title">Group Chat Info</string>
<string name="group_chat_info_add_member">Add Member</string>
<string name="group_chat_info_notification">Notification</string>
<string name="group_chat_info_share">Share</string>
<string name="group_chat_info_unlock_extension">Unlock Group Extension</string>
<string name="group_chat_info_group_memory">Group Memory</string>
<string name="group_chat_info_memory_description">AI will understand you better in group chat based on memory</string>
<string name="group_chat_info_add_memory">Add Memory</string>
<string name="group_chat_info_memory_manage">Memory Management</string>
<string name="group_chat_info_group_settings">Group Settings</string>
<string name="group_chat_info_group_visibility">Group Visibility</string>
<string name="group_chat_info_locked">Locked</string>
<string name="group_chat_info_member_manage">Member Management</string>
<string name="group_chat_info_wallpaper">Group Chat Wallpaper</string>
<string name="group_chat_info_dissolve">Dissolve Group Chat</string>
<string name="group_chat_info_add_group_memory">Add Group Memory</string>
<string name="group_chat_info_member_count">%d members</string>
<string name="group_chat_info_memory_input_hint">Input group memory content, for example: Group members enjoy discussing technology and design …</string>
<string name="group_chat_info_memory_cost">Adding memory consumes 20 coins</string>
<string name="group_chat_info_memory_optimization">AI will optimize replies based on memory</string>
<string name="group_chat_info_memory_editable">Can be edited or deleted at any time</string>
<string name="group_chat_info_memory_add_success">Group memory added successfully</string>
<string name="group_chat_info_permission_settings">Group Permission Settings</string>
<string name="group_chat_info_public_group">Public Group</string>
<string name="group_chat_info_public_group_desc">Anyone can search and join</string>
<string name="group_chat_info_private_group">Private Group</string>
<string name="group_chat_info_private_group_desc">Invitation only</string>
<string name="group_chat_info_private_group_cost">50 coins</string>
<string name="group_chat_info_balance">Balance: %1$d coins</string>
<string name="group_chat_info_unlock_cost">Unlock cost: %1$d coins</string>
<string name="group_chat_info_done">Done</string>
<string name="group_chat_info_recharge_hint">You can recharge to get more coins</string>
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI</string>
<string name="zodiac">Zodiac</string>

0
gradlew vendored Normal file → Executable file
View File