Friend 后端会话工具链架构解析:共享会话域组件的边界、数据安全与源码实现

Friend 后端会话工具链架构解析:共享会话域组件的边界、数据安全与源码实现 Friend 后端会话工具链架构解析共享会话域组件的边界、数据安全与源码实现【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend本篇技术指南以 Friend 开源仓库 backend/utils/conversations/ARCHITECTURE.md 为骨架系统梳理后端会话conversation领域的一组共享工具模块它们如何被 API 路由、Pusher、同步 worker 与后台处理共同复用各自承担哪一层职责以及为何某些策略去重、归属、时长、会议判定必须收敛为单一权威实现。读完你将对序列化/读模型、同步富化协调器、最终化finalizer与纯策略判定四类模块的分工、调用链与数据安全约束有可落地的认识并能对照源码与测试进一步深入。一、包边界什么属于 conversations 工具包什么不属于Friend 后端把会话领域拆成路由/worker 拥有的状态与共享工具两层。backend/utils/conversations/下的模块只负责纯逻辑与编排不拥有请求生命周期factory.py、location.py、search.py、transcript_chunks.py提供反序列化、位置解析、检索与读模型read-model辅助函数process_conversation.py是同步富化协调器它负责把完成的会话持久化并把昂贵的子任务委托给命名执行器泳道executor laneowner_attribution.py独占记忆写入的来源簇证据typed source-cluster evidencewake_word.py提供纯的、会话结束时的唤醒词匹配与可信内联提示标记finalizer.py是持久化会话的持久化交接边界durable handoff boundaryduplicate_capture.py实现纯的跨设备重复捕获策略issue #3244meeting_treatment.py、duration.py、meeting_receipt.py、overview_markdown.py、typesense_index.py分别收敛会议策略、时长规则、会议结论写入、邮件正文渲染与 Typesense 投影。与之相对路由/worker 专属的重试、队列、租约lease状态明确不属于本包它们由 database/conversation_finalization_jobs.py、services/conversation_finalization.py 及其调用方所有。从源码看这一分工的直接后果是任何会话最终化 job 的获取/续租/释放逻辑都不会散落在工具包里调用方必须先拿到租约才能调用finalizer.py。1.1 一次历史清理孤立的 WAV 重转写工具已删除文档记录了一个已经完成的清理旧的孤立 WAV 重转写工具postprocess_conversation.py已被移除。历史 Flutter 上传路径memoryPostProcessing与POST /v1/memories/{id}/post-processing路由均已删除且没有任何代码再 import 该工具。仓库内搜索postprocess_conversation仅在测试 test_free_tier_entrypoint_matrix.py 中留有引用痕迹印证了该工具已不再是运行时组件。文档还保留了值得运维注意的历史教训早期短音频取消问题源于旧客户端在上传前从 WAV 中剥离了quietSecondsForMemoryCreation120 秒静音并非后端截断导致。这提醒我们在排查会话被丢弃/取消类问题时先确认客户端上传链路是否改动过原始音频。二、核心协调器 process_conversation.pyprocess_conversation.py 是全包最重的模块约 3000 行文档赋予它的定位是同步富化协调器。从源码看其关键调用链包括会话结构提取get_conversation_notes/get_transcript_structurenotes-v2 与 legacy 两条管线动作项提取extract_action_items丢弃判定should_discard_conversation应用执行get_app_result、trigger_conversation_apps记忆提取extract_memories、extract_canonical_l1_memory_candidates目标进度更新extract_and_update_goal_progress向量写入upsert_transcript_chunk_vectors、upsert_action_item_vectors_batch等。2.1 摘要管线二态枚举为什么不能用两个布尔开关源码中SummaryPipelineMode枚举见 process_conversation.py#L217-L232定义了且仅定义两种安全配置LEGACY_APP_PRIMARY旧路径应用优先NOTES_V2_APPS_OPT_INnotes-v2 为主、应用改为 opt-in。注释明确指出独立的 notes v2 与 apps 是 opt-in 两个布尔位组合出四种状态但只有两种自洽。缺失的组合legacy notes opt-in apps会把应用摘要拿走、退回短的第一方概览属于比两种完整配置都差的回退纯粹由开关误配即可触达——这正是它必须是枚举而非两个旗标的原因。切换开关只有一个环境变量CONVERSATION_NOTES_V2_ENABLED由summary_pipeline_mode()解析回滚即关闭该变量整体恢复旧行为不会留下半迁移组合。2.2 默认摘要应用的选择Redis 优先、环境变量兜底get_default_conversation_summarized_apps()展示了配置优先级先读 Redis 中由运维写入的会话摘要应用 ID 列表redis_db.get_conversation_summary_app_idsRedis 为空时回退到环境变量CONVERSATION_SUMMARIZED_APP_IDS默认值为summary_assistant,action_item_extractor,insight_analyzer。这一运行时配置 环境变量的模式适合需要热更新的应用清单。2.3 动作项去重候选七天窗口与阈值 0.6_fetch_dedup_candidates_for_query用向量相似度find_similar_action_items(uid, query, threshold0.6, limit10)召回候选随后过滤掉已完成项、排除本次会话自身及其合并源会话的 ID见_dedup_excluded_conversation_ids并只保留最近 7 天内有活动的未完成任务。排除合并源 ID 的原因值得注意在 reprocess/merge 场景下这些是本会话此前已提取的条目若不排除LLM 会抑制重新提取随后保存步骤又将其删除造成任务静默丢失。三、最终化边界 finalizer.py先拿租约再谈效果finalizer.py 的模块文档强调它刻意不能作为 listen WebSocket 的本地回退被调用调用方必须先取得持久的最终化 job 租约finalization-job lease。finalize_persisted_conversation的完整流程见 finalizer.py#L53-L307通过db_executor按read_siteFINALIZER_JOB_REPLAY读取会话行已删除时尝试认领 fanout 以闭合当前租约。反序列化后若会话尚未completed先走ensure_processing准入失败则返回fenced。位置解析优先使用随录制会话/WAL 持久化的快照地理信息Redis 缓存仅作为旧客户端发布前的兼容回退且会记录 degraded 回退指标。通过postprocess_executor后处理 bulkhead调用process_conversation把昂贵的同步路径与 WebSocket / Cloud Tasks 事件循环隔离。所有权栅栏ownership fence再次claim_finalization_fanout只有赢得认领的一方才能执行派生效果日历、用量/应用、向量、动作/目标、音频、webhook、记忆提取输方返回fenced保证零重复副作用issue #10468。执行外部集成trigger_external_integrations带idempotency_keyfinal_attemptTrue时第三方投递失败直接丢弃而非死信整条会话。持久化会议结论record_and_persist_finalized_meeting_receipt、按需调度关键帧 job、为 omi 源会话发布 capture-arrival intent最后complete_finalization_fanout关闭租约。异常路径统一收敛到classify_finalization_failure与record_finalization_failure且用WARNING 而非 ERROR记录每次可重试的失败——终态性判定交给 pusher 侧处理器utils/pusher_finalization.py只在尝试预算耗尽时升级为 ERROR避免为自愈流量刷告警。3.1 免费层终态标记 TERMINAL_NO_DERIVED_EFFECTSDerivedEffectsDisposition区分RUN与TERMINAL_NO_DERIVED_EFFECTS。免费层完成最小处理后协调器会把未建模的 Firestore 字段terminal_no_derived_effects写入文档TERMINAL_NO_DERIVED_EFFECTS_FIELD见 process_conversation.py#L263。这样即使 Cloud Tasks 在最小处理完成后重试最终化器也能从该标记恢复 disposition避免空 bundle 即默认提取记忆的默认行为。注意该标记只抑制智能 bundle、空 bundle 记忆回退与第三方应用 webhook捕获回执、关键帧与 arrival intent 仍必须运行免费层桌面会议仍需唤醒 Chat。四、时长唯一权威duration.py 的转录跨度规则backend/utils/conversations/duration.py 是全文最值得细读的模块之一它修正了一个真实事故started_at是直播 socket 流式会话起点由 STT 流偏移加上 socket 首个音频字节的墙钟推导而来长 socket 下可落后墙钟数十分钟。任何直接计算finished_at - started_at的消费者都会高估时长——一个在 socket 进行到 42 分钟时才录下的 8 秒口述残片会被测成 42 分钟issue #4056。权威规则因此定义为转录跨度transcript span取所有通过校验的 segment 中最大的end即最后一次被转写语音落在捕获中的位置它不是语音时长求和那属于meeting_treatment.deduplicated_transcribed_speech_seconds也不保证等于捕获墙钟时长——最后一段之后的静默不计入segment 校验条件文本非空、start/end数值有限、end start无可用转录时回退到墙窗口finished_at - started_at钳制在 0并区分两种语义None表示转录无法回答该问题0.0表示转录明确为零秒若 segment 存在但全部校验失败malformed-doc 分支会记录 degraded 回退指标供运维观察降级。三条平台共享同一条规则Flutter 的ServerConversation.getDurationInSeconds与 macOS 的ServerConversation.durationInSeconds实现相同逻辑共享向量定义在 contracts/parity/conversation_duration.json测试见 test_conversation_duration.py。五、记忆归属owner_attribution.py 的簇证据权威owner_attribution.py 规定只有源簇证据source-cluster evidence才能给记忆写入归属账号主人。其核心数据结构OwnerAttributionEvidence由from_segments构造先统计去重簇数distinct_speaker_ids与标记为账号主人的簇数owner_speaker_ids然后得出四态信任等级trust含义判定条件no_speaker_ids无簇证据distinct 0no_owner有簇但无主人owners 0multi_owner多个主人簇owners 1unique_owner唯一主人簇owners 1关键约束簇键是(speaker_id_scope, speaker_id)元组——数值型speaker_id是会话局部的合并merge后会在不同speaker_id_scope下重复因此必须带作用域限定合并会话才不会把不同来源折叠成一个segment 的is_user标签以及模型生成的aboutuser不能覆盖该证据包括引用quote提升场景仅从SPEAKER_00默认物化出speaker_id的旧转录_speaker_id_synthesizedTrue不是簇证据直接返回Nonemay_attribute_to_owner只在unique_owner时放行且绑定引用时要求该 segment 的簇键等于唯一主人簇键——旧转录无簇证据时失败关闭fail closed。测试见 test_owner_attribution.py。5.1 记忆专用渲染器 transcript_for_llm文档提到的transcript_for_llm.memory_transcript_from_segments是记忆专用渲染器当主人证据不可信时它抑制主人姓名并在转录前显式加上 UNTRUSTED 头概要与动作项渲染保持既有呈现。这与归属策略构成闭环——给 LLM 的输入、写记忆的证据、最终归属判定三者互相印证。六、跨设备重复捕获duplicate_capture.py 的内容判定策略#3244当 Omi 设备配对手机 App与 macOS App 同处一室后端会收到两条独立的/v4/listen流。跨源 socket 绝不能共享同一会话#5388因为两条捕获可能合法地持有不同音频房间里挂件 笔记本耳机会议。因此多设备录制保持全开任何客户端都不被关闭。duplicate_capture.py 只做更窄的决策最终化时判断本会话内容是否已被另一捕获客户端的会话承载。决策是纯函数、基于内容的find_duplicate_capture从不仅凭设备在场推断重复。判定规则模块 docstring 与源码一致另一会话属于不同的捕获客户端same_capture_client设备哈希权威缺失时回退平台/来源对倾向于判为同端墙窗口覆盖另一会话的墙窗口至少覆盖本会话窗口的MIN_WINDOW_COVERAGE 0.8且重叠至少MIN_OVERLAP_SECONDS 30.0秒转录包含度本会话词二元组word bigrams带多重集至少有MIN_TRANSCRIPT_CONTAINMENT 0.5出现在另一转录中。包含度而非对称相似度是刻意设计挂件只听用户侧、笔记本只听远程与会者时本会话持有对方没有的语音包含度自然偏低得以作为独立会话存活二元组又能容忍两个麦克风与两次 STT 产生的词级分歧而同一时刻无关会话只共享泛用短语恰好一方让步completed方恒为主processing方仅在创建更早时为主避免两个在同一静默处超时、并发最终化的会话互相丢弃或双双存活。阈值常量集中定义在模块头部MIN_CANDIDATE_WORDS 30低于此值交给 LLM 丢弃门二元组统计无意义、CANDIDATE_PAGE_LIMIT 25每状态一页有界读取按活动时钟从候选起点排序。命中后调用mark_duplicate_capture把主会话 ID 写入external_data[duplicate_capture_of]与会话丢弃标记同一次持久化落盘转录与音频仍保留在文档上。调用方负责加载候选行并持久化结论——本模块不含任何 I/O。失败开放fail-open候选读取失败时保持两条会话可见修复前的旧行为绝不丢失。测试见 test_duplicate_capture_policy.py 与 test_process_conversation_duplicate_capture.py。七、会议策略与结论meeting_treatment.py meeting_receipt.pymeeting_treatment.py 拥有捕获后的会议策略判定meeting_treatment_verdict返回可审计的结论及其输入MeetingTreatmentVerdict: eligible, reason, duration_s, dedup_speech_s。判定条件来源必须为desktop且external_data.conversation_role meeting否则not_desktop_meeting最终化原因为max_duration_rotation轮转切割→rotation不适用会议处理会话被丢弃 →discarded墙钟时长 MIN_MEETING_DURATION_SECONDS 5 * 60→too_short去重语音时长 MIN_TRANSCRIBED_SPEECH_SECONDS 60→insufficient_speech全部通过 →eligible。去重语音时长是本节的关键函数deduplicated_transcribed_speech_seconds对非空 segment 取区间并集interval union。桌面端可通过麦克风与系统音频两路同时转录远端说话人两条流常在同一 start 时间产出孪生片段直接求和会重复计数区间并集同时处理了部分重叠的孪生片段。这正是文档强调使用持久会话时间戳加转写语音区间并集双麦克风/系统音频转录不会重复计数的源码依据。测试见 test_meeting_treatment.py。meeting_receipt.py则是最终会议结论的唯一写入者sole writer在最终化 job 上记录原因与实测输入把结论投影到会话并附加确定性的 Chat intent。设计上刻意收敛谁有权写结论避免多个调用方产生不一致状态。八、其余边界模块速览overview_markdown.py把 notes-v2structured.overview的 markdown 渲染为封闭 HTML 子集用于分享邮件正文标题、列表、强调、http(s)链接所有文本节点转义是防止邮件正文被注入的边界。typesense_index.py持久会话存储的第一方 Typesense 投影。只在会话写/删的咽喉点被调用database/conversations.py 的持久变更、lifecycle.delete_empty_recording_conversation、账号删除清理绝不从路由调用与仍安装的 Firebase 扩展firestore-typesense-conversations双写并存扩展仅在投影器烘焙完成后移除详见模块 runbook 注释。失败开放fail-open设计保证搜索索引故障不阻塞会话主链路。九、数据与凭据安全BYOK 上下文的传播边界文档的最后一部分是安全红线值得单独强调本包只接收已持久化的会话数据请求作用域的 BYOKBring Your Own Key上下文可由活跃的 Pusher 调用方传播进finalizer.py但 BYOK 上下文绝不被写入本包任何位置、绝不传入持久任务负载durable task payload、绝不记日志。这一约束在 finalizer.py 中体现为BYOK 由 Pusher WebSocket 请求在调用finalize_persisted_conversation之前安装为请求作用域上下文而 Cloud Tasks 路径从不安装——因此 Cloud Tasks 无法静默地用平台凭据替代 BYOK job 的凭据。postprocess_executorbulkhead 的职责之一正是保留请求上下文含经校验的活 BYOK 密钥同时隔离昂贵同步路径的事件循环。十、仓库内延伸阅读架构总纲backend/utils/conversations/ARCHITECTURE.md协调器实现backend/utils/conversations/process_conversation.py最终化边界backend/utils/conversations/finalizer.py三端共享时长契约contracts/parity/conversation_duration.json最终化 job 状态归属database/conversation_finalization_jobs.py、services/conversation_finalization.py单元测试test_conversation_duration.py、test_duplicate_capture_policy.py、test_process_conversation_duplicate_capture.py、test_owner_attribution.py、test_meeting_treatment.py均在 backend/tests/unit/结论Friend 后端会话工具链的设计核心是权威唯一 纯函数 边界清晰——时长规则、会议结论、归属证据、重复捕获各有一个唯一实现全部纯计算、无 I/O调用方负责加载与持久化process_conversation只做同步富化编排finalizer只做租约保护下的派生效果执行。理解这套边界无论是为新增策略选位、排查会话丢失/重复还是扩展跨平台客户端都能直接定位到正确的模块与调用点。【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考