标签: supabase

  • 【教程】浏览器扩展中实现一键登录 Google(2)

    【教程】浏览器扩展中实现一键登录 Google(2)

    本文接续前一篇 【教程】浏览器扩展中实现一键登录 Google(1),重点介绍代码相关的部分。

    SSO 简介

    为防止有些同学不了解 SSO,这里简单介绍一下 SSO 的基本原理:

    1. 在自己的网站/工具上启动登录流程
    2. 跳转到第三方网站(Single Site)进行登录
    3. 登录完成之后,被重定向回到自己的网站/工具,回来的地址会包含一些 token
    4. 拿 token 去第三方网站校验,如果成功,将用户信息写入本地系统
    5. 之后就可以用本地的用户信息进行交互

    使用 SSO 的好处是:

    1. 减少用户心智负担,用户只需要在一个网站管理自己的用户信息即可
    2. 应用开发者不再需要费劲开发关于账号安全的功能

    使用 chrome.identity API 整合登录功能

    Chrome Extension API 提供两个登录方式:

    1. getAuthToken() 获得基于 Google 账号的 auth token,可以用来访问特定的 Google 服务,比如 Google Drive,在第三方浏览器比如 Edge 里不可用。
    2. launchWebAuthFlow() 使用上述 SSO 流程,在任何支持 SSO 的网站里完成登录。

    因为我并不打算和 Chrome 深度绑定,所以自然,我们要用第二种。

    由于我们要在浏览器扩展中 SSO,所以无法接受登录之后的重定向,所以上述步骤的(3)会变成在扩展里拿到 access_token,然后去 Supabase 换取用户身份信息,并存储在本地。

    manifest.json

    首先,我们要修改 manifest.json,将需要的 oauth2 信息添加进去。因为我用 CRXJS Vite Plugin,所以这里直接用 TS 来写就好:

    {
      permissions: [
        'identity',
      ],
      oauth2: {
        'client_id': 'XXXX-XXXXXX.apps.googleusercontent.com',
        // 我们只需要 openid 和 email 就够用了
        'scopes': [
          'openid',
          'email',
        ],
      },
    }
    

    Login 方法

    接下来,完成登录函数,代码写出来大约是这样:

    async function doLogin(): Promise<void> {
      // 获取 manifest.json 内容,方便重用代码
      const manifest = chrome.runtime.getManifest();
      let url = new URL('<https://accounts.google.com/o/oauth2/auth>');
      if (!manifest?.oauth2?.scopes) return;
    
      url.searchParams.set('client_id', manifest.oauth2.client_id);
      url.searchParams.set('response_type', 'id_token');
      url.searchParams.set('access_type', 'offline');
      url.searchParams.set('redirect_uri', `https://${chrome.runtime.id}.chromiumapp.org`);
      url.searchParams.set('scope', manifest.oauth2.scopes.join(' '));
    
      let redirectedTo;
      try {
        // 开启 SSO 弹窗,获取重定向地址
        redirectedTo = await chrome.identity.launchWebAuthFlow({
          url: url.href,
          interactive: true,
        });
      } catch (e) {
        return;
      }
    
      url = new URL(redirectedTo);
      const params = new URLSearchParams(url.hash.replace(/^#/, ''));
      // 用 token 去 supabase 进行验证
      const { data, error } = await supabase.auth.signInWithIdToken({
        provider: 'google',
        token: params.get('id_token') as string,
      });
      if (error) {
        return;
      }
    
      userStore.setUser(data.user);
    }
    

    登录一定需要后端验证,但是在 Supabase SDK 的帮助下,我们可以完全跳过这部分开发。Supabase SDK 甚至贴心的提供了 signInWithIdToken() ,所以我强烈推荐各位独立开发者使用 Supabase 作为用户体系的基础。

    要避开几个坑

    这里有几个坑必须小心。

    1. 前面在 Google Cloud 里创建凭据时必须创建“Web应用”,“Chrome 扩展”针对的是 getAuthToken()
    2. Google Cloud 创建应用之后,要等很久才能用,我也不知道为什么。Google Cloud 给出的提示是 10 分钟到数小时,我建议一次配置好之后,至少等待 4 个小时再去开发,可以避免很多意外
    3. 不要使用 getRedirectURL() ,它会在末尾多带一个 / ,导致登录失败。我也不知道为啥会有这么蠢的问题,但是调试几个小时之后,我不得不认为的确是这个问题……

    给没有 Google 账户的用户提供兜底

    总有一些用户没有 Google 账号,所以一般来说我们会给他们提供常规的邮箱+密码登录的方式作为兜底。

    因为使用 Supabase,所以这里的功能实现也比较简单。Supabase 默认开启 Email provider,直接在代码增加一个登录函数即可。因为我们暂时不验证用户邮箱,所以在一个函数里帮用户完成注册/登录。表单我就不贴了,两个文本框一个按钮,很简单。

    
    async function doLoginViaForm(): Promise<void> {
      try {
        const user = await userStore.register(email.value, password.value);
        userStore.setUser(user);
      } catch (e) {
        const msg = (e as Error).message || Object.toString.call(e);
        // if the user is already registered, we will try login
        if (msg === 'User already registered') {
          try {
            const user = await userStore.login(email.value, password.value);
            userStore.setUser(user);
          } catch (e) {
            message.value = (e as Error).message || Object.toString.call(e);
          }
        } else {
          message.value = msg;
        }
      }
    }
    

    总结

    至此,在浏览器扩展里集成 Google SSO,实现一键登录的功能就完成了。这个过程本身不复杂,但是有几个坑,还有不少易混淆的地方,我也花了不少时间在上面。希望我的这篇分享对大家有帮助。如果大家对浏览器扩展开发、Serverless 数据库使用、Vue 开发有什么问题,欢迎留言评论讨论。


    感谢几位赞助商支持我创作技术分享,也请大家多多使用我的分享链接注册使用他们的服务。在顶部导航的“各种代理”里就能找到链接。谢谢大家。

  • 【教程】浏览器扩展中实现一键登录 Google(1)

    【教程】浏览器扩展中实现一键登录 Google(1)

    本文分享我最近开发 AutomateGPT 扩展时集成 Google SSO 的经验。除了 Google 外,我还用到 Supabase 提供的用户管理与登录功能。

    本来想一篇博客搞定,没想到写着写着就超长了,那就拆成两篇吧,哈哈。

    内容简介

    开发浏览器扩展的时候,我们有时候需要建立用户体系,以便更好的服务用户。此时我们有多个选择:

    1. 让用户使用用户名密码登录,提供完整的注册、登录、验证邮箱、忘记密码等
    2. 让用户使用 SSO 登录

    很明显,第二种更好,因为我们不需要建立用户注册、校验等一系列功能,只要用第三方提供的用户身份标记就好。这样一来可以减少我们的开发成本,二来可以利用现有的互联网基建,对用户来说也更省事。

    作为面向 ChatGPT 用户的辅助应用,AutomateGPT 选择用户体系时,我很自然的选择了 Google。

    吐槽

    Google 是个让人又爱又恨的公司,一方面他们提供大量免费的互联网基建,给我们开发带来巨大帮助;另一方面,由于摊子铺的太大,各种年久失修或者考虑不周,导致我们经常要自己踩坑自己摸索才能最终完成想要的作品。

    这次也是。Google 提供的浏览器扩展开发文档包含一些错误和遗漏;Google Cloud 又是完全的黑盒,导致我折腾了将近两天才把这项功能做好。

    行吧,闲言少叙,下面开始正文。

    在 Chrome Web Store 里占个坑

    因为 Google SSO 要跟 Extension ID 绑定在一起,所以我们要先去 Chrome Web Store 里占个坑,保证以后扩展无论安装在哪里,都是统一的 ID。

    创建浏览器扩展项目

    我先假定大家都有一些扩展开发经验,可以自行创建浏览器扩展。如果你需要从零学起,我刚好做过一期相关视频,可以方便你快速上手:

    肉山小教程-浏览器扩展开发-快速入门_哔哩哔哩_bilibili

    创建完毕,在本地调试没问题之后,就打包,上传 Chrome Web Store。此时我们不需要添加太多功能,只要能加载,能看到效果就行,并不是最终发布,所以不用太担心。

    CWS 里创建应用

    假定各位读者已经拥有 Google 账号,那么就请进入开发者信息中心(Developer Dashboard)。

    接着点击“上传新内容“,上传刚才准备好的压缩包,稍等片刻,CWS 会帮我们创建一个应用,此时就能看到应用 ID 了,大约如图所示:

    具体的 ID 不重要,点击图中红框的 View public key,将 key 复制下来,粘贴到插件的 manifest.json 里,记得要把换行删掉:

    {
    "key": "刚才复制的 key,不能有换行符"
    }

    但是 manifest.json 加了 key 之后,为生产环境构建发版时可能会出问题(因为本地没有私钥,无法信任公钥)。此时,如果你用了我以前推荐的 CRXJS Vite Plugin,就很简单,判断一下当前环境即可:

    export default defineManifest(async function (env) {
    return {
    ...process.env.NODE_ENV === 'development' && {
    key: '刚才复制的 key,不能有换行符',
    },
    };
    });

    所以,推荐使用 Vite + CRXJS Vite Plugin 构建浏览器扩展开发环境。如果你还不会使用这个插件,建议看我另一个视频:

    2024 浏览器扩展开发必备:CRXJS Vite Plugin_哔哩哔哩_bilibili

    为了验证效果,可以在浏览器里删除之前导入的测试扩展,然后重新加载改好 key 的版本。如果之后看到的 ID 与你在 CWS 开发者中心看到的一致,那就说明操作成功。

    在 Google Cloud 里完成配置

    接下来,我们需要在 Google Cloud 里完成配置。如果你已经有项目,那么沿用之前的项目也没问题。如果没有的话,那就新建一个。目前 Google Cloud 还是免费的,新用户更是有 $300 的试用额度,可以放心体验。

    同样,我假定各位读者已经拥有 Google 账号,那么打开 Google Cloud Console,并按照向导指引创建即可。

    创建完成,点击左上角的菜单按钮,从里面选择“API和服务”,然后,选择“OAuth 同意屏幕”,跟随引导创建 OAuth 同意屏幕。注意,因为新应用默认采用白名单,为了之后普通用户能够正常注册登录,最好把应用设置为“已发布”。

    然后,进入“凭据”页面,添加登录凭证。点上面的“创建凭据“,然后选”OAuth 客户端 ID”。注意,虽然 Google 给我们准备了”Chrome 扩展程序“这个选项,但是不能选,要选择”Web 应用“。

    然后在”已获授权的重定向 URI”里填入 https://{刚才获得的 Extension ID}.chromiumapp.org,创建凭证。然后你会得到一个形如 xxxx-ooooo.apps.googleusercontent.com 的客户端 ID。

    在 Supabase 里注册应用

    用户登录免不了用到一些通用功能,比如用户管理、用户信息存储等等。开发这些功能免不了需要很多工作量,我觉得自己做并不合算。所以我建议大家选用一些第三方服务。

    我目前比较喜欢使用 Supabase,大概有几点优势:

    1. 免费 500MB 数据库和若干存储、带宽
    2. 很好的整合了用户授权体系,支持常见平台的 SSO
    3. 各种常见框架都有 SDK,包括我最常用的 Nuxt
    4. 行级权限管理,方便作为 Serverless 数据库使用
    5. PostgreSQL,插件生态丰富,接入 pgvector 就能作为矢量数据库

    简而言之,下限有保证,上限有空间,非常推荐。使用 Supabase 集成 Google SSO 非常简单。另外,因为 Supabase 的界面比较友好,不像 Google Cloud 那么反人类,我就不截图了,相信大家很容易找到。

    1. 注册账号
    2. 创建项目
    3. Authentication > Providers
    4. Enable Google,然后将上一步获得的客户端 ID 填入 “Authorized Client IDs (for Android, One Tap, and Chrome extensions) ”里面即可

    小结

    本文主要介绍了开发环境的配置。下篇博客会讲解如何写代码,并解释整个流程的原理,方便大家适配其它 SSO provider。

    如果大家对浏览器扩展开发有什么问题和想法,欢迎留言提问、讨论。


    AutomateGPT 是我们最近开发的 ChatGPT 增强扩展,方便大家更好的使用 ChatGPT,充分利用包月的价值。它能够帮你重复执行多个 prompt;也可以分解大文件,拆成块依次处理(开发中)。可以用在大文本翻译、批量生成内容、网站分析等领域。欢迎大家使用,反馈问题和意见,谢谢。

  • Bug求助+悬赏

    Bug求助+悬赏

    哎,老革命遇到新问题,创业+远程工作之后,想拉几个人跟我一起排查 bug 也找不到,只好公开求助+悬赏。

    首先请大家看下面这段视频:

    故障描述

    简单来说,我们做了一个网站:Dailylift.io。在这个网站上,你能描述自己今天的状态,然后得到一些心灵慰藉:你信仰的目标会给你写一封信。

    这个功能并不复杂,我选择用 Nuxt3 + Supabase 作为技术栈。我的理由是:

    1. 因为我们需要 SEO,我希望利用 Nuxt3 SSR 功能提升 SEO 效果。
    2. Supabase 提供包括开箱即用的用户系统在内的一系列 Serverless 功能,可以帮助我们的开发提速。
    3. Supabase 提供 Nuxt3 SDK,比 Auth0 更方便。
    4. Supabase 提供 VectorPG,方便我们使用 Embedding

    但是实际运行起来之后,却发现一些我未曾预料到的问题,主要跟用户登录状态有关。具体复现步骤为:

    1. 用户未登录时,先填写感受和目标,然后点“收信”,我们会要求他登录
    2. 用户登录后,我们需要先检查他的收信记录,确保每天只有收一封信
    3. 接下来,如果他今天还没有收信,我们就继续之前的过程,给他写信
    4. 但是偶尔,前面都正常,登录之后就卡住了,无法进入发信流程

    我尝试了很多方案,想解决这个问题,每次我都觉得自己搞定了,我甚至还写了一篇博客作为分享:使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录。但是之后老板就会再次遇到这个问题,然后把我一通数落。我怎么也找不到稳定重现的方式,只能反复查验代码,希望找到其中的漏洞。

    我有几个怀疑:

    1. Nuxt + Supabase,理论上存在两个环境有用户态:SSR与浏览器。我不确定这里会不会有问题。比如,为了防止 refresh token 的时候触发 onAuthStateChange,我用 useSupabaseUser 获取初始用户状态。会不会是它导致后面判断出错了(很小概率)?
    2. 我需要先检查用户的邮件历史,再决定是否请求信件。这里可能也会有问题,比如两次请求之间的先后顺序。
    3. Supabase SSO 只支持 redirect,必须跳走再回来,所以浏览器里的状态无法保存。从第三方回来,处理 access_token 期间,可能因为网络状态、本地内存状态等,进入到某个我没处理的分支,导致失败。
    4. 登录之后,onAuthStateChange 到用户登录彻底完成之前,中间其实有一小段时间,会不会是网络问题导致它被卡住了?

    总之吧,我现在一个人开发,面对这些问题有些焦头烂额,所以希望所有看到这篇博客的同学,能帮我测试一下网站:Dailylift.io,帮我想想其中可能存在的坑,如果您有处理类似问题的经验,也请多分享一些。无论您:

    1. 帮我找到稳定重现的方式
    2. 帮我固定住 bug 出现的现场
    3. 帮我找到代码中潜藏的问题
    4. 引导我找到正确的方向
    5. 分享有价值的经验

    我一定大红包伺候!感谢大家,期待帮忙。


    更新:

    2023-08-31

    今天我早上测试的时候,遇到一个很诡异的问题:我通过 Google 登录后,遇到一个加载信件列表 400 错误,导致获取新信件失败。

    看路径和参数,这个请求应该是正常的。最诡异的是,我在 Devtools 里看不到这个错误,只能看到两个 GET /letters 的请求,都是正常 200。

    我现在怀疑这个问题是不是这样的:

    1. SSR 阶段它也会发起一个请求
    2. 这个请求在某个情况下会失败
    3. 这个失败会导致渲染出的页面携带着失败的状态,继而无法继续。

    另外,因为我们使用 TiDB Serverless,会不会是冷启动时会产生长延迟,导致 Vercel Serverless 失败?不过我看了数据库,也有很多是几个小时后成功的,也不太像。

  • 使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

    使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

    嗯,是的,我回来填坑了。既然要打广告,就要对得起各位金主,总是水文是不行的,还是要输出干货。

    广告继续。本博客即日起开始常年招商,欢迎各位想推广产品的老板投广告,目前定价 4800/年,亦可增加评测文章、教学文章配合,详情可与我联系。具体详情更新在 本站广告 2023 年招商


    Supabase 用户体系入门

    Supabase 提供 Serverless 数据库,自带用户体系,开发用户系统非常简单。而且 SDK 很完善,适配各种框架,大部分功能开箱即用;官方教程方面做得也很好。所以,首先请大家先认真阅读这两篇官方文档、教程:

    第二篇教程文章详细的介绍了使用 Supabae 开发用户管理系统的过程,配合上面的文档,相信大家都可以轻松入门。唯一的难点(对部分同学来说)可能是英文,我就先不复述。

    接下来的文章主要介绍官方没有细说的最佳实践和各种实用细节。

    邮件登录与邮件模版

    虽然官方提供了很多方案,但是我想,对于大部分同学来说,邮件+密码登录才是最常用的,最多再加上第三方比如 Gmail 登录。

    Supabase 提供了发邮件的功能,方便用户校验邮箱、找回密码。但是,正如很多邮件服务器那样,它的到达率并不理想。我猜测有些用户即使没有滥用 Supabase 的邮件服务,也会因为邮件模版过于简单而被误杀。所以大家第一步应该先配置邮件模版,尽量多体现自己的产品相关信息,尽量跟大家的通用模版区分开,减少误报误杀的可能。

    Supabase 默认要求用户校验邮箱之后才能登录,如果你暂时不关心这一点,可以考虑在 Auth ProviderS > Email关掉这个功能。此后,用户完成注册后就可以登录,不过他们的用户状态在数据里仍然是未验证,我们也可以借此对用户进行一些差异化的控制。

    注册与登录,自动注册

    有一点我不太喜欢。Supabase 提供两个方法:signUpsignInWithPassword,分别用来注册和登录。但是通常情况下,我并不希望区分这两个方法。我认为我们的网站对用户来说,只是一个小工具,我觉得他们不会在意是否在这里注册过,他们只希望做他们要做的事情,注册登录只是我们为了方便自己而做的步骤。

    所以我一般仍然会通过不同的路径区分用户是想注册,还是想登录。但是我会在用户 signUp 后检查错误信息,如果是重复注册,就自动帮他们登录。

    const { data: { user }, error } = await supabase.auth[method]({
      email,
      password,
      options,
    });
    if (error) {
      // registered, login
      if (error.message === 'User already registered') {
        return login(email, password);
      }
    throw error;
    }

    Nuxt3+Pinia 实现用户身份校验及额度查询

    教程中的例子比较简单,通常来说我们的产品会复杂很多,比如,我们可能需要全局状态管理工具,方便不同页面不同组件中使用。在 Nuxt3 中使用 Pinia 需要用到特有的 Module:@pinia/nuxt,简单安装官方教程配置一下即可。这里主要分享我摸索的其它未提及要点。

    额度查询

    Supabase 在创建项目的时候已经帮我们建好了用户表,并且放在默认不对外的库里面。所以我们最好不要直接修改表,而是如教程所讲,创建一个新的表,把用户账户相关的其它数据放进去,比如在这样一款典型的 ChatGPT 产品中,就是帐户额度,然后通过关联 id 的方式访问。

    这就带来一个问题:什么时候去读这个表?

    对 SPA 来说,这个问题比较简单,登录之后,以及网页初始化的时候读取即可,反正所有请求都由我们全权控制。但是对于 Nuxt3 来说就值得讨论一下。因为 Nuxt3 要进行服务器端渲染,所以我们通常希望它在渲染页面的时候,这个数据已经就位了。

    这里我推荐配合 Pinia Store,使用 supabase.auth.onAuthStateChange 函数。我会建立一个 userStore,在里面完成用户注册、登录、登出等动作,同时注册 onAuthStateChange 事件,完成用户状态变更后的处理。

    const useUserStore = defineStore('user', () => {
      const supabase = useSupabaseClient();
      const user = useSupabaseUser();
    
      async function login(email: string, password: string, isSignUp = false): Promise<void> {}
      async function loginWithGoogle(): Promise<void> {}
      async function logout(): Promise<void> {}
      
      // 注册事件处理函数
      supabase.auth.onAuthStateChange((event, session) => {
        switch (event) {
          case 'INITIAL_SESSION':
          case 'SIGNED_IN':
          // 在这里处理登录后和初始化时拿到用户身份后的操作,比如加载用户账户的额度
          // 这里需要注意:此时 `user.value` 尚未被填充,所以我们不能依赖它。有两个方案:1. 延迟执行 `setTimeout`;2. 直接从 `session` 参数里拿到用户信息然后处理
          // 我的做法是,如果需要请求 API,那就选择方案2;否则,使用方案1,等 100ms
        }
      });
    });
    export useUserStore;

    请仔细看上方代码的注释,非常重要,看起来很简单,但其实是血泪铺就的道路。

    判断第三方登录,及 UI 呈现

    当用户选择第三方登录,比如 Gmail 的时候,Supabase 目前只支持跳转这一种方式,不如 Auth0,可以用 popup,省很多事。

    完整的第三方登录流程是:

    1. 跳转到第三方登录页面
    2. 登录
    3. 跳转到 Supabase 中间页
    4. 带着 access_token 跳回我们的网站,Supabase SDK 负责读取 access_token,验证用户身份等工作

    其中涉及到两个(或更多)网站的配置,因为文档中写的有,我就不再详述,需要的同学留言我再补充。

    最麻烦的是(4),跳转回来之后,从页面初始化成功到完成登录,中间可能会隔很久,短则1、2秒,长则10s 都有可能。这期间用户可以继续操作,会产生各种误会和误操作。这里我建议:

    1. 登录时传递 redirectTo,要求最终跳转页面是某个登录落地页
    2. 初始化的时候,检查 URL 里的 #access_token=
    3. 如果有,说明是第三方登录返回,然后就显示登录中的状态
    4. 等待 user.value 变化,说明完成登录,跳转到原先应该去的页面,或者继续之前没做完的操作
    5. 如果没有 access_token,直接跳转到其他落地页。

    总结

    上面这些内容看起来不复杂,但实际上对用户体验影响很大,并且文档、教程说得也不清楚,导致我的老板对产品一直不太满意。希望这篇文章可以节省大家的宝贵时间。

    如果有关于 Supabase、Vercel、Nuxt3 的问题,欢迎评论里面留言。

    系列文章

    1. 使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:1. 系统篇
    2. 【本文】使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

    建议阅读

  • 使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:1. 系统篇

    使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:1. 系统篇

    过去两周,我基本都在跟这套付费/计费系统死磕,相对来说投入到 AI 学习里的时间不多,以至于 我的 AI 学习周记 系列本周停更。如今终于基本搞定这套系统,基本概念、开发调试都不存在难以逾越的问题,所以打算写几篇博客总结一下,给后来者分享我收获的知识、踩到的坑。也让自己需要的时候可以翻查笔记。

    OpenAI 计费系统现状

    目前我们讨论的 OpenAI 计费,基本是针对 ChatGPT 对应的 gpt-3.5-turbo 和 gpt-4 这两个模型,在 API 调用方面的费用。官方规定,gpt-3.5-turbo 是 $0.002/1000 tokens,gpt-4 根据容量不同,最多要贵 30 倍,所以提供服务时,我们基本还是以 3.5 为主。如果用户愿意多付钱,我们也可以提供 gpt-4(已经拥有权限)。

    不使用 stream: true 的时候,OpenAI 会返回使用的 token 数量,这个时候,数据准确可靠。但是为用户体验考虑,也为云服务考虑,使用 stream: true 模式明显更好。但是在这种模式下,OpenAI 不返回使用的 token 数量。(我猜测这跟 Transformer 模型的工作原理有关。)所以我们就需要手动统计 token 的消耗。

    这个时候得到的结果可能并不准确,但是没办法,我们只能这么做。

    我们开始也不知道 OpenAI 怎么统计 token 数,好在官方提供了计算页面:https://platform.openai.com/tokenizer 和推荐方案(看起来是传说中的 GPT 地牢作者的作品),可以帮我们完成计算。至于详情,将来会具体介绍。

    基于 Edge Function 的计费系统设计

    首先,我们要选择服务器架构。

    传统方案是配置一台云服务器,然后前面架一个 CDN。但由于我们是一个面向全球的服务,这样的架构不甚理想。而且基于 stream: true 的方案需要长时间维持网络连接,单机容量也不够。全球部署的话,成本太高,初创团队不现实。

    所以很自然的,我们准备使用 Edge Function。Edge Function 的优势在于:

    1. 它借助云服务厂商的边缘节点提供服务,节点分布广泛,可以就近服务用户,大大改进响应时间。
    2. Edge Function 在用户请求的时候才工作,平时休眠。这种按需付费成本更低。
    3. 相比于传统的 Serverless Function,Edge Function 一般都会修改运行时,用减少负载的方式提升启动速度,以几乎 0 延迟的方式启动,用户体验很好。

    以上几点我均已在之前的试做项目中在 Vercel 平台上体验过,所以这次没怎么纠结,直接选择 Vercel Edge Function 开发。

    也欢迎大家阅读我前一篇分享文:使用 Vercel Edge Function 访问 OpenAI API 的注意事项

    数据库选择:Supabase

    确定使用 Edge Function 之后,下一步要选择数据库。传统的、基于数据库协议的方式不可行,必须支持 http 请求,必须支持连接池,可选方案不太多。刚好前阵子我为了抽键盘,了解到有一家叫 Supabase 的新 serverless 服务商,可以很好满足我们的需求。

    首先,他们能满足 Vercel Edge Function 的需要,也是官方推荐的厂家之一。

    其次,他们的服务基于 PosgreSQL,具备丰富的插件生态,可以实现各种功能,包括未来给 ChatGPT 提供内容拓展的 pg_vector,可以不用担心将来需求延伸。

    再次,作为一家 serverless 服务商,他们家的免费额度看起来还不错,应该可以满足我们早期验证产品的目标(薅羊毛就是爽)。而且,自带 RLS(行级安全策略)也会给未来的开发带来很多方便,比如我们可以放心的把用户身份有关的读取操作放在客户端,不用单独开发接口,节省很多人力。

    最后,我们决定选用 Supabase,作为数据库服务供应商。

    其实 Supabase 也提供 Edge Function,效果理论上并不比 Vercel 差,但它是基于 Deno 的封装,生态和环境都跟 Node.js 不太一样。考虑到学习成本,以及分散投入有利于多嫖资源,所以我暂时不打算用,还是先集中在他们家的数据库上。

    收费选择:Stripe

    作为超迷你初创团队,主攻海外英文市场,我们的策略自然是一切从简,那么付费方案很自然就选择了 Stripe。Stripe 的功能全,文档强大,很适合我们这种小团队使用:

    1. 接受多种收费方式,各种信用卡不一而足,尽可能满足用户
    2. 提供订阅式购买
    3. 支持优惠码等促销手段
    4. 提供 SaaS 服务,方便管理用户
    5. 可以用 Payment link 创建付费页面,省去几乎所有购买流程的开发成本
    6. 提供 webhook,对接我们的账户体系
    7. 提供大量 API,如果有需要,将来我们可以逐步迁移到自己的平台

    最终选择

    基本上,我们最终形成了这样一套方案:

    1. Vercel Edge Function 提供 OpenAI API 的封装,我们借由它完成用户请求、额度拦截、计费等功能
    2. Supabase 提供存储,我们借由它存储用户的账户状态、付费记录、消费记录等功能
    3. Stripe 提供购买功能,我们用它的 Payment link 让用户完成购买付费

    这些产品都支持 TS/JS 为主的语言,提供基于 npm 的 SDK,开发环境很大众,开发体验不错。未来属于 JS。

    这套架构在初期没什么成本,可以覆盖几乎全球的用户,提供不俗的性能。将来用户量增长,需要扩容的时候,也不需要我们再手动开发扩容功能,只要根据用户量、使用量付费给云服务商就好。如果将来我们要提供 embedding,嵌入用户自己的数据,也可以很容易的在现有框架下实现扩展。

    乐观估计,未来半年到一年内,我们都可以在这个体系下开发。


    小结

    我感觉全世界就我一个人在做计费系统,其他人:

    1. 要么是不知道是弄了很多免费账号还是自己负担费用先抢用户,反正是敞开给用户免费用
    2. 要么是收一大笔钱割韭菜,反正收的钱多,普通用户随便用也用不完

    总之都不在意区分用户,也不在意回本。于是几乎看不到有人讨论如何做自己的付费系统。

    我们认为成熟的商品,还是要在成本、收益、风险上形成稳定、友好的比例,计费系统早晚都得做,不如先做好。

    如果你对 OpenAI 计费系统,对我们这套基于公共云平台的 Edge Function 方案感兴趣或者有问题,欢迎留言讨论。