feat(task): 优化任务列表和审批功能

- 更新TaskExtraItem数据结构,将index/id/processStatus/aiDescription字段
  替换为taskId/desc/numberNo字段
- 在Service中新增pushToNext、recommendHandler、recommendBackNode、backTo
  等工作流相关API接口
- 在ReviewActivity中实现完整的审批流程,包括同意和驳回操作
- 在TaskListActivity中实现可视区域任务数据同步和动态索引更新
- 优化语音识别的额外数据提供机制,支持当前可见任务列表的准确传递
- 添加工作流实例ID和活动ID的参数传递功能
这个提交包含在:
徐勤民 2026-04-18 15:00:58 +08:00
父节点 f20255947c
当前提交 d6eb9bd3bd
共有 4 个文件被更改,包括 244 次插入30 次删除

查看文件

@ -4,7 +4,7 @@ package com.nova.brain.glass.model.data
// "text": "查看下C919第45架机的零部件采购情况",
// "scence": "home",
// "extra": [
// { "index": 1, "id": "xxx", "taskType": "审核任务", "processStatus": "进行中", "aiDescription": "..." }
// { "taskId": "xxx", "taskType": "审核任务", "desc": "...", "numberNo": 1 }
// ],
// "actions": ["goToDecisionCenter"]
//}
@ -17,9 +17,8 @@ data class RecognizeData(
// list 场景下传给服务端,帮助 NLP 定位用户所说的任务
data class TaskExtraItem(
val index: Int, // 列表中的显示序号1-based
val id: String,
val taskId: String,
val taskType: String,
val processStatus: String,
val aiDescription: String
val desc: String,
val numberNo: Int // 当前可见列表中的显示序号1-based
)

查看文件

@ -2,7 +2,16 @@ package com.nova.brain.glass.repository
import com.nova.brain.glass.model.RecognizeModel
import com.nova.brain.glass.model.data.ChatData
import com.nova.brain.glass.model.data.ApiResponse
import com.nova.brain.glass.model.data.BackToData
import com.nova.brain.glass.model.data.BackToRequest
import com.nova.brain.glass.model.data.RecognizeData
import com.nova.brain.glass.model.data.PushToNextData
import com.nova.brain.glass.model.data.PushToNextRequest
import com.nova.brain.glass.model.data.RecommendBackNodeData
import com.nova.brain.glass.model.data.RecommendBackNodeRequest
import com.nova.brain.glass.model.data.RecommendHandlerData
import com.nova.brain.glass.model.data.RecommendHandlerRequest
import com.nova.brain.glass.model.data.TaskListData
import com.nova.brain.glass.model.TaskSearchResponse
import com.nova.brain.glass.model.data.TopicData
@ -12,6 +21,7 @@ import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Streaming
@ -36,4 +46,16 @@ interface Service {
@POST("/cbrain-gateway/cbrain-task-server/cbrain-task/task/glassesTaskSearch")
fun glassesTaskSearch(@Body body: TaskListData): Observable<TaskSearchResponse>
@POST("/cbrain-gateway/cbrain-execute/cbrain-execute/workflow/instance/pushToNext")
fun pushToNext(@Body body: PushToNextRequest): Observable<ApiResponse<PushToNextData>>
@POST("/cbrain-gateway/cbrain-execute/cbrain-execute/glasses/recommendHandler")
fun recommendHandler(@Body body: RecommendHandlerRequest): Observable<ApiResponse<RecommendHandlerData>>
@POST("/cbrain-gateway/cbrain-execute/cbrain-execute/glasses/recommendBackNode")
fun recommendBackNode(@Body body: RecommendBackNodeRequest): Observable<ApiResponse<RecommendBackNodeData>>
@POST("/cbrain-gateway/cbrain-execute/cbrain-execute/workflow/instance/backTo")
fun backTo(@Body body: BackToRequest): Observable<ApiResponse<BackToData>>
}

查看文件

@ -6,11 +6,22 @@ import com.nova.brain.glass.databinding.ActivityReviewBinding
import com.nova.brain.glass.helper.OfflineCmdListener
import com.nova.brain.glass.helper.OfflineCmdServiceHelper
import com.nova.brain.glass.model.ItemItem
import com.nova.brain.glass.model.data.BackToRequest
import com.nova.brain.glass.model.data.PushToNextRequest
import com.nova.brain.glass.model.data.RecommendBackNodeRequest
import com.nova.brain.glass.model.data.RecommendHandlerRequest
import com.nova.brain.glass.repository.Service
import com.nova.brain.glass.viewmodel.ItemListVM
import com.xuqm.base.adapter.BasePagedAdapter
import com.xuqm.base.adapter.CommonPagedAdapter
import com.xuqm.base.adapter.ViewHolder
import com.xuqm.base.di.manager.HttpManager
import com.xuqm.base.extensions.showMessage
import com.xuqm.base.ui.BaseListFormLayoutNormalActivity
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
class ReviewActivity : BaseListFormLayoutNormalActivity<ItemItem, ItemListVM, ActivityReviewBinding>() {
override fun getLayoutId(): Int = R.layout.activity_review
@ -18,6 +29,17 @@ class ReviewActivity : BaseListFormLayoutNormalActivity<ItemItem, ItemListVM, Ac
override fun getRecyclerOrientation(): Int = RecyclerView.HORIZONTAL
private var isSubmitting = false
private var submitDisposable: Disposable? = null
private val workProcessInstanceId: String by lazy {
intent.getStringExtra("workProcessInstanceId").orEmpty()
}
private val currentActivityId: String by lazy {
intent.getStringExtra("currentActivityId").orEmpty()
}
private val listener = object : OfflineCmdListener {
override fun onOfflineCmd(cmd: String) {
runOnUiThread {
@ -26,18 +48,21 @@ class ReviewActivity : BaseListFormLayoutNormalActivity<ItemItem, ItemListVM, Ac
finish()
}
"驳回", "拒绝", "不同意" -> {
finish()
submitBack()
}
"同意","通过"->{
finish()
"同意", "通过", "拟同意" -> {
submitAgree()
}
}
}
}
}
override fun initData() {
super.initData()
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding.tvTaskHeader.text = intent.getStringExtra("taskType").orEmpty()
binding.content.text = intent.getStringExtra("content").orEmpty()
}
override fun onResume() {
@ -53,26 +78,140 @@ class ReviewActivity : BaseListFormLayoutNormalActivity<ItemItem, ItemListVM, Ac
}
override fun onDestroy() {
submitDisposable?.dispose()
super.onDestroy()
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun submitAgree() {
if (isSubmitting) return
if (workProcessInstanceId.isBlank() || currentActivityId.isBlank()) {
"缺少流程参数,无法同意".showMessage()
return
}
isSubmitting = true
submitDisposable?.dispose()
val service = HttpManager.getApi(Service::class.java)
submitDisposable = service.pushToNext(
PushToNextRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId,
needHandle = 1,
innerRequest = true
)
).flatMap { response ->
if (response.status != 200 || response.data == null) {
return@flatMap Observable.error(
IllegalStateException(if (response.msg.isBlank()) "同意失败" else response.msg)
)
}
val needHandlerActivityIds = response.data.result.orEmpty().mapNotNull { it.activityId }.distinct()
if (needHandlerActivityIds.isEmpty()) {
Observable.just(emptyList())
} else {
service.recommendHandler(
RecommendHandlerRequest(
workProcessInstanceId = workProcessInstanceId,
needHandlerActivityIds = needHandlerActivityIds
)
).flatMap { recommendResponse ->
if (recommendResponse.status != 200 || recommendResponse.data == null) {
return@flatMap Observable.error(
IllegalStateException(
if (recommendResponse.msg.isBlank()) "推荐处理人失败" else recommendResponse.msg
)
)
}
Observable.just(recommendResponse.data.selectedHandler.orEmpty())
}
}
}.flatMap { selectedHandler ->
service.pushToNext(
PushToNextRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId,
needHandle = 0,
innerRequest = true,
comment = "拟同意",
selectedHandler = selectedHandler
)
)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { isSubmitting = false }
.subscribe({ response ->
if (response.status == 200) {
"已同意".showMessage()
finish()
} else {
(if (response.msg.isBlank()) "同意失败" else response.msg).showMessage()
}
}, { e ->
(e.message ?: "同意失败").showMessage()
})
}
private fun submitBack() {
if (isSubmitting) return
if (workProcessInstanceId.isBlank() || currentActivityId.isBlank()) {
"缺少流程参数,无法回退".showMessage()
return
}
isSubmitting = true
submitDisposable?.dispose()
val service = HttpManager.getApi(Service::class.java)
submitDisposable = service.recommendBackNode(
RecommendBackNodeRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId
)
).flatMap { response ->
if (response.status != 200 || response.data?.backToActivityId.isNullOrBlank()) {
return@flatMap Observable.error(
IllegalStateException(if (response.msg.isBlank()) "推荐回退节点失败" else response.msg)
)
}
service.backTo(
BackToRequest(
workProcessInstanceId = workProcessInstanceId,
currentActivityId = currentActivityId,
backToActivityId = response.data?.backToActivityId.orEmpty(),
comment = "驳回,请线下沟通原因"
)
)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { isSubmitting = false }
.subscribe({ response ->
if (response.status == 200) {
"已回退".showMessage()
finish()
} else {
(if (response.msg.isBlank()) "回退失败" else response.msg).showMessage()
}
}, { e ->
(e.message ?: "回退失败").showMessage()
})
}
private val adapter = object : CommonPagedAdapter<ItemItem>(R.layout.item_item) {
override fun convert(holder: ViewHolder, item: ItemItem, position: Int) {
holder
.setText(R.id.text, item.text)
.setClickListener(R.id.text
) {
.setClickListener(R.id.text) {
when (item.text) {
"同意" -> {
finish()
submitAgree()
}
"拒绝" -> {
finish()
submitBack()
}
}
}
}
}
override fun adapter(): BasePagedAdapter<ItemItem> = adapter
}

查看文件

@ -26,6 +26,7 @@ class TaskListActivity :
override fun getLayoutId(): Int = R.layout.activity_task_list
override fun fullscreen(): Boolean = true
private var pageStartPosition = 0
private var visibleTaskExtras: List<TaskExtraItem> = emptyList()
companion object {
private val EXIT_CMDS = setOf("退出", "返回", "退回", "关闭")
@ -41,6 +42,15 @@ class TaskListActivity :
}
}
})
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
binding.baseRecyclerView.post { refreshVisibleOrder() }
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
binding.baseRecyclerView.post { refreshVisibleOrder() }
}
})
binding.baseRecyclerView.post {
refreshVisibleOrder()
}
@ -100,7 +110,9 @@ class TaskListActivity :
private fun refreshVisibleOrder() {
val lm = binding.baseRecyclerView.layoutManager as? LinearLayoutManager ?: return
val firstVisible = lm.findFirstVisibleItemPosition()
if (firstVisible == RecyclerView.NO_POSITION || firstVisible == pageStartPosition) return
if (firstVisible == RecyclerView.NO_POSITION) return
updateVisibleTaskExtras(lm, firstVisible)
if (firstVisible == pageStartPosition) return
pageStartPosition = firstVisible
adapter.notifyDataSetChanged()
}
@ -121,6 +133,20 @@ class TaskListActivity :
Intent(this, ReviewActivity::class.java)
.putExtra("taskId", item.id)
.putExtra("taskType", item.taskType)
.putExtra("content", item.aiDescription)
.putExtra(
"workProcessInstanceId",
item.params.firstNotBlank("processInstanceId", "work_process_instance_id")
)
.putExtra(
"currentActivityId",
item.params.firstNotBlank(
"currentActivityId",
"current_activity_id",
"activityId",
"activity_id"
)
)
)
else -> Log.d("TaskListActivity", "unknown taskType: ${item.taskType}")
}
@ -132,15 +158,7 @@ class TaskListActivity :
OfflineCmdServiceHelper.addOnLineListener(offlineCmdListener)
AsrHelper.scene = "list"
AsrHelper.extraProvider = {
viewModel.currentItems.mapIndexed { i, item ->
TaskExtraItem(
index = i + 1,
id = item.id,
taskType = item.taskType,
processStatus = item.processStatus,
aiDescription = item.aiDescription
)
}
if (visibleTaskExtras.isNotEmpty()) visibleTaskExtras else buildTaskExtra(viewModel.currentItems)
}
AsrHelper.onGoToDecisionCenter = { action ->
Log.d("TaskListActivity", "recognize result: $action")
@ -166,6 +184,42 @@ class TaskListActivity :
AsrHelper.onOpenTaskDetail = null
}
private fun updateVisibleTaskExtras(
lm: LinearLayoutManager,
firstVisible: Int = lm.findFirstVisibleItemPosition()
) {
val items = viewModel.currentItems
if (items.isEmpty()) {
visibleTaskExtras = emptyList()
return
}
val lastVisible = lm.findLastVisibleItemPosition()
if (firstVisible == RecyclerView.NO_POSITION || lastVisible == RecyclerView.NO_POSITION) {
visibleTaskExtras = buildTaskExtra(items)
return
}
val safeFirst = firstVisible.coerceAtLeast(0).coerceAtMost(items.lastIndex)
val safeLast = lastVisible.coerceAtLeast(0).coerceAtMost(items.lastIndex)
visibleTaskExtras = if (safeFirst <= safeLast) {
buildTaskExtra(items.subList(safeFirst, safeLast + 1))
} else {
buildTaskExtra(items)
}
}
private fun buildTaskExtra(items: List<TaskItem>): List<TaskExtraItem> =
items.mapIndexed { index, item ->
TaskExtraItem(
taskId = item.id,
taskType = item.taskType,
desc = item.aiDescription,
numberNo = index + 1
)
}
private fun Map<String, String>.firstNotBlank(vararg keys: String): String =
keys.firstNotNullOfOrNull { key -> this[key]?.takeIf { it.isNotBlank() } }.orEmpty()
private val adapter = object : CommonPagedAdapter<TaskItem>(R.layout.item_task_list) {
override fun convert(holder: ViewHolder, item: TaskItem, position: Int) {
val displayPosition = (position - pageStartPosition + 1).coerceAtLeast(1)