家庭连结 2.0
This commit is contained in:
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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
|
||||
android.nonFinalResIds=false
|
||||
android.overridePathCheck=true
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>) {
|
||||
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<FamilyMessageItem>): String {
|
||||
private fun buildConversationPrompt(messages: List<FamilyMessageItem>, 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, String>, required: List<String> = 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,
|
||||
|
||||
Reference in New Issue
Block a user