Commit 2186c946592013e8d38c00c75d6d9baf540aa7cf
1 parent
a7af2318
fix(lifelog/chat+summary): 本地对话去 thinking/null + 纪要解耦 llama-swap + Room fail-loud
- ChatClient: 本地 Qwen 目标加 chat_template_kwargs.enable_thinking=false (Hermes/Kimi 不认会忽略,故只对 LOCAL 加);修 parseDelta —— delta.content 为 JSON null(思考阶段/role-only 首块)时跳过,不再因 org.json.optString 返回字面 "null" 污染回复开头。 - SummaryWorker: 纪要后端解耦到本地 llama-swap(LLM_BASE_URL=8080 / LLM_MODEL="2b"), 独立于 keys.properties——那个留给聊天的 Hermes 选项(8642 网关 → Kimi)。 - AppDatabase: 删掉 fallbackToDestructiveMigration 改 fail-loud(已有 MIGRATION_2_3; 以后改 schema 不写迁移直接抛异常,绝不再静默清掉真实录音)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showing
3 changed files
with
18 additions
and
3 deletions
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ChatClient.kt
| @@ -46,6 +46,11 @@ class ChatClient { | @@ -46,6 +46,11 @@ class ChatClient { | ||
| 46 | put("model", target.model) | 46 | put("model", target.model) |
| 47 | put("stream", true) | 47 | put("stream", true) |
| 48 | put("max_tokens", maxTokens) | 48 | put("max_tokens", maxTokens) |
| 49 | + // 本地 Qwen3.5 默认开思考,聊天里直接出答案不走 <think>; | ||
| 50 | + // Hermes/Kimi 不认这个参数(会被忽略),故只对本地目标加。 | ||
| 51 | + if (target.kind == ChatTargetKind.LOCAL) { | ||
| 52 | + put("chat_template_kwargs", JSONObject().put("enable_thinking", false)) | ||
| 53 | + } | ||
| 49 | put("messages", JSONArray().apply { | 54 | put("messages", JSONArray().apply { |
| 50 | messages.forEach { (role, content) -> | 55 | messages.forEach { (role, content) -> |
| 51 | put(JSONObject().apply { | 56 | put(JSONObject().apply { |
| @@ -101,7 +106,11 @@ class ChatClient { | @@ -101,7 +106,11 @@ class ChatClient { | ||
| 101 | return try { | 106 | return try { |
| 102 | val choices = JSONObject(data).optJSONArray("choices") ?: return null | 107 | val choices = JSONObject(data).optJSONArray("choices") ?: return null |
| 103 | if (choices.length() == 0) return null | 108 | if (choices.length() == 0) return null |
| 104 | - choices.getJSONObject(0).optJSONObject("delta")?.optString("content", "") | 109 | + val delta = choices.getJSONObject(0).optJSONObject("delta") ?: return null |
| 110 | + // content 为 JSON null(思考阶段 / role-only 首块)时跳过—— | ||
| 111 | + // 否则 org.json.optString 会返回字面 "null" 字符串污染回复。 | ||
| 112 | + if (delta.isNull("content")) return null | ||
| 113 | + delta.optString("content", "") | ||
| 105 | } catch (t: Throwable) { | 114 | } catch (t: Throwable) { |
| 106 | Log.w(TAG, "bad chunk: ${data.take(80)}") | 115 | Log.w(TAG, "bad chunk: ${data.take(80)}") |
| 107 | null | 116 | null |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/AppDatabase.kt
| @@ -51,7 +51,8 @@ abstract class AppDatabase : RoomDatabase() { | @@ -51,7 +51,8 @@ abstract class AppDatabase : RoomDatabase() { | ||
| 51 | "lifelog.db", | 51 | "lifelog.db", |
| 52 | ) | 52 | ) |
| 53 | .addMigrations(MIGRATION_2_3) | 53 | .addMigrations(MIGRATION_2_3) |
| 54 | - .fallbackToDestructiveMigration() // 1→x 等无迁移路径仍兜底(dev 早期库可丢) | 54 | + // 不再 fallbackToDestructiveMigration:以后改 schema 必须写 Migration, |
| 55 | + // 否则启动直接抛 IllegalStateException(fail-loud),绝不静默清掉真实录音。 | ||
| 55 | .build() | 56 | .build() |
| 56 | .also { instance = it } | 57 | .also { instance = it } |
| 57 | } | 58 | } |
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt
| @@ -47,6 +47,11 @@ class SummaryWorker( | @@ -47,6 +47,11 @@ class SummaryWorker( | ||
| 47 | const val PREFS = "lifelog_prefs" | 47 | const val PREFS = "lifelog_prefs" |
| 48 | const val KEY_GAP_MIN = "session_gap_min" | 48 | const val KEY_GAP_MIN = "session_gap_min" |
| 49 | 49 | ||
| 50 | + /** 纪要后端:本地 llama-swap 的 2B(与聊天共用 8080,按 model 名热切换)。 | ||
| 51 | + * 独立于 keys.properties(那个留给聊天的 Hermes 选项)。 */ | ||
| 52 | + const val LLM_BASE_URL = "http://127.0.0.1:8080" | ||
| 53 | + const val LLM_MODEL = "2b" | ||
| 54 | + | ||
| 50 | /** 纯函数:把按时间排好序的段切成若干会话。 */ | 55 | /** 纯函数:把按时间排好序的段切成若干会话。 */ |
| 51 | fun groupSessions(segments: List<Segment>, gapMs: Long): List<List<Segment>> { | 56 | fun groupSessions(segments: List<Segment>, gapMs: Long): List<List<Segment>> { |
| 52 | val out = mutableListOf<MutableList<Segment>>() | 57 | val out = mutableListOf<MutableList<Segment>>() |
| @@ -98,7 +103,7 @@ class SummaryWorker( | @@ -98,7 +103,7 @@ class SummaryWorker( | ||
| 98 | val userPrompt = buildPrompt(repo, session, speakerNames, start, end) | 103 | val userPrompt = buildPrompt(repo, session, speakerNames, start, end) |
| 99 | if (userPrompt == null) continue // 整段无有效转写 | 104 | if (userPrompt == null) continue // 整段无有效转写 |
| 100 | 105 | ||
| 101 | - val client = HermesClient() | 106 | + val client = HermesClient(baseUrl = LLM_BASE_URL, apiKey = "", model = LLM_MODEL) |
| 102 | val resp = try { | 107 | val resp = try { |
| 103 | client.chat(SYSTEM_PROMPT, userPrompt) | 108 | client.chat(SYSTEM_PROMPT, userPrompt) |
| 104 | } catch (t: Throwable) { | 109 | } catch (t: Throwable) { |