EasyApp

AI 功能

学习如何在 EasyAppSwiftUI 中使用 Supabase Edge Function 调用 AI 接口

AI 功能集成指南

本文档将详细介绍如何在 EasyAppSwiftUI 中使用 Supabase Edge Function 调用 AI 接口,以收据分析功能为例展示完整的实现流程。

安全提醒

强烈建议:将 API Key 放在服务端而非客户端。移动应用的网络请求很容易被抓包工具拦截,导致 API Key 泄露。

相关资源

想了解更多 Supabase Edge Function 的详细信息?请参考官方文档:

关于更多如何 开发 Supabase Edge Function 的介绍,请参考:

系统架构概述

AI 功能采用以下架构设计:

SwiftUI App → Supabase Storage → Supabase Edge Function → OpenAI API → 流式响应 → SwiftUI

Demo演示

核心功能特性

智能收据分析

  • 图片上传:支持从相册或相机选择收据图片
  • AI 识别:使用 OpenAI 多模态模型提取收据信息
  • 实时流式响应:展示分析过程,提升用户体验
  • 结构化数据:自动解析为标准化的 JSON 格式
  • 数据存储:分析结果保存到 Supabase 数据库

技术实现详解

1. 数据模型设计

首先定义收据数据的结构化模型:

struct ReceiptModel: Identifiable, Codable, Equatable {
    var id = UUID()
    var userID: String?
    let restaurantInfo: RestaurantInfo
    let foodItems: [FoodItem]
    let pricingSummary: PricingSummary
    let paymentInfo: PaymentInfo
    let receiptMetadata: ReceiptMetadata
}

struct RestaurantInfo: Codable {
    let name: String?
    let address: String?
    let phone: String?
    let date: String?
    let time: String?
}

struct FoodItem: Codable, Identifiable {
    var id = UUID()
    let name: String?
    let unitPrice: Double?
    let quantity: Int?
    let totalPrice: Double?
}

2. SwiftUI 前端实现

图片上传功能

func uploadImage(selectedImage: UIImage) async throws -> String {
    isUploading = true
    defer { isUploading = false }
    
    guard let imageData = selectedImage.jpegData(compressionQuality: 0.8) else { 
        return "" 
    }

    let imageName = UUID().uuidString
    let imagePath = "\(imageName).jpeg"

    return try await supabase.request {
        let response = try await supabase.storage
            .from("receipt-analysis")
            .upload(
                imagePath,
                data: imageData,
                options: FileOptions(contentType: "image/jpeg")
            )
        return response.fullPath
    }
}

AI 分析调用

func analyzeReceipt(imagePath: String) async {
    isAnalyzing = true
    analysisContent = ""
    parsedReceipt = nil

    let parameters = ["imgPath": imagePath]

    do {
        try await supabase.callStreamingFunction(
            functionName: "receipt-analysis",
            parameters: parameters,
            onContent: { [weak self] (content: String) in
                guard let self = self else { return }
                self.analysisContent += content
            },
            onCompletion: { [weak self] in
                guard let self = self else { return }
                self.isAnalyzing = false
                self.parseAnalysisContent()
            }
        )
    } catch {
        isAnalyzing = false
        state = .result(.failure(error))
    }
}

函数入口和认证

Deno.serve(async (req) => {
    const userAuth = await getUserAuth(req);
    if (!userAuth) {
        return new Response(JSON.stringify({ error: "Unauthorized" }), {
            status: 401,
        });
    }
    
    return responseAIModelStream(req);
});

async function getUserAuth(req: Request) {
    const supabaseClient = getSupabaseClient(req);
    const authHeader = req.headers.get("Authorization");
    if (!authHeader) return null;
    
    const token = authHeader.replace("Bearer ", "");
    const { data } = await supabaseClient.auth.getUser(token);
    return data?.user || null;
}

AI 模型调用和流式响应

