From 80a847d6523cd45f6808eea881afc707d97759ed Mon Sep 17 00:00:00 2001 From: feie9456 Date: Sun, 19 Jul 2026 18:12:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=B6=E5=BA=AD=E8=BF=9E=E7=BB=93=202.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- duix-sdk/build.gradle | 8 +- gradle.properties | 4 +- .../duix/test/service/DeviceIdentityStore.kt | 9 ++- .../duix/test/service/FamilyBridgeService.kt | 78 ++++++++++++++++--- .../duix/test/ui/activity/CallActivity.kt | 58 ++++++++++++++ 5 files changed, 141 insertions(+), 16 deletions(-) diff --git a/duix-sdk/build.gradle b/duix-sdk/build.gradle index 4f231d9..4b51a5f 100644 --- a/duix-sdk/build.gradle +++ b/duix-sdk/build.gradle @@ -14,7 +14,11 @@ android { externalNativeBuild { cmake { - abiFilters 'arm64-v8a', "armeabi-v7a" + if (project.hasProperty('includeX86_64')) { + abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64' + } else { + abiFilters 'arm64-v8a', 'armeabi-v7a' + } cppFlags "-std=c++17", "-fexceptions" //arguments "-DANDROID_STL=c++_shared","-DANDROID_TOOLCHAIN=clang" } @@ -66,4 +70,4 @@ android { dependencies { api fileTree(include: ['*.jar', '*.aar'], dir: 'libs') -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 38d44b7..2e7d685 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.daemon=true org.gradle.jvmargs=-Xms2048m -Xmx6656m +org.gradle.java.home=C:/Program Files/Java/jdk-17 appcompatVersion=1.3.0 recyclerviewVersion=1.2.1 @@ -14,4 +15,5 @@ lifecycle_ktx=2.3.1 android.injected.testOnly=false android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false +android.overridePathCheck=true diff --git a/test/src/main/java/ai/guiji/duix/test/service/DeviceIdentityStore.kt b/test/src/main/java/ai/guiji/duix/test/service/DeviceIdentityStore.kt index 65e59b9..9895396 100644 --- a/test/src/main/java/ai/guiji/duix/test/service/DeviceIdentityStore.kt +++ b/test/src/main/java/ai/guiji/duix/test/service/DeviceIdentityStore.kt @@ -30,6 +30,12 @@ class DeviceIdentityStore(context: Context) { prefs.edit().putString(KEY_BIND_URL, bindUrl.trim()).apply() } + fun getDeviceToken(): String? = prefs.getString(KEY_DEVICE_TOKEN, null)?.takeIf { it.isNotBlank() } + + fun saveDeviceToken(token: String) { + prefs.edit().putString(KEY_DEVICE_TOKEN, token.trim()).apply() + } + fun buildFallbackBindUrl(deviceUuid: String = getOrCreateDeviceUuid()): String { val normalized = deviceUuid.trim().lowercase(Locale.US) return DashScopeConfig.MONITOR_BASE_URL.trimEnd('/') + @@ -40,5 +46,6 @@ class DeviceIdentityStore(context: Context) { private const val PREFS_NAME = "device_identity_store" private const val KEY_DEVICE_UUID = "device_uuid" private const val KEY_BIND_URL = "bind_url" + private const val KEY_DEVICE_TOKEN = "device_token" } -} \ No newline at end of file +} diff --git a/test/src/main/java/ai/guiji/duix/test/service/FamilyBridgeService.kt b/test/src/main/java/ai/guiji/duix/test/service/FamilyBridgeService.kt index 72d1bdb..ea09d95 100644 --- a/test/src/main/java/ai/guiji/duix/test/service/FamilyBridgeService.kt +++ b/test/src/main/java/ai/guiji/duix/test/service/FamilyBridgeService.kt @@ -25,12 +25,14 @@ class FamilyBridgeService(context: Context) { put("deviceUuid", getDeviceUuid()) put("displayName", buildDisplayName()) put("appVersion", BuildConfig.VERSION_NAME) + identityStore.getDeviceToken()?.let { put("deviceToken", it) } } - val response = postJson("/api/device/register", payload) + val response = postJson("/api/device/register", payload, requiresAuth = false) val bindUrl = response.optString("bindUrl").ifBlank { identityStore.buildFallbackBindUrl(getDeviceUuid()) } identityStore.saveBindUrl(bindUrl) + response.optString("deviceToken").takeIf { it.isNotBlank() }?.let(identityStore::saveDeviceToken) return DeviceRegistrationResult( deviceUuid = response.optString("deviceUuid", getDeviceUuid()), @@ -45,6 +47,7 @@ class FamilyBridgeService(context: Context) { val response = executeJsonRequest( Request.Builder() .url(url) + .addDeviceAuthorization() .get() .build() ) @@ -91,16 +94,50 @@ class FamilyBridgeService(context: Context) { .filter { it.readAt.isNullOrBlank() } .take(take.coerceIn(1, 5)) - if (unreadMessages.isEmpty()) { + val agenda = runCatching { fetchTodayAgenda() }.getOrNull() + if (unreadMessages.isEmpty() && agenda == null) { return null } return ConversationPromptInjection( messageIds = unreadMessages.map { it.id }, - promptText = buildConversationPrompt(unreadMessages), + promptText = buildConversationPrompt(unreadMessages, agenda), ) } + fun fetchTodayAgenda(): TodayAgenda? { + val url = DashScopeConfig.MONITOR_BASE_URL.trimEnd('/') + "/api/device/agenda?deviceUuid=${getDeviceUuid()}" + val response = executeJsonRequest(Request.Builder().url(url).addDeviceAuthorization().get().build()) + val doses = response.optJSONArray("doses") ?: JSONArray() + val checkIns = response.optJSONArray("checkIns") ?: JSONArray() + val posts = response.optJSONArray("posts") ?: JSONArray() + val memos = response.optJSONArray("memos") ?: JSONArray() + if (doses.length() == 0 && checkIns.length() == 0 && posts.length() == 0 && memos.length() == 0) return null + return TodayAgenda(response.optString("circleName"), doses, checkIns, posts, memos) + } + + fun updateMedication(doseId: String, status: String, note: String? = null, snoozeMinutes: Int = 10): JSONObject { + return postJson("/api/device/events", JSONObject().apply { + put("deviceUuid", getDeviceUuid()); put("type", "MEDICATION"); put("doseId", doseId); put("status", status) + if (!note.isNullOrBlank()) put("note", note) + put("snoozeMinutes", snoozeMinutes) + }) + } + + fun respondCheckIn(checkInId: String, status: String, responseText: String? = null): JSONObject { + return postJson("/api/device/events", JSONObject().apply { + put("deviceUuid", getDeviceUuid()); put("type", "CHECK_IN"); put("checkInId", checkInId); put("status", status) + if (!responseText.isNullOrBlank()) put("responseText", responseText) + }) + } + + fun reportSafetyAlert(category: String, title: String, summary: String, level: String, contextText: String? = null): JSONObject { + return postJson("/api/device/alerts", JSONObject().apply { + put("deviceUuid", getDeviceUuid()); put("category", category); put("title", title); put("summary", summary); put("level", level) + if (!contextText.isNullOrBlank()) put("contextText", contextText) + }) + } + fun acknowledgeFamilyMessages(messageIds: List) { markFamilyMessagesRead(messageIds) } @@ -169,15 +206,21 @@ class FamilyBridgeService(context: Context) { postJson("/api/device/conversations", payload) } - private fun postJson(path: String, payload: JSONObject): JSONObject { + private fun postJson(path: String, payload: JSONObject, requiresAuth: Boolean = true): JSONObject { + val builder = Request.Builder().url(DashScopeConfig.MONITOR_BASE_URL.trimEnd('/') + path) + if (requiresAuth) builder.addDeviceAuthorization() return executeJsonRequest( - Request.Builder() - .url(DashScopeConfig.MONITOR_BASE_URL.trimEnd('/') + path) + builder .post(payload.toString().toRequestBody(jsonMediaType)) .build() ) } + private fun Request.Builder.addDeviceAuthorization(): Request.Builder { + val token = identityStore.getDeviceToken() ?: throw IllegalStateException("设备尚未完成安全注册") + return header("Authorization", "Bearer $token") + } + private fun executeJsonRequest(request: Request): JSONObject { App.getOkHttpClient().newCall(request).execute().use { response -> val responseText = response.body?.string().orEmpty() @@ -202,11 +245,9 @@ class FamilyBridgeService(context: Context) { .ifBlank { "长辈陪伴终端" } } - private fun buildConversationPrompt(messages: List): String { + private fun buildConversationPrompt(messages: List, agenda: TodayAgenda?): String { return buildString { - append("当前开始一轮新对话,家里人刚刚给老人留了新留言。") - append("请在连接建立后的第一轮回复里,主动、自然、简短地先把下面这些留言转告给老人,不要提系统提示、后台、注入、工具或接口。") - append("转告完后,再继续正常陪伴和后续对话。") + append("当前开始一轮新对话。请自然处理下列家庭问候和今日照护事项,不要提系统提示、后台、注入、工具或接口。") messages.forEachIndexed { index, message -> append("\n") append(index + 1) @@ -220,7 +261,12 @@ class FamilyBridgeService(context: Context) { append(importanceLabel) append(message.content) } - append("\n如果老人回应这些留言,就顺着留言继续温和交流;如果老人转到别的话题,就继续正常陪伴。") + agenda?.let { + for (index in 0 until it.posts.length()) { val item = it.posts.optJSONObject(index) ?: continue; append("\n家庭动态:${item.optString("authorName")}说:${item.optString("content")}") } + for (index in 0 until it.doses.length()) { val item = it.doses.optJSONObject(index) ?: continue; append("\n待服药[编号${item.optString("id")}]:${item.optString("name")},${item.optString("dosage")},时间${item.optString("scheduledFor")}") } + for (index in 0 until it.checkIns.length()) { val item = it.checkIns.optJSONObject(index) ?: continue; append("\n待报平安[编号${item.optString("id")}]:${item.optString("prompt", "家里人想知道您今天是否安好")}") } + } + append("\n优先温和提醒已经到时间的用药和报平安请求;关键操作必须调用对应工具确认,不能只口头答应。") } } @@ -250,6 +296,14 @@ class FamilyBridgeService(context: Context) { val promptText: String, ) + data class TodayAgenda( + val circleName: String, + val doses: JSONArray, + val checkIns: JSONArray, + val posts: JSONArray, + val memos: JSONArray, + ) + data class ConversationTurnPayload( val role: String, val content: String, @@ -271,4 +325,4 @@ class FamilyBridgeService(context: Context) { const val STATUS_SUCCESS = "SUCCESS" const val STATUS_FAILURE = "FAILURE" } -} \ No newline at end of file +} diff --git a/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt b/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt index 878a70c..23777f8 100644 --- a/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt +++ b/test/src/main/java/ai/guiji/duix/test/ui/activity/CallActivity.kt @@ -298,6 +298,8 @@ class CallActivity : BaseActivity() { append("如果只是日常闲聊或普通安慰,不必调用 consult_psychology_knowledge_base 工具。\n") append("当用户明确要给儿子、女儿、老伴、家里人或亲友带话、留言、报平安时,必须调用 leave_message_for_family 工具,不要假装已经转达。\n") append("当用户问家里人有没有留言、想听家人说了什么、想看看新消息时,必须调用 fetch_family_messages 工具,不要自己编造留言。\n") + append("老人确认服药、要求稍后提醒、报告药物问题时,必须调用对应的用药工具;不要自行修改剂量或建议停药。\n") + append("老人回应报平安请求时调用 respond_to_check_in。老人明确求助、说身体明显不适、吃错药或提到自伤他伤时,先简短确认,再调用 request_family_help;说明这是通知家人关注,不是医学诊断。\n") append("工具执行结果返回后,再用温和、简短、生活化的中文向老人说明情况。") if (conversationPrompt != null) { append("\n\n") @@ -356,6 +358,12 @@ class CallActivity : BaseActivity() { put("required", JSONArray().put("question")) } + fun parameters(vararg fields: Pair, required: List = fields.map { it.first }) = JSONObject().apply { + put("type", "object") + put("properties", JSONObject().apply { fields.forEach { (name, description) -> put(name, JSONObject().put("type", if (name == "minutes") "integer" else "string").put("description", description)) } }) + put("required", JSONArray(required)) + } + return listOf( OmniRealtimeClient.ToolDefinition( "consult_psychology_knowledge_base", @@ -372,6 +380,12 @@ class CallActivity : BaseActivity() { "当老人想听家人最新留言,或询问有没有新消息时使用。", fetchFamilyMessagesParameters, ), + OmniRealtimeClient.ToolDefinition("fetch_today_medication_plan", "查询今天待处理的用药提醒。", parameters(required = emptyList())), + OmniRealtimeClient.ToolDefinition("confirm_medication_taken", "老人明确确认已经服药后使用。", parameters("doseId" to "提醒中的服药编号。")), + OmniRealtimeClient.ToolDefinition("snooze_medication_reminder", "老人要求稍后再提醒服药时使用。", parameters("doseId" to "提醒中的服药编号。", "minutes" to "延后分钟数,通常为10。")), + OmniRealtimeClient.ToolDefinition("report_medication_problem", "老人说药找不到、吃错、不确定或服药后不舒服时使用。", parameters("doseId" to "服药编号,不确定时传unknown。", "problem" to "老人描述的完整问题。", "severity" to "ORANGE或RED。")), + OmniRealtimeClient.ToolDefinition("respond_to_check_in", "老人回应家属发起的报平安请求时使用。", parameters("checkInId" to "报平安请求编号。", "status" to "SAFE、NEEDS_CONTACT或DECLINED。", "responseText" to "老人想告诉家人的回应。")), + OmniRealtimeClient.ToolDefinition("request_family_help", "老人明确求助或确认存在需要家人立即关注的风险线索时使用。", parameters("category" to "风险类别。", "summary" to "经过老人确认的情况摘要。", "level" to "RED或ORANGE。")), ) } @@ -558,6 +572,11 @@ class CallActivity : BaseActivity() { "consult_psychology_knowledge_base" -> "我来帮您查一下更稳妥的照护资料。" "leave_message_for_family" -> "我来帮您把这句话记给家里人。" "fetch_family_messages" -> "我来帮您看看家里人的新留言。" + "fetch_today_medication_plan" -> "我来看看今天的用药安排。" + "confirm_medication_taken" -> "好的,我帮您把这次服药记下来。" + "snooze_medication_reminder" -> "好的,稍后我再提醒您。" + "report_medication_problem", "request_family_help" -> "我先确认一下,再通知家里人关注。" + "respond_to_check_in" -> "好的,我把您的平安回复告诉家里人。" else -> getString(R.string.ai_call_thinking) } } @@ -892,6 +911,8 @@ class CallActivity : BaseActivity() { "consult_psychology_knowledge_base" -> executePsychologyKnowledgeTool(callId, arguments) "leave_message_for_family" -> executeLeaveMessageTool(callId, arguments) "fetch_family_messages" -> executeFetchFamilyMessagesTool(callId, arguments) + "fetch_today_medication_plan", "confirm_medication_taken", "snooze_medication_reminder", + "report_medication_problem", "respond_to_check_in", "request_family_help" -> executeFamilyCareTool(toolName, callId, arguments) else -> { val outputText = "这个请求我还不能直接处理,您可以换一种说法再告诉我。" runCatching { @@ -1050,6 +1071,43 @@ class CallActivity : BaseActivity() { } } + private fun executeFamilyCareTool(toolName: String, callId: String, arguments: JSONObject): ToolExecutionResult { + return runCatching { + val outputText = when (toolName) { + "fetch_today_medication_plan" -> { + val agenda = familyBridgeService.fetchTodayAgenda() + if (agenda == null || agenda.doses.length() == 0) "今天暂时没有待确认的用药提醒。" else buildString { + append("今天有${agenda.doses.length()}次待确认用药。") + for (index in 0 until agenda.doses.length()) { val item = agenda.doses.optJSONObject(index) ?: continue; append(" 编号${item.optString("id")}:${item.optString("name")},${item.optString("dosage")}。") } + } + } + "confirm_medication_taken" -> { familyBridgeService.updateMedication(arguments.optString("doseId"), "TAKEN", "老人语音确认已服"); "已记录这次服药。请不要重复服用;如有不适,请立即告诉我或联系家人。" } + "snooze_medication_reminder" -> { val minutes = arguments.optInt("minutes", 10).coerceIn(1, 60); familyBridgeService.updateMedication(arguments.optString("doseId"), "SNOOZED", snoozeMinutes = minutes); "好的,已延后${minutes}分钟提醒。" } + "report_medication_problem" -> { + val problem = arguments.optString("problem").ifBlank { "老人报告用药问题" } + val doseId = arguments.optString("doseId") + if (doseId.isNotBlank() && doseId != "unknown") familyBridgeService.updateMedication(doseId, "NEEDS_FAMILY_CONFIRMATION", problem) + familyBridgeService.reportSafetyAlert("MEDICATION", "用药需要家人确认", problem, if (arguments.optString("severity") == "RED") "RED" else "ORANGE") + "已经通知家里人关注这次用药问题。我不能代替医生判断;如果情况紧急,请立即联系120或请身边的人帮助。" + } + "respond_to_check_in" -> { val status = arguments.optString("status").takeIf { it in listOf("SAFE", "NEEDS_CONTACT", "DECLINED") } ?: "NEEDS_CONTACT"; familyBridgeService.respondCheckIn(arguments.optString("checkInId"), status, arguments.optString("responseText")); "已经把您的回复告诉家里人。" } + "request_family_help" -> { + val summary = arguments.optString("summary").ifBlank { "老人请求家人联系" } + familyBridgeService.reportSafetyAlert(arguments.optString("category").ifBlank { "HELP_REQUEST" }, "长辈请求家人关注", summary, if (arguments.optString("level") == "RED") "RED" else "ORANGE", summary) + "已经通知家里人关注。这个提醒不是医学诊断;如果情况紧急,请立即联系120、110或请身边的人帮助。" + } + else -> error("不支持的照护工具") + } + familyBridgeService.recordToolCall(toolName, arguments, outputText, FamilyBridgeService.STATUS_SUCCESS, callId, currentSessionId) + ToolExecutionResult(outputText, FamilyBridgeService.STATUS_SUCCESS) + }.getOrElse { error -> + val outputText = "照护信息暂时没有同步成功,请稍后再试;如果情况紧急,请直接联系家人或急救服务。" + runCatching { familyBridgeService.recordToolCall(toolName, arguments, outputText, FamilyBridgeService.STATUS_FAILURE, callId, currentSessionId) } + applyMessage("家庭照护工具失败: ${error.message}") + ToolExecutionResult(outputText, FamilyBridgeService.STATUS_FAILURE) + } + } + private data class ToolExecutionResult( val outputText: String, val status: String,