From 2186c946592013e8d38c00c75d6d9baf540aa7cf Mon Sep 17 00:00:00 2001 From: yibo Date: Thu, 4 Jun 2026 18:01:26 +0800 Subject: [PATCH] fix(lifelog/chat+summary): 本地对话去 thinking/null + 纪要解耦 llama-swap + Room fail-loud --- lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ChatClient.kt | 11 ++++++++++- lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/AppDatabase.kt | 3 ++- lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ChatClient.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ChatClient.kt index 8d99b72..ec8840b 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ChatClient.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ChatClient.kt @@ -46,6 +46,11 @@ class ChatClient { put("model", target.model) put("stream", true) put("max_tokens", maxTokens) + // 本地 Qwen3.5 默认开思考,聊天里直接出答案不走 ; + // Hermes/Kimi 不认这个参数(会被忽略),故只对本地目标加。 + if (target.kind == ChatTargetKind.LOCAL) { + put("chat_template_kwargs", JSONObject().put("enable_thinking", false)) + } put("messages", JSONArray().apply { messages.forEach { (role, content) -> put(JSONObject().apply { @@ -101,7 +106,11 @@ class ChatClient { return try { val choices = JSONObject(data).optJSONArray("choices") ?: return null if (choices.length() == 0) return null - choices.getJSONObject(0).optJSONObject("delta")?.optString("content", "") + val delta = choices.getJSONObject(0).optJSONObject("delta") ?: return null + // content 为 JSON null(思考阶段 / role-only 首块)时跳过—— + // 否则 org.json.optString 会返回字面 "null" 字符串污染回复。 + if (delta.isNull("content")) return null + delta.optString("content", "") } catch (t: Throwable) { Log.w(TAG, "bad chunk: ${data.take(80)}") null diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/AppDatabase.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/AppDatabase.kt index 6576cc8..ab58a2e 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/AppDatabase.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/AppDatabase.kt @@ -51,7 +51,8 @@ abstract class AppDatabase : RoomDatabase() { "lifelog.db", ) .addMigrations(MIGRATION_2_3) - .fallbackToDestructiveMigration() // 1→x 等无迁移路径仍兜底(dev 早期库可丢) + // 不再 fallbackToDestructiveMigration:以后改 schema 必须写 Migration, + // 否则启动直接抛 IllegalStateException(fail-loud),绝不静默清掉真实录音。 .build() .also { instance = it } } diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt index c632c49..d26f64e 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt @@ -47,6 +47,11 @@ class SummaryWorker( const val PREFS = "lifelog_prefs" const val KEY_GAP_MIN = "session_gap_min" + /** 纪要后端:本地 llama-swap 的 2B(与聊天共用 8080,按 model 名热切换)。 + * 独立于 keys.properties(那个留给聊天的 Hermes 选项)。 */ + const val LLM_BASE_URL = "http://127.0.0.1:8080" + const val LLM_MODEL = "2b" + /** 纯函数:把按时间排好序的段切成若干会话。 */ fun groupSessions(segments: List, gapMs: Long): List> { val out = mutableListOf>() @@ -98,7 +103,7 @@ class SummaryWorker( val userPrompt = buildPrompt(repo, session, speakerNames, start, end) if (userPrompt == null) continue // 整段无有效转写 - val client = HermesClient() + val client = HermesClient(baseUrl = LLM_BASE_URL, apiKey = "", model = LLM_MODEL) val resp = try { client.chat(SYSTEM_PROMPT, userPrompt) } catch (t: Throwable) { -- libgit2 0.21.4