
Omi Backend Hermetic E2E Harness 全解析零外部依赖运行真实 FastAPI 后端的密封集成测试【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend导读本文深入剖析 OmiFriend开源仓库中的backend/testing/e2e密封式Hermetic后端端到端测试框架。该框架直接导入真实的 FastAPI 后端应用却通过内存级 FakeMockFirestore、fakeredis、文件系统版 GCS 等与本地 socket 网络守卫在完全不触碰 Firestore、Redis、GCS、Pinecone、Typesense、Google ADC 与任何生产 API Key 的前提下跑通核心 CRUD、移动端生命周期、账户删除、Webhook、任务集成、检索等关键路由。读完本文你将掌握该 Harness 的运行方式、密封机制原理、场景覆盖矩阵、测试编写范式以及它如何作为后端 PR 的强制 CI 检查门禁。一、为什么需要Hermetic密封的 E2E 测试传统端到端测试通常依赖真实的数据库、消息队列与第三方服务这让本地环境难以复现、CI 不稳定、外呼服务产生费用与安全隐患。Omi 的backend/testing/e2e采用了相反的设计哲学见 READMEA manually runnable integration test suite that imports thereal omi FastAPI backendand exercises selected routes againstfaked or disabled external dependencies.它有两个核心定位本地 dogfood 工具开发者可手动运行作为日常自测的试运行环境强制 CI 检查作为backend/**变更 PR 的必需 GitHub Actions 检查项。截至文档记录当前 dogfood 状态为113 passed, 3 skipped, 24 warnings该套件在导入任何后端代码之前先安装仅允许本地 socket的网络守卫任何非本地的 DNS/socket 尝试都会抛出断言异常因此真实 API 调用会让测试失败fail closed而不是静默泄漏真实流量。同时 runner 用进程级超时E2E_PYTEST_TIMEOUT默认120s包裹 pytest保证 WebSocket/provider 接缝处的回归问题快速失败而不是无限挂起。二、快速上手安装依赖与一键运行2.1 安装 E2E 专属依赖E2E 依赖独立于后端运行时依赖一次性安装即可cd backend python -m pip install -r testing/e2e/requirements.txtrequirements.txt 中的依赖如下包版本用途pytest8.4.1测试框架pytest-asyncio1.4.0异步 WebSocket/流式测试支持pytest-xdist3.8.0并行执行fake-firestore0.13.1Firestore 的内存级替身MockFirestorefakeredis[lua]2.36.2Redis 的内存级替身含 Lua 脚本支持pytest-httpserver1.1.5本地 HTTP 假服务器LLM/Webhook 等aioresponses0.7.8aiohttp 响应拦截注意run.sh只校验这些依赖是否已安装不会动态安装。这样测试入口本身不会在 pytest 导入 socket 守卫之前触达 PyPI。2.2 一键运行bash backend/testing/e2e/run.shrun.sh 的执行流程值得注意定位虚拟环境依次探测$REPO_ROOT/.venv、$BACKEND_DIR/.venv、$REPO_ROOT/venv、$BACKEND_DIR/venv并激活校验依赖先验证fake_firestore、fakeredis、pytest_httpserver、aioresponses再验证fastapi、firebase_admin、google.cloud.firestore创建临时目录main.py期望存在的_temp、_samples、_segments、_speech_profiles设置内存模式export MEMORY_MODE${E2E_MEMORY_MODE:-read}——生产清单默认MEMORY_MODEoff而 Hermetic 套件必须显式开启read模式才能跑通用记忆 CRUD同时保留E2E_MEMORY_MODEoff覆盖口供 incident-fence 测试暂停写入预热 tokenizer 缓存在 pytest 导入 socket 守卫前调用tiktoken.encoding_for_model(gpt-4)避免冷缓存首次下载 tokenizer 数据被误判为外部网络请求执行 pytest优先使用 GNUtimeoutmacOS 可用brew install coreutils安装gtimeout以--preserve-status --kill-after5s包裹超时退出码 124/143时明确报错。无参数时默认追加-v --tbshort。退出码语义0全部通过1存在失败pytest 退出码透传。2.3 运行单个场景使用 pytest 的-k表达式按关键字筛选# CRUD / 数据形状 bash backend/testing/e2e/run.sh -k test_crud # 会话处理与状态接缝 bash backend/testing/e2e/run.sh -k conversation_processing # 核心 listen/sync/会话生命周期接缝 bash backend/testing/e2e/run.sh -k core_flow_expansion # Listen/STT WebSocket 路由接缝 bash backend/testing/e2e/run.sh -k listen_stt # 存储支撑的语音画像路由 bash backend/testing/e2e/run.sh -k storage_speech_profile # Webhook 与任务集成接缝 bash backend/testing/e2e/run.sh -k webhooks or task_integrations # 检索/搜索接缝 bash backend/testing/e2e/run.sh -k search or retrieval or embedding or vector # 用户/认证/画像/账户路由 bash backend/testing/e2e/run.sh -k user_auth_profile # 持久化账户删除的 Cloud Tasks 生命周期 bash backend/testing/e2e/run.sh -k account_deletion_cloud_tasks # 移动端生命周期 / 客户端兼容 bash backend/testing/e2e/run.sh -k mobile_lifecycle # 失败/边缘模式 bash backend/testing/e2e/run.sh -k test_failure_modes # 遗留形状兼容 bash backend/testing/e2e/run.sh -k test_migration_safety三、v1 范围场景覆盖矩阵v1 的目标是证明后端可以在完全密封的状态下启动并让核心 CRUD、移动端生命周期、用户/账户、存储、Webhook、任务集成、listen 路由、同步任务、会话生命周期、检索/搜索、确定性处理接缝与遗留形状等路径在没有真实 Firestore、Redis、GCS、Pinecone、Typesense、Google ADC 和生产 API Key的情况下完整执行。文档原文的覆盖矩阵如下完整继承场景状态说明CRUD golden path✅ Green会话因POST /v1/conversations处理的是进行中的会话而直接 seedaction items 与 memories 使用真实的创建/更新/删除路由移动端生命周期 / 客户端兼容✅ Green针对 Flutter/桌面端流断言会话列表/详情、记忆创建/列表/可见性/删除、action-item 列表/更新/批量/pending-sync/删除、语言与转写偏好等响应形状。无通用创建路由处仍直接 seed 会话确定性会话处理接缝✅ Partial重新处理与 finalize 路由、鉴权、模型序列化、Firestore 更新、记忆回读、action-item 可查询性均在provider 重处理函数被确定性输出替换下运行完整 LLM 客户端接线留待 v2Listen/STT 路由接缝✅ Partial/v4/web/listen的 WebSocket 鉴权/查询解析/自定义-STT 分发用假 stream handler 覆盖自定义-STT suggested transcript 事件也走真实 listen WebSocket 循环进入客户端发射、重连行为、解密会话回读与 finalize 生命周期完整 Deepgram 兼容流式假件留待 v2Sync v2 任务生命周期✅ Partial/v2/sync-local-files快速路径、Redis 任务创建、确定性后台管线完成、任务轮询与会话持久化在 decode/VAD/STT/provider 重处理段被确定性接缝替换后运行完整音频解码与 provider 转写仍属底层/单元或 v2 假件工作存储 / 语音画像✅ Greengoogle.cloud.storage.Client被替换为临时目录假件画像存在性、签名 URL、样本列表、删除路径均走真实路由/辅助函数Webhooks✅ Partial开发者 Webhook 配置/状态路由、禁用时 no-op、realtime 投递载荷、非 2xx 失败健康记录、超时/异常健康记录、阈值自动熔断均用httpx.MockTransport覆盖Marketplace 应用 Webhook 重试/熔断留待 v2任务集成✅ GreenCRUD/默认/删除路径 已连接/未连接/无 token、Todoist 成功、provider 500、401 断开、超时失败路径都在假 Firestore 上跑真实的任务集成数据库辅助函数用户/鉴权/画像/账户✅ Green鉴权守卫、画像、onboarding、语言/转写偏好、联系人 CRUD、通知/助手设置、AI 画像、BYOK 激活/停用路由均已覆盖。账户删除额外在其真实准入路由、持久化标记、不透明 Cloud Tasks 载荷、worker 认领、required-purge 重试与幂等重投递上演练本地假件Firebase 删除、计费查询、Twilio 与派生数据清理保持为受控测试接缝检索/搜索✅ Partial记忆、action-item、会话摘要与转写片段检索路由走真实公开 API以 Firestore 支撑记录 database.vector_db客户端接缝处的确定性内存替代替代 Pinecone/OpenAI embeddings完整 Pinecone/Typesense 服务兼容不在范围内失败/边缘模式✅ Partial非法输入与边缘用例覆盖已运行Redis 不可用、LLM 500、STT 超时等用例显式跳过或推迟到逐测试失败假件接线完成遗留形状兼容✅ Green演练遗留会话/记忆形状与确定性假存储重复写入不执行生产迁移脚本源码佐证这套矩阵在目录中都有对应测试文件例如 test_crud.py、test_mobile_lifecycle_compatibility.py、test_listen_stt.py、test_sync_lifecycle.py、test_storage_speech_profile.py、test_webhooks.py、test_task_integrations.py、test_user_auth_profile.py、test_account_deletion_cloud_tasks.py、test_retrieval_search.py、test_failure_modes.py、test_migration_safety.py。四、什么被 Fake、什么被禁用下表完整继承文档的依赖处理策略依赖v1 行为原因Firestorefake-firestore的MockFirestore内存数据存储支撑真实数据库模块Redisfakeredis内存 Redis 替代Google Cloud Storagegoogle.cloud.storage.Client被替换为文件系统假件无需 GCS 凭据/网络即可支撑存储路由Cloud Tasks / OIDC严格内存版tasks_v2.CloudTasksClient 账户删除生命周期测试中的本地 token 校验接缝不依赖 Cloud Tasks 控制平面或 Google token 校验即可演练生产任务 protobuf、队列载荷、OIDC 身份/audience 与重试头Google ADCgoogle.auth.default返回匿名凭据防止导入时发生真实凭据查找Pinecone全局移除PINECONE_API_KEY定向检索/搜索测试将database.vector_db.indexmonkeypatch 为确定性内存假件保证应用导入密封同时允许无真实 Pinecone 的路由级向量 upsert/query/delete 断言Typesense哑 host/port/API key让导入期 Typesense 客户端构造成功检索/搜索测试依赖向量结果与 fail-open 关键词搜索而非真实 Typesense 兼容Google Translate匿名 Google 凭据允许导入期客户端构造v1 测试不调用实时翻译LLM/STT/VAD/embeddings假模块已搭骨架路由与 custom-STT suggested-transcript 接缝在可确定性 patch 处覆盖需要真实出站 HTTP/WS/provider 断言的场景留待 v2Webhook/任务外部 HTTP定向测试使用httpx.MockTransport无网络情况下捕获出站载荷4.1 源码级佐证conftest 的环境覆盖conftest.py 的_set_e2e_env()以覆盖overwrite而非 setdefault方式写入环境变量刻意防止开发者 shell 中的真实 Key 泄漏进 E2E 运行禁用 dotenvPYTHON_DOTENV_DISABLED1并把dotenv.load_dotenv替换为空操作移除真实凭据变量SERVICE_ACCOUNT_JSON、GOOGLE_APPLICATION_CREDENTIALS、FIREBASE_AUTH_CREDENTIALS_PATH全部 pop彻底移除PINECONE_API_KEY——注释明确指出空字符串仍会触发Pinecone(api_key)并在导入期失败所以必须 pop 而非置空关闭 Typesense 会话索引写入TYPESENSE_CONVERSATION_INDEX_WRITES0因为本机 8108 端口拒绝连接的重试会每个持久化会话阻塞约 5 秒击穿 E2E 时间预算注入假 KeyDEEPGRAM_API_KEY、OPENAI_API_KEY、ANTHROPIC_API_KEY、OPENROUTER_API_KEY、GOOGLE_API_KEY、TYPESENSE_API_KEY均为fake-*占位关闭 Stripe 校验STRIPE_SECRET_KEY、ADMIN_KEY清空全部代理变量HTTP_PROXY/HTTPS_PROXY等防止出站流量经代理逃逸。4.2 关键 patch 顺序时序敏感conftest 顶部有一段关键顺序注释database/_client.py在模块级line 14调用firestore.Client()如果google.auth.default尝试真实凭据会直接DefaultCredentialsError。因此 conftest 在 import 任何 Firestore 相关模块之前先把google.auth.default替换为返回AnonymousCredentials()的函数。随后才依次 patchfirestore.Client、redis.Redis、google.cloud.storage.Client最后import main导入真实应用。五、什么是真的文档明确列出了 Harness 中保持真实的部分完整继承FastAPI 应用导入main.app即 backend/main.py 的app FastAPI()路由器、中间件、鉴权依赖、WebSocket 路由入口、Pydantic 请求/响应校验数据库模块与模型序列化/反序列化由MockFirestore支撑的 Firestore 查询/更新/删除代码路径Redis 客户端构造与委托给 fakeredis 的操作由临时目录假 GCS 支撑的存储辅助代码路径也就是说只有外部依赖是假的业务代码全部真实执行——这正是集成测试与单元测试的本质区别。六、架构与目录结构文档给出的架构图完整继承run.sh └── pytest testing/e2e/ ├── conftest.py # env, auth, fake setup, TestClient ├── fakes/ │ ├── firestore.py # MockFirestore seed/read helpers │ ├── redis.py # FakeRedis redis.Redis patch │ ├── storage.py # filesystem-backed fake GCS client │ ├── vector_search.py # deterministic embeddings in-memory Pinecone-like index │ ├── llm.py # deterministic LLM fake scaffold │ ├── stt.py # deterministic custom-STT event helper; Deepgram WS TODO │ └── embeddings.py # VAD/diarization/embedding fake scaffold ├── fixtures/ │ ├── conversations.json │ ├── memories.json │ └── action_items.json ├── test_core_flow_expansion.py ├── test_crud.py ├── test_conversation_processing.py ├── test_conversation_processing_deterministic.py ├── test_account_deletion_cloud_tasks.py ├── test_failure_modes.py ├── test_harness_guards.py ├── test_listen_stt.py ├── test_migration_safety.py ├── test_mobile_lifecycle_compatibility.py ├── test_retrieval_search.py ├── test_storage_speech_profile.py ├── test_task_integrations.py ├── test_user_auth_profile.py └── test_webhooks.py实际目录中还扩展了文档发布后新增的内容test_sync_lifecycle.py、test_v3_memories_route.py、test_canonical_memory_pipeline.py、test_boundary_contract_compatibility.py、test_listen_pusher_wire_contract.py、listen_test_helpers.py、sync_helpers.py以及 fakes/listen_pusher_wire.py印证了core-flow / listen / sync / v3 memories等接缝的持续扩展。6.1 Fake 实现要点fakes/firestore.py基于fake_firestore.MockFirestore并额外 patch 了 4 处 fake 与真实 Firestore 的行为差异set(mergeTrue)保留父文档已有的子集合数据避免覆盖嵌套数据DELETE_FIELD对缺失键是 no-opfake 原会抛KeyError导致首次 sync ledger 认领变成 500删除不存在的文档应成功原会抛KeyErrorto_dict()暴露文档 ID 哨兵__name__以匹配 Firestore 按保留文档 ID 字段排序的行为。提供seed_conversation/seed_memory/seed_action_item与read_*系列辅助函数其中seed_memory会补齐scoring00_999_0000000000、visibility、reviewed等默认值让遗留形状测试演练后端校验而非 fake-firestore 更严格的实现细节。fakes/redis.py创建fakeredis.FakeRedis单例patchredis.Redis.__init__后把 fakeredis 的全部公开方法委托过去——注释强调精选 allowlist 会落后于后端 Redis 用法mget、setex、集合、哈希、Lua 脚本辅助等并可能落到真实客户端因此选择全量委托同时 patchRedis.from_url、StrictRedis.from_url与模块级from_url。fakes/storage.pyFakeStorageClient把 bucket/blob 落到tempfile.mkdtemp目录覆盖upload_from_filename、download_as_bytes、generate_signed_url、list_blobs等常用 API并预创建speech-profiles、postprocessing、omi-private-cloud-sync、sync-temporal、memories-recordings等 10 个默认 bucket。fakes/vector_search.pyDeterministicEmbeddings用 SHA-256 生成稳定的 16 维向量FakeVectorIndex实现内存版 Pinecone 风格upsert/update/delete/query含$and/$or/$eq/$in/$gte/$lte过滤器query 用 Dice 式 token 重合评分保证多 token 精确匹配能超过 action-item 的min_score0.3阈值。install_vector_search_fakes通过 monkeypatch 替换database.vector_db的embeddings与index两个模块级接缝。fakes/llm.py基于pytest-httpserver提供 OpenAI/v1/chat/completions、Anthropic/v1/messages、OpenRouter/api/v1/chat/completions与 embeddings 端点的确定性响应configure_llm_error(status_code500)用于失败模式测试的优雅降级验证。fakes/stt.pyFakeStreamingSTTSocket模拟 Parakeet/Modulate 的流式 socket支持die_on_first_send合成provider 中途断开以演练 failoverfake_suggested_transcript_event()产出 custom-STT 的suggested_transcript事件注入真实 listen WebSocket 循环。七、密封机制深度解析四道防线7.1 第一道防线socket 级网络守卫conftest 在 import 任何后端代码前替换了socket.socket.connect、connect_ex、sendto、socket.create_connection与socket.getaddrinfo。只有{127.0.0.1, ::1, localhost, 0.0.0.0}内的地址放行其余一律raise AssertionErrorDNS 查询、TCP 连接、UDPsendto两参数/三参数形式分别拦截。这意味着任何漏网的真实外呼都会把用例打红而不是悄悄访问外网。该防线的有效性由 test_harness_guards.py 自证test_dotenv_loading_is_disabled本地.env无法重新注入SERVICE_ACCOUNT_JSON、PINECONE_API_KEYtest_e2e_environment_clears_firebase_auth_credential_path继承的FIREBASE_AUTH_CREDENTIALS_PATH被清除test_network_guard_blocks_external_dns_lookup/create_connection/sendto外网 DNS、TCP、UDP 全部 fail closedtest_backend_storage_client_is_fake_after_app_import断言utils.other.storage持有的确实是FakeStorageClienttest_backend_database_globals_are_fake_after_app_import断言database._client.db、database.redis_db.r、database.webhook_health.r、utils.fair_use.redis_client均指向 fake 实例test_backend_registered_redis_scripts_are_rebound_to_fakeLua 脚本边界不得保留导入时的 localhost 客户端。7.2 第二道防线import 竞态下的模块级全局重绑conftest._create_backend_app中有一段fails closed的补救逻辑部分后端模块用from database._client import db或from database.redis_db import r在 import 时绑定模块级全局变量如果某个模块的 import 抢在构造函数 monkeypatch 之前执行就会持有真实客户端。conftest 遍历sys.modules中所有模块的属性把指向旧db/旧r的属性全部替换为 fake 实例同时把已注册的 LuaScript对象逐一用fake_redis.register_script(script)重新注册——注释明确指出仅重绑redis_db.r会让 rate limit 等 Lua 路径继续对话本机 localhost Redis既破坏密封性又让计数器跨 wrapper 运行泄漏。7.3 第三道防线dotenv 禁用与凭据清除如 4.1 节所述PYTHON_DOTENV_DISABLED1 替换load_dotenv pop 真实凭据变量三重保证本地.env无法复活生产凭据。7.4 第四道防线进程级超时run.sh用 GNUtimeout --preserve-status --kill-after5s包裹 pytest超时值由E2E_PYTEST_TIMEOUT控制默认120s调试慢速运行时可设E2E_PYTEST_TIMEOUT300s确保 WebSocket/provider 接缝的挂起回归在 CI 上超时失败而非无限消耗资源。八、测试生命周期文档定义的九步生命周期完整继承并标注对应源码位置导入后端模块前设置密封环境变量conftest._set_e2e_env在任何 Firestore/Translate 客户端构造前 patch Google authgoogle.auth.default→ 匿名凭据构建内存 Firestore/Redis fake 与临时目录 fake GCSsetup_fake_firestore/setup_fake_redis/setup_fake_storage禁用 dotenv 加载防止本地.env重新注入真实凭据在import main之前 patch Firestore/Redis/Storage 客户端构造函数patch_google_firestore/patch_redis_client/patch_google_storage导入真实 FastAPI 应用并用TestClient包裹TestClient按测试函数级创建以隔离状态每个测试前后清空 fake Firestore/Redis/Storage 状态isolate_e2e_stateautouse fixture还会清理 Webhook 熔断器、STT 熔断器状态与 webhook 健康记录脚本缓存后端无通用创建端点处先 seed 数据fakes.firestore.seed_*通过真实应用执行路由级断言。8.1 常用 Fixtureconftest 提供一组开箱即用的 fixtureclient核心 fixture返回包裹真实后端的TestClientauth_headers开发 token 鉴权头Authorization: Bearer dev-token固定 dev UID 为123test_uid/fresh_uid固定 UID 与每次唯一的 UUIDconversation_fixture/memory_fixture/action_item_fixture从fixtures/*.json加载测试数据sample_conversation_data/sample_memory_data/sample_action_item_data可直接用于 API 创建的样本字典含结构化字段、转写片段、时间戳等。九、如何新增测试文档给出的黄金准则是优先走真实公开路由如果后端不存在用于构造数据的通用路由例如任意会话创建则先用fakes.firestore.seed_*seed再测目标路由。文档示例完整继承from fakes.firestore import seed_conversation def test_read_seeded_conversation(client, auth_headers, sample_conversation_data): seed_conversation(123, sample_conversation_data) resp client.get( f/v1/conversations/{sample_conversation_data[id]}, headersauth_headers, ) assert resp.status_code 200结合 test_crud.py 可以观察到完整的 CRUD 范式会话无通用创建路由因此seed_conversation后走真实GET /v1/conversations/{id}、GET /v1/conversations、PATCH /v1/conversations/{id}/title、DELETE /v1/conversations/{id}Action Items走真实创建POST /v1/action-items、列表、PATCH /v1/action-items/{id}、PATCH /v1/action-items/{id}/completed、DELETEMemories走POST /v3/memories、GET /v3/memories、PATCH /v3/memories/{id}?value...、DELETE /v3/memories/{id}、POST /v3/memories/batch断言created_count。另有数据形状保持类测试TestDataShapePreservation逐字段断言 round-trip 后会话/action-item 的字段完整性用于捕捉模型序列化回归。十、当前限制与 v2 路线图文档明确的 v2 工作清单完整继承增加 custom-STT listen 重连/finalize、sync v2 任务生命周期与会话 finalization 的密封核心流覆盖为/v4/listen/ pusher 场景实现 Deepgram 流式 WebSocket fake将确定性 LLM 端点接入处理代码用到的全部 OpenAI/Anthropic/OpenRouter 客户端为 LLM 500 / 超时场景增加逐测试 HTTP 失败注入增加真实 Redis 不可用的 fail-open 测试v1 走 fakeredis 路径如需迁移脚本覆盖对 fake fixtures 执行生产迁移脚本在开发者 realtime transcript 失败/自动熔断路径之外增加 marketplace 应用 Webhook 重试/熔断测试改进 fake-firestore 对嵌套任务集成单文档 lookup/delete 的支持使任务创建路由不再需要确定性 lookup patch在确定性内存向量接缝之外扩展检索/搜索覆盖真实 Typesense 关键词行为与更接近 Pinecone 的响应兼容若服务契约纳入范围在类 CI 环境跑通 Python 3.11必需的Backend Hermetic E2EGitHub Actions 现已安装依赖、预热 tokenizer 缓存并运行 Harness。从目录可以看到部分已完成的扩展例如 test_sync_lifecycle.py 承接 sync v2 生命周期、test_listen_pusher_wire_contract.py 与 fakes/listen_pusher_wire.py 面向 listen/pusher 接缝、test_v3_memories_route.py 覆盖 v3 记忆路由。十一、依赖总览Omi 后端运行时依赖来自backend/requirements.txtE2E 专属依赖来自 backend/testing/e2e/requirements.txt见 2.1 节表。需要强调的是run.sh只校验依赖存在性而不自动安装从而保证测试入口本身在 socket 守卫生效前不触网若依赖缺失脚本会提示先运行cd backend ./scripts/sync-python-deps.sh再重试。结语Omi 的 Hermetic E2E Harness 给出了一条可复制的工程路径让真实业务代码在完全本地化的假依赖上完整执行再用 socket 守卫强制密封。它不仅为backend/**的 PR 提供了可本地复现、可进 CI 的回归门禁也通过test_harness_guards.py这种守卫自测确立了测试基础设施本身的可靠性。对于需要为高依赖后端搭建可控集成测试的团队这套真实 App 假外部边界 网络 fail-closed 进程超时的四层组合是一个可以直接借鉴的实践范本。【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考