Omi Frontend:h.omi.me 公开分享页与商店前端的技术架构与 Cloud Run 部署指南
2026/9/17 11:04:49 网站建设 项目流程

Omi Frontend:h.omi.me 公开分享页与商店前端的技术架构与 Cloud Run 部署指南

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

本文基于 Friend(Omi)开源仓库中 web/frontend/README.md 展开,系统讲解frontend这一 Next.js 公开 Web 服务的定位、技术栈、本地开发、CI 检查与 Cloud Run 部署全链路,并结合 next.config.mjs、Dockerfile、gcp_frontend.yml 与 public-build-contract.json 等仓库源码给出源码级佐证。读完本文,你将掌握如何在本仓库中运行、测试、构建并部署 Omi 公开前端,理解其与登录态 Web 客户端web/app的边界,以及/conversations/memories双 URL 空间的实现机制。

一、项目定位:公开分享页与商店门户

web/frontend是一个基于 Next.js App Router 构建的公开 Web 应用,生产环境以 Cloud Run 服务frontend部署在 https://h.omi.me。它承载两类核心页面:

  1. 公开分享页/chat/:id/conversations/:id,用于向外部访客展示 Omi 用户分享的对话与记忆内容;
  2. 公开商店门户:应用市场/apps、开发者入口/create-app、年度回顾/wrapped、以及/unlimited等页面。

README 特别强调了一个关键边界:web/frontend不是登录态 Web 客户端,签名用户使用的 Web 客户端位于web/app(对应 app.omi.me)。两者在仓库中分属独立的 Next.js 工程,拥有各自的 Dockerfile 与部署工作流(web/app走 gcp_app.yml,frontend走 gcp_frontend.yml)。理解这一分工,是后续阅读部署配置的前提。

二、技术栈与目录结构

按 README 与 package.json 的依赖清单,该工程的技术栈为:

层次选型仓库证据
框架Next.js(App Router),当前锁定~16.2.11package.json
语言/样式TypeScript、Tailwind CSStailwind.config.ts、globals.css
UI 组件Radix UI / shadcn 风格组件(accordion、dialog、progress、scroll-area 等)package.json、src/components/ui
数据服务Firebase(auth/Firestore)、Algolia(搜索)、Redis(缓存)package.json
包管理器npmpackage-lock.jsonpackage.json

源码主体位于web/frontend/src,可按职责划分为几块:

  • src/app/:App Router 路由,包括memories/[id](分享页)、chat/[token]appscreate-appwrappedunlimitedtasks/[token]等页面;
  • src/actions/:Server Actions 层,如memories/get-shared-memory.tschat/get-shared-chat.tsapps/submit-app.tstrends/get-trends.ts
  • src/components/:业务组件(memories/plugins/trends/)与通用 UI 组件(ui/);
  • src/lib/:纯函数工具(URL 拼接、Markdown 转纯文本、Firebase 初始化)与 API 客户端;
  • src/__tests__/:基于 Node 内置node --test的测试用例。

值得注意的一点是,仓库根目录同时存在web/admin(管理后台)、web/app(登录态客户端)、web/personas-open-source等多个独立前端工程,web-checks.yml 会通过detect-changes按变更路径精确选择需要执行的 lint/test/build 任务,避免无关工程互相阻塞。

三、本地开发:三行命令启动

README 给出的开发流程非常简洁,直接继承如下:

npm ci cp .env.template .env.local # fill in values npm run dev # http://localhost:3000

三点实操提示(结合源码):

  • 必须用npm ci而非npm install:包管理器固定为 npm,且 Dockerfile 与 CI 均使用 lockfile 冻结依赖,本地保持一致可避免依赖漂移。
  • npm run dev实际执行next dev --turbo(见 package.json),启用 Turbopack 加速开发迭代。
  • 环境变量以 .env.template 为准,其中API_URL默认指向http://127.0.0.1:8787(本地后端服务),WEB_URL默认http://localhost:3000src/constants/envConfig.ts会在运行时读取这些值,其中WEB_URL还会经过shareBaseUrl()归一化处理(见下文第六节)。

工程提供的全部 npm scripts 如下(来自 package.json):