async function responseAIModelStream(req: Request) {
    const { imgPath } = (await req.json()) as ReceiptAnalysisRequest;
    
    // 获取图片的公开URL用于AI分析
    const imageUrl = getImageUrl(req, imgPath);
    console.log("Processing image:", imageUrl);

    const stream = await openAIClient.chat.completions.create({
        model: "qwen-vl-max-latest",
        response_format: { type: "json_object" },
        messages: [
            {
                role: "system",
                content: "You are a professional restaurant receipt analysis expert..."
            },
            {
                role: "user",
                content: [
                    {
                        type: "image_url",
                        image_url: { url: imageUrl }  // 使用动态生成的图片URL
                    },
                    {
                        type: "text",
                        text: `Analyze this receipt and return JSON with fields:
                        - restaurant_info
                        - food_items
                        - pricing_summary
                        - payment_info
                        - receipt_metadata`
                    }
                ]
            }
        ],
        stream: true,
    });

    const readableStream = new ReadableStream({
        start(controller) {
            (async () => {
                try {
                    for await (const chunk of stream) {
                        const content = chunk.choices[0]?.delta?.content || "";
                        if (content) {
                            const encoder = new TextEncoder();
                            controller.enqueue(
                                encoder.encode(`data: ${JSON.stringify({ content })}\n\n`)
                            );
                        }
                    }
                    const encoder = new TextEncoder();
                    controller.enqueue(encoder.encode("data: [DONE]\n\n"));
                } catch (error) {
                    controller.error(error);
                } finally {
                    controller.close();
                }
            })();
        },
    });

    return new Response(readableStream, {
        headers: {
            "Content-Type": "text/event-stream",
            "Cache-Control": "no-cache",
            Connection: "keep-alive",
        },
    });
}

3. 数据解析和存储

JSON 解析

func parseAnalysisContent() {
    guard !analysisContent.isEmpty else {
        state = .result(.failure(ParsingError.emptyAnalysisContent))
        return
    }

    let cleanedContent = extractJSON(from: analysisContent)
    
    guard let jsonData = cleanedContent.data(using: .utf8) else {
        state = .result(.failure(ParsingError.invalidJSON))
        return
    }

    do {
        let decoder = JSONDecoder()
        let receipt = try decoder.decode(ReceiptModel.self, from: jsonData)
        self.parsedReceipt = receipt
    } catch {
        state = .result(.failure(error))
    }
}

private func extractJSON(from content: String) -> String {
    guard let firstBrace = content.firstIndex(of: "{"),
          let lastBrace = content.lastIndex(of: "}") else {
        return content
    }
    return String(content[firstBrace...lastBrace])
}

数据库存储

func saveReceiptAnalysisResult(receipt: ReceiptModel) async {
    isSaving = true
    defer { isSaving = false }

    guard let user = supabase.auth.currentUser else { return }

    let saveReceipt = ReceiptModel(
        userID: user.id.uuidString,
        restaurantInfo: receipt.restaurantInfo,
        foodItems: receipt.foodItems,
        pricingSummary: receipt.pricingSummary,
        paymentInfo: receipt.paymentInfo,
        receiptMetadata: receipt.receiptMetadata
    )

    do {
        try await supabase.request {
            try await supabase.client.from("receipt_analysis_results")
                .insert(saveReceipt)
                .select()
                .single()
                .execute()
        }
    } catch {
        state = .result(.failure(error))
    }
}

配置要求

⚠️ 重要提醒:请确保在项目根目录的 .gitignore 文件中添加以下内容,避免敏感信息泄露:

# 环境变量文件
.env
.env.local
.env.production
.env.*.local

# Supabase
supabase/.branches
supabase/.temp

1. Supabase 配置

环境变量设置

在 Supabase 项目根目录创建 .env 文件,配置以下环境变量:

# .env 文件配置示例

# OpenAI API 配置(此处使用阿里云通义千问API)
OPENAI_API_KEY=your_dashscope_api_key_here

