You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Firebase第二代可调用函数中调用其他可调用函数并完善代码

在Firebase第二代可调用函数中调用另一个可调用函数的两种方案

方案一:直接复用核心逻辑(推荐)

这种方式无需跨函数网络请求,性能更高且代码易维护。我们可以把getMetadata的核心功能抽离为独立函数,让两个可调用函数共享该逻辑。

步骤1:重构getMetadata.ts,抽离核心函数

import * as functions from "firebase-functions";
import metascraper from "metascraper";
import metascraperTitle from "metascraper-title";
import metascraperDescription from "metascraper-description";

// 抽离独立的元数据获取函数,供其他模块复用
export async function getUrlMetadata(url: string) {
  try {
    const res = await fetch(url);
    const html = await res.text();
    const metadata = await metascraper([
      metascraperTitle(),
      metascraperDescription(),
    ])({ html, url });
    return {
      title: metadata.title,
      description: metadata.description,
    };
  } catch (error) {
    functions.logger.error("获取网页元数据失败:", error);
    throw new functions.https.HttpsError("internal", "无法获取网页元数据");
  }
}

// 原可调用函数,调用抽离的核心函数
export const getMetadata = functions.https.onCall(async (data, context) => {
  // 验证输入有效性
  if (!data.url || typeof data.url !== "string") {
    throw new functions.https.HttpsError("invalid-argument", "必须提供有效的URL");
  }
  
  return getUrlMetadata(data.url);
});

步骤2:完善saveBookmark.ts,调用核心函数

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
// 导入抽离的核心函数
import { getUrlMetadata } from "./getMetadata";

// 初始化Firebase Admin(全局仅需初始化一次)
if (!admin.apps.length) {
  admin.initializeApp();
}

export const saveBookmark = functions.https.onCall(async (data, context) => {
  // 1. 验证用户身份(如需登录才能保存书签)
  if (!context.auth) {
    throw new functions.https.HttpsError("unauthenticated", "用户未登录,无法保存书签");
  }

  // 2. 验证输入参数
  if (!data.url || typeof data.url !== "string") {
    throw new functions.https.HttpsError("invalid-argument", "必须提供有效的URL");
  }

  try {
    // 3. 获取网页元数据
    const metadata = await getUrlMetadata(data.url);

    if (!metadata.title) {
      throw new functions.https.HttpsError("internal", "无法提取网页标题");
    }

    // 4. 将书签保存到Firestore
    const bookmarkRef = await admin.firestore().collection("bookmarks").add({
      url: data.url,
      title: metadata.title,
      description: metadata.description || "", // 兼容描述为空的情况
      createdAt: admin.firestore.FieldValue.serverTimestamp(), // 使用Firestore服务器时间更可靠
      userId: context.auth.uid, // 关联用户ID,方便后续查询管理
    });

    return {
      success: true,
      bookmarkId: bookmarkRef.id,
      metadata,
    };
  } catch (error) {
    functions.logger.error("保存书签失败:", error);
    // 传递原始HTTPS错误,否则统一抛出内部错误
    if (error instanceof functions.https.HttpsError) {
      throw error;
    }
    throw new functions.https.HttpsError("internal", "保存书签失败,请稍后重试");
  }
});

方案二:通过HTTP调用可调用函数端点

如果两个函数不在同一项目,或必须通过可调用函数的公开接口调用,可使用此方式,需处理认证和请求格式。

完善后的saveBookmark.ts代码

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import { GoogleAuth } from "google-auth-library";

// 初始化Firebase Admin
if (!admin.apps.length) {
  admin.initializeApp();
}

const auth = new GoogleAuth();

export const saveBookmark = functions.https.onCall(async (data, context) => {
  // 验证用户身份
  if (!context.auth) {
    throw new functions.https.HttpsError("unauthenticated", "用户未登录,无法保存书签");
  }

  // 验证输入参数
  if (!data.url || typeof data.url !== "string") {
    throw new functions.https.HttpsError("invalid-argument", "必须提供有效的URL");
  }

  try {
    // 构造目标可调用函数的URL(推荐通过环境变量配置,避免硬编码)
    const functionRegion = process.env.FUNCTION_REGION || "us-central1";
    const projectId = process.env.GCP_PROJECT;
    const getMetadataUrl = `https://${functionRegion}-${projectId}.cloudfunctions.net/getMetadata`;

    // 获取服务账号ID Token,用于认证调用可调用函数
    const client = await auth.getIdTokenClient(getMetadataUrl);
    const idToken = await client.idTokenProvider.fetchIdToken(getMetadataUrl);

    // 发送请求到可调用函数
    const response = await fetch(getMetadataUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${idToken}`,
      },
      body: JSON.stringify({
        data: { url: data.url },
      }),
    });

    // 处理响应错误
    if (!response.ok) {
      const errorData = await response.json();
      throw new functions.https.HttpsError(errorData.error.status, errorData.error.message);
    }

    const result = await response.json();
    const metadata = result.data;

    if (!metadata.title) {
      throw new functions.https.HttpsError("internal", "无法提取网页标题");
    }

    // 保存书签到Firestore
    const bookmarkRef = await admin.firestore().collection("bookmarks").add({
      url: data.url,
      title: metadata.title,
      description: metadata.description || "",
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
      userId: context.auth.uid,
    });

    return {
      success: true,
      bookmarkId: bookmarkRef.id,
      metadata,
    };
  } catch (error) {
    functions.logger.error("保存书签失败:", error);
    if (error instanceof functions.https.HttpsError) {
      throw error;
    }
    throw new functions.https.HttpsError("internal", "保存书签失败,请稍后重试");
  }
});

注意事项

  • 需要安装依赖:npm install google-auth-library
  • 确保函数的服务账号拥有调用目标可调用函数的权限
  • 推荐通过环境变量配置函数区域和项目ID,避免硬编码

内容的提问来源于stack exchange,提问作者Julien Reszka

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:17:03