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 → 流式响应 → SwiftUIDemo演示
核心功能特性
智能收据分析
- 图片上传:支持从相册或相机选择收据图片
- 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/.temp1. 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获取配置信息的方法:
-
OPENAI_API_KEY:
- 当前代码使用阿里云通义千问API
- 访问 阿里云灵积平台
- 注册账号并创建API Key
- 也可以替换为标准的 OpenAI API Key
-
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: 主键,自动生成的UUIDuser_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