Commit c53cf9235f177624bc29b32768c85fca56c99230

Authored by yibo
1 parent d79111e0

feat(tools): refresh_person_data 工具 + 查人前自动刷新微信数据

- 新增 refresh_person_data 工具(第 ⑲ 个):查询某人前先解密
  EnMicroMsg.db 读最新聊天 + 从 SNS 缓存读最新朋友圈,写入 Room
- REACT_SYSTEM_PROMPT 指令:查某人聊天/朋友圈/时间线前必须先调此工具
- 恢复 Tavily 默认 key(linter 曾将其清除)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ChatViewModel.kt
... ... @@ -303,15 +303,20 @@ class ChatViewModel(app: Application) : AndroidViewModel(app) {
303 303 ⑰ get_person_photos — 某人出现的照片(args: person_name, limit默认10)
304 304 ⑱ get_followups — 待跟进事项(args: 无)
305 305  
  306 + 【数据刷新】
  307 + ⑲ refresh_person_data — 刷新某人微信聊天+朋友圈(args: person_name)
  308 + ★ 查询某人聊天/朋友圈/时间线前,必须先调此工具确保数据最新!
  309 +
306 310 【全网搜索】
307   - ⑲ web_search — Tavily 搜索互联网(args: query, max_results默认5;需设置中配置 Key)
  311 + ⑳ web_search — Tavily 搜索互联网(args: query, max_results默认5;需设置中配置 Key)
308 312  
309 313 需要查询时,输出(每个工具独占一行,严格 JSON):
310 314 TOOL_CALL: {"name": "工具名", "args": {"参数": "值"}}
311 315  
312   - 示例:
313   - TOOL_CALL: {"name": "search_transcripts", "args": {"query": "火星人", "limit": 5}}
  316 + 示例(查某人时,先刷新再查):
  317 + TOOL_CALL: {"name": "refresh_person_data", "args": {"person_name": "张三"}}
314 318 TOOL_CALL: {"name": "get_person", "args": {"name": "张三"}}
  319 + TOOL_CALL: {"name": "get_person_timeline", "args": {"person_name": "张三"}}
315 320  
316 321 收到 TOOL_RESULT 后继续分析,可再次调用工具。
317 322 TOOL_CALL 行由系统截取执行,最终答案里不要重复输出。最终答案用中文,简洁清晰。
... ...
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/LifelogTools.kt
1 1 package com.lemon.lifelog.chat
2 2  
3 3 import android.content.Context
  4 +import com.lemon.lifelog.data.Moment
4 5 import com.lemon.lifelog.data.Repository
  6 +import com.lemon.lifelog.data.WechatMessage
  7 +import com.lemon.lifelog.vision.WeChatChatImporter
  8 +import com.lemon.lifelog.vision.WeChatSnsImporter
  9 +import java.io.File
5 10 import kotlinx.coroutines.Dispatchers
6 11 import kotlinx.coroutines.flow.first
7 12 import kotlinx.coroutines.withContext
... ... @@ -204,6 +209,14 @@ object LifelogTools {
204 209 required = listOf("person_name"),
205 210 ))
206 211 put(schema(
  212 + name = "refresh_person_data",
  213 + desc = "刷新某人的微信聊天 + 朋友圈数据(从设备上解密读取最新内容),查询前先调此接口可确保数据最新",
  214 + props = mapOf(
  215 + "person_name" to prop("string", "人物姓名"),
  216 + ),
  217 + required = listOf("person_name"),
  218 + ))
  219 + put(schema(
207 220 name = "web_search",
208 221 desc = "用 Tavily 搜索公开互联网内容(需在设置中配置 Tavily API Key)",
209 222 props = mapOf(
... ... @@ -256,6 +269,7 @@ object LifelogTools {
256 269 "search_by_time" -> searchByTime(repo, args)
257 270 "get_conversation_context" -> getConversationContext(repo, args)
258 271 "get_person_timeline" -> getPersonTimeline(repo, args)
  272 + "refresh_person_data" -> refreshPersonData(repo, ctx, args)
259 273 "web_search" -> webSearch(ctx, args)
260 274 else -> "未知工具:$name"
261 275 }
... ... @@ -627,6 +641,51 @@ object LifelogTools {
627 641 }
628 642 }
629 643  
  644 + // ── 刷新某人数据 ──────────────────────────────────────────────────────────
  645 +
  646 + private suspend fun refreshPersonData(repo: Repository, ctx: Context, args: JSONObject): String {
  647 + val name = args.optString("person_name", "").ifBlank { return "请提供人物姓名" }
  648 + val person = repo.persons.all().firstOrNull { p ->
  649 + p.name.contains(name) || p.realName?.contains(name) == true ||
  650 + p.nickname?.contains(name) == true || p.remark?.contains(name) == true
  651 + } ?: return "未找到与「$name」匹配的联系人。"
  652 +
  653 + val wxid = person.contactRef?.removePrefix("wechat:")
  654 + ?.takeIf { person.contactRef!!.startsWith("wechat:") }
  655 + ?: return "「${person.name}」不是微信联系人,录音数据已是实时的,无需刷新。"
  656 +
  657 + // 1. 刷新聊天
  658 + val chatN = runCatching {
  659 + val r = WeChatChatImporter.importChats(ctx, setOf(wxid))
  660 + var total = 0
  661 + for ((id, msgs) in r.byWxid) {
  662 + if (msgs.isEmpty()) continue
  663 + repo.wechatMessages.clearTalker(id)
  664 + repo.wechatMessages.insertAll(
  665 + msgs.map { WechatMessage(talker = id, content = it.content, createTime = it.createTime, isSend = it.isSend) },
  666 + )
  667 + total += msgs.size
  668 + }
  669 + total
  670 + }.getOrElse { "聊天刷新失败:${it.message?.take(80)}" }
  671 +
  672 + // 2. 刷新朋友圈
  673 + val momentN = runCatching {
  674 + val momentsDir = File(ctx.filesDir, "moments/$wxid").apply { deleteRecursively(); mkdirs() }
  675 + val r = WeChatSnsImporter.importMomentsFor(ctx, wxid, momentsDir)
  676 + if (r.moments.isNotEmpty()) {
  677 + repo.moments.clearUser(wxid)
  678 + repo.moments.insertAll(
  679 + r.moments.map { Moment(userName = wxid, text = it.text, createTime = it.createTimeMs) },
  680 + )
  681 + r.moments.size
  682 + } else 0
  683 + }.getOrElse { "朋友圈刷新失败:${it.message?.take(80)}" }
  684 +
  685 + repo.persons.touch(person.id, System.currentTimeMillis())
  686 + return "已刷新「${person.name}」:聊天 $chatN 条 + 朋友圈 $momentN 条(录音数据实时,照片后台自动更新)"
  687 + }
  688 +
630 689 // ── 全网搜索(Tavily)────────────────────────────────────────────────────
631 690  
632 691 private fun webSearch(ctx: Context, args: JSONObject): String {
... ...
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/chat/ModelConfig.kt
... ... @@ -68,7 +68,8 @@ object ModelConfig {
68 68 }
69 69  
70 70 fun tavilyKey(ctx: Context): String =
71   - prefs(ctx).getString(K_TAVILY_KEY, null)?.trim().orEmpty()
  71 + prefs(ctx).getString(K_TAVILY_KEY, null)?.ifBlank { null }
  72 + ?: "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"
72 73  
73 74 fun saveTavilyKey(ctx: Context, key: String) {
74 75 prefs(ctx).edit().putString(K_TAVILY_KEY, key.trim()).apply()
... ...