Friend 后端会话工具链架构解析:共享会话域组件的边界、数据安全与源码实现
2026/9/15 14:40:55 网站建设 项目流程

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.pylocation.pysearch.pytranscript_chunks.py提供反序列化、位置解析、检索与读模型(read-model)辅助函数;
  • process_conversation.py同步富化协调器:它负责把完成的会话持久化,并把昂贵的子任务委托给命名执行器泳道(executor lane);
  • owner_attribution.py独占"记忆写入的来源簇证据"(typed source-cluster evidence);
  • wake_word.py提供纯的、会话结束时的唤醒词匹配与可信内联提示标记;
  • finalizer.py是持久化会话的持久化交接边界(durable handoff boundary);
  • duplicate_capture.py实现纯的跨设备重复捕获策略(issue #3244);
  • meeting_treatment.pyduration.pymeeting_receipt.pyoverview_markdown.pytypesense_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 上传路径memoryPostProcessingPOST /v1/memories/{id}/post-processing路由均已删除,且没有任何代码再 import 该工具。仓库内搜索postprocess_conversation仅在测试 test_free_tier_entrypoint_matrix.py 中留有引用痕迹,印证了该工具已不再是运行时组件。

文档还保留了值得运维注意的历史教训:早期"短音频取消"问题源于旧客户端在上传前从 WAV 中剥离了quietSecondsForMemoryCreation(120 秒)静音,并非后端截断导致。这提醒我们在排查"会话被丢弃/取消"类问题时,先确认客户端上传链路是否改动过原始音频。

二、核心协调器 process_conversation.py

process_conversation.py 是全包最重的模块(约 3000 行),文档赋予它的定位是"同步富化协调器"。从源码看,其关键调用链包括:

  • 会话结构提取:get_conversation_notes/get_transcript_structure(notes-v2 与 legacy 两条管线);
  • 动作项提取:extract_action_items
  • 丢弃判定:should_discard_conversation
  • 应用执行:get_app_resulttrigger_conversation_apps
  • 记忆提取:extract_memoriesextract_canonical_l1_memory_candidates
  • 目标进度更新:extract_and_update_goal_progress
  • 向量写入:upsert_transcript_chunk_vectorsupsert_action_item_vectors_batch等。

2.1 摘要管线二态枚举:为什么不能用两个布尔开关

源码中SummaryPipelineMode枚举(见 process_conversation.py#L217-L232)定义了且仅定义两种安全配置:

  • LEGACY_APP_PRIMARY(旧路径,应用优先);
  • NOTES_V2_APPS_OPT_IN(notes-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_ids),Redis 为空时回退到环境变量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, threshold=0.6, limit=10)召回候选,随后过滤掉已完成项、排除本次会话自身及其合并源会话的 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):

  1. 通过db_executorread_site=FINALIZER_JOB_REPLAY读取会话;行已删除时尝试认领 fanout 以闭合当前租约。
  2. 反序列化后,若会话尚未completed,先走ensure_processing准入,失败则返回fenced
  3. 位置解析:优先使用随录制会话/WAL 持久化的快照地理信息;Redis 缓存仅作为旧客户端发布前的兼容回退(且会记录 degraded 回退指标)。
  4. 通过postprocess_executor(后处理 bulkhead)调用process_conversation,把昂贵的同步路径与 WebSocket / Cloud Tasks 事件循环隔离。
  5. 所有权栅栏(ownership fence):再次claim_finalization_fanout,只有赢得认领的一方才能执行派生效果(日历、用量/应用、向量、动作/目标、音频、webhook、记忆提取),输方返回fenced,保证零重复副作用(issue #10468)。
  6. 执行外部集成trigger_external_integrations(带idempotency_keyfinal_attempt=True时第三方投递失败直接丢弃而非死信整条会话)。
  7. 持久化会议结论record_and_persist_finalized_meeting_receipt、按需调度关键帧 job、为 omi 源会话发布 capture-arrival intent,最后complete_finalization_fanout关闭租约。

异常路径统一收敛到classify_finalization_failurerecord_finalization_failure,且用WARNING 而非 ERROR记录每次可重试的失败——终态性判定交给 pusher 侧处理器(utils/pusher_finalization.py),只在尝试预算耗尽时升级为 ERROR,避免为自愈流量刷告警。

3.1 免费层终态标记 TERMINAL_NO_DERIVED_EFFECTS

DerivedEffectsDisposition区分RUNTERMINAL_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)才能给记忆写入归属账号主人。其核心数据结构OwnerAttributionEvidencefrom_segments构造,先统计去重簇数distinct_speaker_ids与标记为账号主人的簇数owner_speaker_ids,然后得出四态信任等级:

trust含义判定条件
no_speaker_ids无簇证据distinct == 0
no_owner有簇但无主人owners == 0
multi_owner多个主人簇owners > 1
unique_owner唯一主人簇owners == 1

关键约束:

  • 簇键是(speaker_id_scope, speaker_id)元组——数值型speaker_id是会话局部的,合并(merge)后会在不同speaker_id_scope下重复,因此必须带作用域限定,合并会话才不会把不同来源折叠成一个;
  • segment 的is_user标签以及模型生成的about=user不能覆盖该证据,包括引用(quote)提升场景;
  • 仅从SPEAKER_00默认物化出speaker_id的旧转录(_speaker_id_synthesized=True不是簇证据,直接返回None
  • may_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 与源码一致):

  1. 另一会话属于不同的捕获客户端same_capture_client:设备哈希权威,缺失时回退平台/来源对,倾向于判为"同端");
  2. 墙窗口覆盖:另一会话的墙窗口至少覆盖本会话窗口的MIN_WINDOW_COVERAGE = 0.8,且重叠至少MIN_OVERLAP_SECONDS = 30.0秒;
  3. 转录包含度:本会话词二元组(word bigrams,带多重集)至少有MIN_TRANSCRIPT_CONTAINMENT = 0.5出现在另一转录中。包含度而非对称相似度是刻意设计:挂件只听用户侧、笔记本只听远程与会者时,本会话持有对方没有的语音,包含度自然偏低,得以作为独立会话存活;二元组又能容忍两个麦克风与两次 STT 产生的词级分歧,而同一时刻无关会话只共享泛用短语;
  4. 恰好一方让步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.py

meeting_treatment.py 拥有捕获后的会议策略判定,meeting_treatment_verdict返回可审计的结论及其输入(MeetingTreatmentVerdict: eligible, reason, duration_s, dedup_speech_s)。判定条件:

  • 来源必须为desktopexternal_data.conversation_role == 'meeting',否则not_desktop_meeting
  • 最终化原因为max_duration_rotation(轮转切割)→rotation,不适用会议处理;
  • 会话被丢弃 →discarded
  • 墙钟时长 <MIN_MEETING_DURATION_SECONDS = 5 * 60too_short
  • 去重语音时长 <MIN_TRANSCRIBED_SPEECH_SECONDS = 60insufficient_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 上下文的传播边界

文档的最后一部分是安全红线,值得单独强调:

  • 本包只接收已持久化的会话数据
  • 请求作用域的 BYOK(Bring 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.pytest_duplicate_capture_policy.pytest_process_conversation_duplicate_capture.pytest_owner_attribution.pytest_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询