# Supabase 项目配置
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here

获取配置信息的方法

  1. OPENAI_API_KEY

    • 当前代码使用阿里云通义千问API
    • 访问 阿里云灵积平台
    • 注册账号并创建API Key
    • 也可以替换为标准的 OpenAI API Key
  2. SUPABASE_URL

    • 登录 Supabase
    • 选择您的项目
    • 在 Project Settings → Data API 页面找到 Project URL
    • 在 Project Settings → API Keys 页面找到 Service Anon Key

2. 存储桶设置

创建 receipt-analysis 存储桶来存储收据图片,配置适当的访问权限:

-- 创建收据分析存储桶
INSERT INTO storage.buckets (id, name, public) 
VALUES ('receipt-analysis', 'receipt-analysis', true);

-- 设置访问策略:允许认证用户上传
CREATE POLICY "Allow authenticated users to upload receipts" 
ON storage.objects FOR INSERT 
WITH CHECK (bucket_id = 'receipt-analysis' AND auth.role() = 'authenticated');

-- 设置访问策略:允许认证用户查看自己上传的文件
CREATE POLICY "Allow users to view their own receipts" 
ON storage.objects FOR SELECT 
USING (bucket_id = 'receipt-analysis' AND auth.role() = 'authenticated');

-- 设置访问策略:允许用户删除自己的文件
CREATE POLICY "Allow users to delete their own receipts" 
ON storage.objects FOR DELETE 
USING (bucket_id = 'receipt-analysis' AND auth.role() = 'authenticated');

存储桶配置说明

  • public = true:允许通过公开URL访问(用于AI分析)
  • 访问策略确保只有认证用户才能上传、查看和删除文件
  • 图片文件会自动生成UUID文件名,避免命名冲突

3. 数据表结构

完整的收据分析结果表结构:

-- 创建收据分析结果表
CREATE TABLE public.receipt_analysis_results (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id),
    restaurant_info JSONB,
    food_items JSONB,
    pricing_summary JSONB,
    payment_info JSONB,
    receipt_metadata JSONB,
    raw_response JSONB,           -- 存储原始AI响应,用于调试
    img_path TEXT,                -- 图片存储路径
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 启用行级安全策略
ALTER TABLE public.receipt_analysis_results ENABLE ROW LEVEL SECURITY;

-- 创建访问策略:用户只能访问自己的数据
CREATE POLICY "Users can access their own receipt analysis results"
    ON public.receipt_analysis_results
    FOR ALL
    USING (auth.uid() = user_id);

表结构说明

  • id: 主键,自动生成的UUID
  • user_id: 用户ID,关联到认证用户表
  • restaurant_info: 餐厅信息(名称、地址、电话、日期、时间)
  • food_items: 食物项目数组(名称、单价、数量、总价)
  • pricing_summary: 价格汇总(小计、折扣、税费、服务费、总计)
  • payment_info: 支付信息(支付方式、支付金额、找零)
  • receipt_metadata: 收据元数据(收据号、收银员、桌号)
  • raw_response: 原始AI响应数据,便于调试和审计
  • img_path: 收据图片在存储中的路径
  • created_at/updated_at: 记录的创建和更新时间

最佳实践

安全性

  • 环境变量管理
    • .env 文件不要提交到版本控制系统
  • 用户认证:所有 API 调用都需要有效的认证令牌
  • 数据验证:对上传的图片进行格式和大小验证
  • 错误处理:提供友好的错误提示和重试机制
  • 行级安全:确保用户只能访问自己的分析结果

扩展应用

基于此架构,您可以轻松扩展更多 AI 功能:

  • 文档识别:身份证、护照等证件信息提取
  • 文本翻译:多语言实时翻译功能
  • 图片分析:商品识别、场景分析等
  • 语音处理:语音转文字等

这个架构提供了灵活、可扩展的 AI 集成方案,让您可以快速构建各种AI应用功能。

Last updated on