Script命令说明
devnext dev --turbo开发服务器(Turbopack)
buildnext build生产构建(输出 standalone)
startnext start启动生产服务器
testnode --test src/__tests__/*.test.mjs运行 Node 原生测试
linteslint ./src ...ESLint 静态检查
lint:fixeslint ./src ... --fix自动修复
lint:formatprettier ... --writePrettier 格式化

四、测试与 CI:lint + test + build 三道关卡

README 指出测试采用node --test,测试用例位于src/__tests__。从 src/tests目录可见测试覆盖了相当多的关键契约,例如:

  • share-base-url.test.mjs:断言shareBaseUrl()WEB_URL的解析与回退逻辑;
  • shared-api-url.test.mjs:断言sharedApiUrl()对动态路由参数的 URL 编码;
  • shared-conversation-chat-contract.test.mjswrapped-unlimited-deeplink-parity.test.mjs:验证前后端分享链路与深链的一致性。

之所以把share-base-url.mjsshared-api-url.mjs实现为纯 JS 模块,源码注释写得很明确:“Kept as plain JS so node:test can assert without a TS loader”(share-base-url.mjs),这是为降低测试运行成本而做的刻意设计。

CI 侧有两份工作流与本工程相关:

  1. web-checks.yml:push 到main或 PR 到main时触发,先由detect-changes判断是否有has_frontend变更;随后在frontend-lint任务中执行:
npm ci npm run lint npm run lint:format -- --check bash scripts/run-web-frontend-tests.sh

(Node 版本固定为 20,npm 缓存指向web/frontend/package-lock.json。)

  1. gcp_frontend.yml:负责真正的生产部署,详见下一节。

五、生产构建与 Cloud Run 部署

5.1 部署触发条件与目标环境

按 gcp_frontend.yml,部署由两类事件触发:

  • push 事件:分支为maindevelopment,且变更路径命中web/frontend/**config/public-build-*.json.github/actions/deploy-public-build/**.github/scripts/preflight_public_build_*.py.github/scripts/smoke_public_build_browser.py等;
  • workflow_dispatch:支持手动指定environment(可选值仅developmentprod)与可选的release_version

环境映射规则为:development分支 →development环境,main分支 →prod环境。README 明确说明:该服务只有developmentprod两档环境,不存在 staging tier,也没有 docker-compose 配置concurrency组按环境隔离部署队列,且cancel-in-progress: false,避免新一次 push 中断正在进行的滚动发布。

5.2 构建契约:public-build-contract.json

部署动作由 .github/actions/deploy-public-build 复合 Action 执行,其参数化配置来自仓库级的 config/public-build-contract.json。其中targets.frontend段定义了本工程的完整契约:

  • 服务名service: "frontend",生产 URLhttps://h.omi.me
  • Dockerfileweb/frontend/Dockerfile构建上下文为仓库根(build_context: "."),因为 Dockerfile 需要从仓库根复制源码;
  • 部署区域us-central1,平台linux/amd64
  • prod 专属 gcloud 标志--ingress=internal-and-cloud-load-balancing(仅允许内部流量 + Cloud Load Balancing 入口,公网通过负载均衡接入),development环境无额外标志;
  • 运行时 SecretDD_API_KEY(Datadog)与PUBLIC_SHARED_CONVERSATION_CHAT_IP_HMAC_KEY(分享页 IP HMAC 密钥),并显式移除OPENAI_API_KEY等敏感项;
  • 构建输入(inputs)NEXT_PUBLIC_FIREBASE_API_KEYNEXT_PUBLIC_FIREBASE_AUTH_DOMAINNEXT_PUBLIC_FIREBASE_PROJECT_IDNEXT_PUBLIC_FIREBASE_STORAGE_BUCKETNEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_IDNEXT_PUBLIC_FIREBASE_APP_IDNEXT_PUBLIC_FIREBASE_MEASUREMENT_IDAPI_URL全部标记为required: true
  • 候选发布验收(candidate_acceptance):部署候选后执行python3 .github/scripts/smoke_public_build_browser.py --target frontend --base-url {base_url} --expect-sha {sha},通过后以frontend:ready为标记,采用candidate_after_browser_acceptance策略切流量——即先构建“候选版本”并用真实浏览器冒烟,验证通过后才把流量切到新版本。

5.3 部署前校验与发布后验证

工作流在部署前专门校验“前端调用共享对话服务的身份契约”(gcp_frontend.yml):PUBLIC_SHARED_CONVERSATION_CHAT_FRONTEND_INVOKER_SA必须是合法的 Cloud Run 服务账号(用户型xxxx@project.iam.gserviceaccount.com或计算型123-compute@developer.gserviceaccount.com),PUBLIC_SHARED_CONVERSATION_CHAT_FRONTEND_AUDIENCE必须是仅含 origin 的 HTTPS audience,否则直接失败。

部署完成后,工作流调用backend/scripts/deploy_status_report.py对 Cloud Run 服务做 rollout 状态核验,形成“校验 → 构建 → 候选 → 浏览器冒烟 → 切流量 → 核验”的完整闭环。

5.4 多阶段 Docker 构建

Dockerfile 采用经典三段式:

  1. deps阶段:基于node:20-alpine,按 lockfile 优先级执行npm ciyarn/pnpm为兜底分支);
  2. builder阶段:通过ARG注入全部NEXT_PUBLIC_*构建期环境变量,并有一段强制校验逻辑(Dockerfile)——遍历OMI_REQUIRED_PUBLIC_BUILD_INPUTS中列出的 8 个必填变量,任一为空即构建失败,从构建期杜绝“缺环境变量上线”;
  3. runner阶段:非 root 用户nextjs运行,仅拷贝public/.next/standalone.next/staticEXPOSE 3000并以node server.js启动。

注意 next.config.mjs 中的outputFileTracingRoot配置:它把文件追踪根固定到web/frontend自身。源码注释解释了原因——Next.js 15+ 在 monorepo 中会依据兄弟 lockfile 推断工作区根,从而把 standalone 输出嵌套到仓库根路径下,破坏 Dockerfile 的COPY .../.next/standalone ./CMD ["node","server.js"]。这一配置是 monorepo 部署能成功的关键细节。

六、路由机制:/conversations 与 /memories 双 URL 空间

README 的 Notes 一节揭示了一个重要的路由事实:公开分享 URL 对外使用/conversations/:id,但内部页面与组件仍存放在src/app/memories/。其桥梁是 next.config.mjs 中的一对规则:

// 301 永久重定向:旧路径 /memories/* → 新路径 /conversations/* redirects: [{ source: '/memories/:path*', destination: '/conversations/:path*', permanent: true }], // 内部重写:/conversations/* → 仍由 /memories/* 的页面组件渲染 rewrites: [{ source: '/conversations/:path*', destination: '/memories/:path*' }],

两者配合实现了“对外新 URL 稳定、对内组件零迁移”的平滑演进。sitemap 侧同样联动:sitemap.ts 通过getPublicMemoriesPrerender(20000)拉取最多 2 万条公开记忆,并输出memories/<id>路径(development环境下返回空数组,避免污染线上索引)。

其他值得留意的next.config.mjs细节:

  • output: 'standalone':配合上文 Dockerfile 的自包含产物;
  • cacheHandler指向 cache-handler.cjs,其注释表明目的是“绕过 Next.js 2MB fetch 缓存上限”,直接复用框架内置的FileSystemCache实现,让分享页对后端数据的 ISR 缓存(revalidate: 60)可以承载更大响应体;
  • 安全响应头:全站下发X-Frame-Options: DENY(防点击劫持),并对/.well-known/apple-app-site-association设置application/json内容类型以支持 iOS 通用链接;
  • 远程图片白名单images.remotePatterns仅放行raw.githubusercontent.comstorage.googleapis.compbs.twimg.comabs.twimg.comstatic.vecteezy.com
  • Server Actions 请求体上限experimental.serverActions.bodySizeLimit: '10mb',为应用市场缩略图上传等操作预留空间。

七、运行时配置与 URL 安全细节

src/constants/envConfig.ts统一汇集运行时配置,其中两个纯函数工具值得关注:

  • share-base-url.mjsshareBaseUrl(raw)WEB_URL做严格归一化——补全https://前缀、剔除 query/hash/认证信息、去除尾斜杠,非法输入一律回退到默认值https://h.omi.meshareHost()则提取 hostname 用于intent:///omi://深链。该逻辑与后端OMI_SHARE_BASE_URL及 Flutter/桌面端分享助手保持一致(注释引用 issue #4339),保证“一条分享链接多端一致”。
  • shared-api-url.mjssharedApiUrl(base, ...segments)对每个路径段执行encodeURIComponent再拼接。其动机在源码注释中讲得很清楚:Next.js 会把动态路由参数预先解码(如/tasks/a%2Fb变成a/b),若直接拼进 fetch URL 会导致请求误路由或 query 损坏;逐段编码则保证 wire URL 始终合法。

分享页的实际调用链可参考 memories/[id]/page.tsx:generateMetadata通过sharedApiUrl(envConfig.API_URL, 'v1', 'conversations', params.id, 'shared')请求后端共享对话接口,并设置next.revalidate: 60做 ISR 缓存;接口失败或返回非 JSON 时静默降级为默认标题与“Shared Conversation Not Found”文案,兼顾 SEO 元数据生成与健壮性。

八、本地验证与进阶排查路径

若想在本地完整跑通分享页链路,可参考以下顺序(均为仓库内已有资产):

  1. 启动本地后端(使API_URL=http://127.0.0.1:8787可访问),或将其指向已部署的 API;
  2. 按 .env.template 补齐 Firebase 与 Algolia 配置后执行npm run dev
  3. node --test src/__tests__跑契约测试,重点看shared-api-urlshare-base-urlshared-conversation-chat-contract三组用例;
  4. 部署行为验证可阅读 .github/scripts/preflight_public_build_config.py 与 .github/scripts/smoke_public_build_browser.py(均为仓库内真实脚本),理解候选版本在切流量前经过的配置预检与浏览器冒烟步骤。

小结

web/frontend是 Omi 面向公众的“门面”服务:用 Next.js App Router 同时支撑分享页与商店页,以standalone输出 + 多阶段 Docker 构建交付 Cloud Run,并通过public-build-contract.json将服务名、区域、入口策略、运行时 Secret、必填构建输入与“浏览器冒烟后再切流量”的发布策略全部声明化。其/memories ↔ /conversations的双 URL 重写方案、绕过 2MB 缓存上限的文件系统 cache handler,以及为测试友好而刻意保持纯 JS 的 URL 工具函数,都是值得在同类公开分享型 Next.js 服务中复用的工程实践。

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询