Commit c53cf9235f177624bc29b32768c85fca56c99230
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>
Showing
3 changed files
with
69 additions
and
4 deletions
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() | ... | ... |