Friend 开源仓库插件体系深度解析:共享 SDK 模型、独立 FastAPI 插件服务与遗留单体架构
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
本篇技术指南围绕plugins/目录的权威说明文档(plugins/README.md)展开,系统梳理 Friend 开源项目中插件体系的三层结构:模型共享的omi-plugin-sdk、可独立部署的 28 个omi-*-app/FastAPI 服务,以及仅因既有部署目标而保留的遗留单体main.py + Dockerfile。读完本文,你将掌握 Omi webhook 载荷模型的字段语义、SDK 与各消费方的依赖安装方式、独立服务的部署描述符(Dockerfile / Procfile / railway.toml)形态,以及遗留单体的路由构成与维护红线,可直接用于插件开发、迁移或审计。
一、目录全景:plugins/里装的是三件不同的事
plugins/不是一个单一的应用,而是三类性质完全不同的代码的集合,这一点在 plugins/README.md 的开篇就明确声明:
omi-plugin-sdk/——共享 Python 包,只负责 Omi webhook 载荷模型(Conversation、TranscriptSegment、ActionItem等),不包含任何运行时业务逻辑;omi-*-app/——28 个彼此独立部署的插件服务(notion、github、slack、dropbox、whoop……),每个目录自包含main.py、依赖文件和部署描述符;- 遗留单体
main.py + Dockerfile——旧的"一站式"插件 API,保留的唯一原因是其 Cloud Run 部署目标仍然存在。
理解这种"共享模型 + 独立服务 + 遗留单体"的混合形态,是进入 Friend 插件生态的第一步:模型层做契约,独立服务做业务,单体只做历史兼容。
二、omi-plugin-sdk:仅含模型的共享契约层
2.1 设计定位与历史演进
omi-plugin-sdk是一个很小的 Python 包,其职责被刻意收窄为只拥有 Omi webhook 载荷模型(omi_plugin_sdk.models下的Conversation、TranscriptSegment、ActionItem等)。根据 plugins/README.md 的记载,原本包含的 auth / webhook / FastAPI 辅助模块已于 2026 年 7 月被移除,原因是"零调用方的投机性代码";审计文档 plugins/PLUGIN_REFACTOR_AUDIT.md 补充确认:删除发生在提交f6ac87773f,如今 SDK 的源码仅剩__init__.py与models.py两个文件。
从 plugins/omi-plugin-sdk/pyproject.toml 可以看出包的关键元数据:
[project] name = "omi-plugin-sdk" version = "0.1.0" description = "Shared Omi plugin webhook models and small integration primitives." requires-python = ">=3.10" dependencies = ["pydantic>=2.0.0"] [tool.setuptools.packages.find] where = ["src"]依赖仅pydantic>=2.0.0,全部模型基于 Pydantic v2 构建,包源码采用src/布局,导入路径为omi_plugin_sdk.models。
2.2 SDK 安装方式:按消费方不同而不同
SDK 的安装方式并不统一,取决于谁在使用它(这是 plugins/README.md 明确强调的要点):
- 遗留单体(plugins/main.py + plugins/Dockerfile):通过 plugins/requirements.txt 安装 SDK,该文件第一行就是路径依赖
./omi-plugin-sdk; - 独立部署的
omi-*-app/服务:每个服务有各自的requirements.txt,在自己目录内通过相对路径../omi-plugin-sdk安装 SDK,不经过plugins/requirements.txt。
独立服务之所以用相对路径,是因为这些服务通常以仓库 checkout 形式构建(见后文 notion 示例),plugins/omi-plugin-sdk与 app 目录互为同级。审计文档 plugins/PLUGIN_REFACTOR_AUDIT.md 特别警示了这一模式的依赖风险:如果某个服务以"隔离根目录"方式构建(不包含同级 SDK 目录),依赖安装就会失败——这也是当年不做破坏性迁移的原因之一。
SDK 自己的 plugins/omi-plugin-sdk/README.md 给出了本地开发安装与规范导入方式:
pip install -e plugins/omi-plugin-sdkfrom omi_plugin_sdk.models import ( ActionItem, Conversation, ConversationPhoto, EndpointResponse, Event, Structured, TranscriptSegment, )2.3 核心载荷模型逐字段拆解
模型实现的完整源码位于 plugins/omi-plugin-sdk/src/omi_plugin_sdk/models.py,下面按 webhook 数据流自上而下拆解关键模型。
Conversation—— 会话根对象
class Conversation(BaseModel): id: Optional[str] = None created_at: datetime started_at: Optional[datetime] = None finished_at: Optional[datetime] = None transcript_segments: List[TranscriptSegment] = Field(default_factory=list) photos: Optional[List[ConversationPhoto]] = Field(default_factory=list) structured: Structured apps_results: List[AppResult] = Field(default_factory=list) plugins_results: List[PluginResult] = Field(default_factory=list) discarded: bool = FalseConversation是 webhook 载荷的顶层容器,聚合了转写片段、照片、结构化笔记与应用/插件结果。值得注意的实现细节:
- 它带有一个
@model_validator(mode="after")的sync_plugin_results:当apps_results非空而plugins_results为空时,自动把AppResult转换为PluginResult(plugin_id=app.app_id, content=app.content),实现新旧结果字段的兼容; - 便捷方法
get_transcript(include_timestamps, user_name)直接委托TranscriptSegment.segments_as_string生成可读文本; get_duration()取所有片段start的最小值与end的最大值之差,格式化为HH:MM:SS字符串。
TranscriptSegment—— 转写片段
class TranscriptSegment(BaseModel): id: Optional[str] = None text: str speaker: Optional[str] = "SPEAKER_00" speaker_id: Optional[int] = None is_user: bool person_id: Optional[str] = None start: float end: floatTranscriptSegment是插件处理转写的基本单位。源码为其实现了四个静态/实例工具,直接服务于插件的转写处理:
speaker_id自动推导:重写的__init__会从speaker字段(形如SPEAKER_03)解析出数字序号写入speaker_id,解析失败时兜底为 0;get_timestamp_string():把start/end秒数转成00:01:23 - 00:01:45样式的可读时间戳;segments_as_string(...):把片段列表渲染成User: .../Speaker N: ...的对话文本,可用user_name参数替换用户显示名,include_timestamps仅在片段时间无重叠时启用(见can_display_seconds);combine_segments(...):增量合并片段,支持delta_seconds时间偏移;相邻片段若说话人相同(或同为用户),文本直接拼接并延伸end时间;合并后还会做一次文本清洗(去掉多余空格、修正,/./?等标点间距)。
ActionItem—— 行动项
ActionItem是插件提取"待办"的核心模型,字段设计体现了相当成熟的意图理解能力:
- 基础字段:
description(行动项内容)、completed、created_at/updated_at/due_at/completed_at、conversation_id; - 捕获语义:
capture_kind枚举explicit_command(显式指令)/clear_commitment(明确承诺)/direct_request(直接请求)/inferred_next_step(推断的下一步);capture_confidence与ownership_confidence均为0~1的置信度; - 归属语义:
capture_owner枚举user/other/unknown,owner_name记录已知归属人姓名; - 上下文与确定性:
context用一句话说明该行动项为何重要;due_certainty区分confirmed/tentative;concrete_deliverable仅在承诺涉及具体交付物/结果时为真; - 任务联动:
candidate_action(create/update/complete)与target_task_id用于与任务系统双向同步,source_segment_ids回指支撑它的转写片段。
静态方法actions_to_string会把行动项列表格式化为- 描述 (pending|completed) [Created: ...] [Due: ...]的文本,便于直接拼进 LLM prompt 或通知。
Event与Structured—— 结构化笔记
Event表示从对话中提取的日历事件(title、description、start、duration分钟数、created),as_dict_cleaned_dates会把start序列化为 ISO 字符串。
Structured是整段对话的结构化总结,字段包括title、overview、emoji、category、sections(Section为"标题 + Markdown 正文 + 支撑片段 ID"的笔记小节)、action_items与events。其category字段由CategoryEnum约束,枚举覆盖 personal、education、health、finance、legal、work、sports、technology、business 等三十余个分类,并带一个mode="before"的字段校验器:非法分类值会被静默兜底为CategoryEnum.other,保证下游容错。__str__则把标题、分类、概览、行动项与事件渲染为统一文本。
其余模型
ConversationPhoto:会话照片(base64 内容 + 描述 + 创建时间 +discarded标记),photos_as_string用于把照片描述渲染为文本列表;PluginResult/AppResult:插件/应用运行结果,仅含plugin_id/app_id与content文本;Geolocation、ExternalIntegrationCreateConversation、ExternalIntegrationConversationSource:供外部集成以文本创建会话的入参模型(text_source区分audio_transcript与other_text);EndpointResponse:webhook 的标准响应,只有一个message字段——"如需通知用户的一条短消息",默认空字符串。
2.4 SDK 模型的测试保障
SDK 自带测试 plugins/omi-plugin-sdk/tests/test_models.py,与models.py同处一个包内。对于任何新增插件,直接复用这套模型即可保证 webhook 载荷解析与后端行为一致,无需在插件内重复定义Structured/ActionItem/Event(这正是 plugins/PLUGIN_REFACTOR_AUDIT.md 中"Before → After"对照表所解决的问题:此前同一组模型在 backend、根plugins/models.py、Dropbox 中各自漂移重复,如今收敛为单一实现)。
三、omi-*-app/:28 个可独立部署的 FastAPI 插件服务
3.1 自包含服务的组织原则
每个omi-<name>-app/目录(notion、github、slack、dropbox、whoop……)都是一个自包含的 FastAPI 服务,拥有自己的main.py、依赖文件与部署描述符(Dockerfile / Procfile / railway.toml),彼此独立部署、也与遗留单体相互独立。README 记载这类服务共 28 个(具体数量以仓库当前目录清单为准)。
以 plugins/omi-notion-app/ 为例,其目录构成展示了典型形态:
omi-notion-app/ ├── Procfile # Heroku 风格进程声明 ├── README.md ├── db.py # 业务数据存取 ├── main.py # FastAPI 应用入口 ├── models.py ├── notion_content.py # Notion 内容拼装 ├── railway.toml # Railway 部署描述符 ├── requirements.txt # 独立依赖(不含 SDK) ├── test_main.py # 应用级测试 └── test_pagination.py关键点在于:业务逻辑(OAuth 状态、持久化设置、提供商客户端)全部留在 app 内部,SDK 只提供 webhook 载荷模型——这是 plugins/omi-plugin-sdk/README.md 明确划定的边界:"App-specific OAuth state, persisted settings, provider clients, and business logic stay inside each app."
3.2 部署描述符:以 Notion 插件为例
plugins/omi-notion-app/railway.toml 展示了 Nixpacks 构建方式的配置:
[build] builder = "nixpacks" [deploy] startCommand = "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}" healthcheckPath = "/health" healthcheckTimeout = 100 restartPolicyType = "on_failure" restartPolicyMaxRetries = 3值得注意的细节:
- 启动命令读取
PORT环境变量并默认 8080,与遗留单体 Dockerfile 的EXPOSE 8080端口约定一致; - 健康检查路径为
/health,超时 100 秒,失败重启策略为on_failure、最多 3 次重试; - 该服务的
requirements.txt(plugins/omi-notion-app/requirements.txt)是独立于 monolith 的依赖集(fastapi、uvicorn、python-dotenv、requests、pydantic、Jinja2、python-multipart、redis 等),并不包含./omi-plugin-sdk——因为 Notion 服务未导入 SDK 模型,正如审计文档所列"未添加 SDK 依赖,除非 app 导入了 SDK 支撑的模型"。
3.3 使用 SDK 的独立服务与隔离构建风险
与 Notion 不同,dropbox、linear、hive、shopify、shipbob 等服务在requirements.txt中以../omi-plugin-sdk安装 SDK。审计文档 plugins/PLUGIN_REFACTOR_AUDIT.md 明确了两类事实:
- 迁移成果:
omi-dropbox-app已迁移为通过 SDK 的Conversation解析 webhook(此前其本地ActionItem/Structured与后端漂移);linear / hive / shopify / shipbob 的"未来用 Omi webhook 模型"均直接 re-export SDK,业务模型保持本地; - 构建约束:
../omi-plugin-sdk只在"仓库 checkout 且 SDK 与 app 为同级目录"的构建模式下有效。若某服务被配置为隔离根目录(排除同级目录),依赖安装将失败——这是评估任何"拆出独立服务"方案时必须先验证的前提。
3.4 契约检查脚本:check_plugin_imports.py
plugins/scripts/check_plugin_imports.py 是对"隔离根构建"模式的防护性检查工具。它遍历所有plugins/omi-*-app/main.py,逐个执行:
- 把 app 目录与 SDK 的
src目录临时加入sys.path,os.chdir到 app 目录; importlib.import_module("main")导入服务,要求模块暴露app属性;- 调用
app.openapi()生成 OpenAPI 模式,验证 FastAPI 应用可正常初始化; - 每次导入前通过
_purge_plugin_modules清理上一次导入的插件模块缓存,避免跨目录模块串扰。
脚本最终打印checked=N并汇总 blockers(缺失依赖、无app属性、导入异常等),任一 blocker 存在即返回退出码 1。这个脚本本质上是在构建期之前做一次"每个 app 能否在自身目录内独立解析并启动"的冒烟验证。
四、遗留单体:main.py+Dockerfile
4.1 路由构成与启动方式
plugins/main.py 是旧的"一站式"插件 API,通过uvicorn main:app启动(默认监听0.0.0.0:8080)。从源码可见其挂载的 live routers:
| 路由模块 | 说明 |
|---|---|
basic/(conversation_created) | 核心会话 webhook(conversation_created、mentor 等) |
oauth/ | OAuth 流程相关 webhook |
zapier/ | Zapier 集成 webhook |
chatgpt/ | ChatGPT 集成 |
subscription/ | 订阅相关接口 |
notifications/(hey_omi) | 通知触发 |
iq_rating/ | IQ 评分 |
_multion/ | 最后一个遗留集成(处于休眠状态) |
main.py中还保留了被注释掉的 REALTIME 插件注册代码,并附有一段有价值的弃用说明:实时类插件因"每 3 秒跑一次 LLM 逻辑、每天运行 10 小时成本过高、触发方式低效、缺少杀手级用例"等原因被搁置。根路由/返回可用路由清单(/score/、/subscription/、/chatgpt/、/docs),静态资源通过app.mount("/templates/static", ...)挂载自templates/目录——该目录存放单体提供的设置流程 HTML(见 plugins/templates/)。
4.2 Dockerfile 与依赖装配
plugins/Dockerfile 采用两阶段构建:
FROM gcr.io/based-hardware-dev/python:3.11-slim-forky AS builder COPY plugins/requirements.txt /tmp/requirements.txt COPY plugins/omi-plugin-sdk /app/omi-plugin-sdk WORKDIR /app RUN pip install --no-cache-dir -r /tmp/requirements.txt FROM gcr.io/based-hardware-dev/python:3.11-slim-forky COPY plugins/ . EXPOSE 8080 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]其中COPY plugins/omi-plugin-sdk /app/omi-plugin-sdk这一行,正是为了让requirements.txt中的./omi-plugin-sdk路径依赖在镜像内可解析——构建期先把 SDK 复制进镜像,再整体安装依赖。运行时层还安装了ffmpeg curl unzip等系统包(针对 #7136 的安全补丁)。
4.3 保留原因与维护红线
单体保留的唯一原因是部署目标仍存在:plugins/LEGACY_MONOLITH.md 明确记载,.github/workflows/gcp_plugins.yml仍会构建plugins/Dockerfile并以uvicorn main:app从plugins/包根启动;只要该 Cloud Run 目标未被替换或移除,删除单体会导致部署链路断裂。
LEGACY_MONOLITH.md 同时给出了清晰的维护规则:
- 不要向单体添加新的插件业务逻辑(新功能一律走独立
omi-*-app服务); - 保持根 plugins/models.py 作为
omi_plugin_sdk.models的兼容层(它 re-export SDK 模型,并额外保留单体专用的实时/主动通知模型,如RealtimePluginRequest、ProactiveNotificationResponse及其question+filters(people / entities / topics)上下文查询结构); - 不要删除
_multion,除非先替换或移除 GCP 插件部署目标; - 保持
plugins/Dockerfile与plugins/Dockerfile.datadog与单体决策对齐。
五、其他目录:兼容层、指令内容与独立托管服务
models.py(根级兼容层):如 4.3 所述,re-exportomi_plugin_sdk.models的全部模型,并新增主动通知(proactive notification)响应模型。SDK 与兼容层并存,使旧 import 路径无需改动即可继续工作;scripts/:check_plugin_imports.py即上文 3.4 的契约检查脚本;instructions/:每个 app 的指令/资产内容,由移动端应用读取展示;hume-ai/、composio/、uber_call/:托管在 monorepo 部署工作流之外的独立服务;logos/、import/:静态资产与导入工具;.env.template:单体所需环境变量清单(README 提及,用于本地配置)。
六、历史决策与迁移审计:为什么是现在这个形态
plugins/PLUGIN_REFACTOR_AUDIT.md 是理解当前形态的关键决策记录(2026-06-29):
- 单一模型实现:
Structured、ActionItem、Event曾分散在后端、根plugins/models.py、Dropbox 等多个位置并发生漂移,重构后统一收敛到omi-plugin-sdk/src/omi_plugin_sdk/models.py,后端与插件文件改为 re-export; - 后端镜像的兼容回退:由于
backend/Dockerfile只复制backend/目录(不含 SDK),backend/models/structured.py 在 SDK 不可用时保留后端本地的兼容实现;完整仓库运行时才导入 SDK 版本; - 逐步迁移而非破坏性重构:已迁移服务(dropbox 等)用
../omi-plugin-sdk相对路径安装;未验证"隔离根构建"可行性前,不做破坏性删除。
七、总结
Friend 的plugins/目录呈现了一条清晰的演进路线:从"单一遗留单体"走向"共享模型契约 + 独立微服务"。omi-plugin-sdk用最小的 Pydantic 依赖承载了 webhook 载荷的全部字段语义(会话、转写片段、行动项、结构化笔记、外部集成入参),omi-*-app各自独立打包部署并只通过../omi-plugin-sdk复用模型,而遗留单体则以LEGACY_MONOLITH.md的红线被"冻结"。对于插件开发者而言,正确的实践是:业务逻辑放进独立 app,webhook 模型统一 importomi_plugin_sdk.models,并通过check_plugin_imports.py验证服务的隔离构建可行性。这一套体系同时服务于 AI 转写处理、OAuth 集成、通知与主动提醒等场景,是理解 Friend 开放生态的入口。
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考