前后端基本架构和完全excel表的解析及统计图表的生成以及excel表的到出
This commit is contained in:
5
frontend/supabase/config.toml
Normal file
5
frontend/supabase/config.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[auth.phone]
|
||||
enable_confirmations = false
|
||||
|
||||
[auth.email]
|
||||
enable_confirmations = false
|
||||
4
frontend/supabase/functions/_shared/cors.ts
Normal file
4
frontend/supabase/functions/_shared/cors.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
61
frontend/supabase/functions/chat-assistant/index.ts
Normal file
61
frontend/supabase/functions/chat-assistant/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { messages, userId } = await req.json();
|
||||
if (!messages || !userId) throw new Error('Missing messages or userId');
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
const miniMaxApiKey = Deno.env.get('INTEGRATIONS_API_KEY');
|
||||
const miniMaxResponse = await fetch(
|
||||
'https://app-a6ww9j3ja3nl-api-Aa2PqMJnJGwL-gateway.appmiaoda.com/v1/text/chatcompletion_v2',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Gateway-Authorization': `Bearer ${miniMaxApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'MiniMax-M2.5',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `你是一个专业的文档智能交互助手。你的目标是协助用户完成文档管理、信息提取和表格填写。
|
||||
你所在的系统支持:
|
||||
1. 列出当前用户的文档列表。
|
||||
2. 查看某个文档提取到的关键实体。
|
||||
3. 协助用户通过自然语言发起文档编辑、排版等指令(当前模拟执行)。
|
||||
请以专业且友好的语气回复。`
|
||||
},
|
||||
...messages
|
||||
]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const miniMaxData = await miniMaxResponse.json();
|
||||
if (!miniMaxResponse.ok) throw new Error('MiniMax chat failed');
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
choices: miniMaxData.choices
|
||||
}), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in chat assistant:', error);
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
});
|
||||
94
frontend/supabase/functions/fill-template/index.ts
Normal file
94
frontend/supabase/functions/fill-template/index.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { taskId } = await req.json();
|
||||
if (!taskId) throw new Error('Missing taskId');
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// Get task and documents
|
||||
const { data: task, error: taskError } = await supabase
|
||||
.from('fill_tasks')
|
||||
.select('*, templates(*)')
|
||||
.eq('id', taskId)
|
||||
.single();
|
||||
|
||||
if (taskError || !task) throw new Error('Task not found');
|
||||
|
||||
const { data: entities, error: entitiesError } = await supabase
|
||||
.from('extracted_entities')
|
||||
.select('*')
|
||||
.in('document_id', task.document_ids);
|
||||
|
||||
if (entitiesError) throw new Error('Failed to fetch entities');
|
||||
|
||||
// Aggregate entities for context
|
||||
const context = entities.map(e => `${e.entity_type}: ${e.entity_value}`).join('\n');
|
||||
|
||||
// Get template content (assume docx/xlsx text extraction for template)
|
||||
// Actually, MiniMax can help generate the final filled content if we provide the template structure
|
||||
const miniMaxApiKey = Deno.env.get('INTEGRATIONS_API_KEY');
|
||||
const miniMaxResponse = await fetch(
|
||||
'https://app-a6ww9j3ja3nl-api-Aa2PqMJnJGwL-gateway.appmiaoda.com/v1/text/chatcompletion_v2',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Gateway-Authorization': `Bearer ${miniMaxApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'MiniMax-M2.5',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个专业的文档处理助手。请根据提供的源数据实体,自动填充到给定的表格模板中。生成的文档应当格式严谨,满足业务应用需求。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `源数据信息如下:\n\n${context}\n\n模板名称:${task.templates.name}\n模板类型:${task.templates.type}\n\n请生成填充后的文档内容预览(以 Markdown 格式呈现,确保包含所有源数据中的关键指标)。`
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const miniMaxData = await miniMaxResponse.json();
|
||||
if (!miniMaxResponse.ok) throw new Error('MiniMax filling failed');
|
||||
|
||||
const resultText = miniMaxData.choices[0].message.content;
|
||||
|
||||
// Update task
|
||||
const { error: updateError } = await supabase
|
||||
.from('fill_tasks')
|
||||
.update({
|
||||
status: 'completed',
|
||||
result_path: `results/${taskId}.md` // Save as md for simplicity in this demo
|
||||
})
|
||||
.eq('id', taskId);
|
||||
|
||||
// Store result in storage
|
||||
await supabase.storage
|
||||
.from('document_storage')
|
||||
.upload(`results/${taskId}.md`, new Blob([resultText], { type: 'text/markdown' }));
|
||||
|
||||
return new Response(JSON.stringify({ success: true, result: resultText }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error filling template:', error);
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
});
|
||||
146
frontend/supabase/functions/process-document/index.ts
Normal file
146
frontend/supabase/functions/process-document/index.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import * as XLSX from 'https://esm.sh/xlsx@0.18.5';
|
||||
import * as mammoth from 'https://esm.sh/mammoth@1.5.1';
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { documentId } = await req.json();
|
||||
if (!documentId) throw new Error('Missing documentId');
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
const supabase = createClient(supabaseUrl, supabaseKey);
|
||||
|
||||
// Get document details
|
||||
const { data: document, error: docError } = await supabase
|
||||
.from('documents')
|
||||
.select('*')
|
||||
.eq('id', documentId)
|
||||
.single();
|
||||
|
||||
if (docError || !document) throw new Error('Document not found');
|
||||
|
||||
// Download file
|
||||
const { data: fileData, error: dlError } = await supabase.storage
|
||||
.from('document_storage')
|
||||
.download(document.storage_path);
|
||||
|
||||
if (dlError || !fileData) throw new Error('Failed to download file');
|
||||
|
||||
const arrayBuffer = await fileData.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
let extractedText = '';
|
||||
|
||||
// Parse file based on type
|
||||
const fileExt = document.type.toLowerCase();
|
||||
if (fileExt === 'txt' || fileExt === 'md') {
|
||||
extractedText = new TextDecoder().decode(uint8Array);
|
||||
} else if (fileExt === 'docx') {
|
||||
const result = await mammoth.extractRawText({ arrayBuffer });
|
||||
extractedText = result.value;
|
||||
} else if (fileExt === 'xlsx' || fileExt === 'xls') {
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||
extractedText = workbook.SheetNames.map(name => {
|
||||
const sheet = workbook.Sheets[name];
|
||||
return XLSX.utils.sheet_to_txt(sheet);
|
||||
}).join('\n\n');
|
||||
} else {
|
||||
throw new Error(`Unsupported file type: ${fileExt}`);
|
||||
}
|
||||
|
||||
if (!extractedText.trim()) throw new Error('Document is empty');
|
||||
|
||||
// Call MiniMax for entity extraction
|
||||
const miniMaxApiKey = Deno.env.get('INTEGRATIONS_API_KEY');
|
||||
const miniMaxResponse = await fetch(
|
||||
'https://app-a6ww9j3ja3nl-api-Aa2PqMJnJGwL-gateway.appmiaoda.com/v1/text/chatcompletion_v2',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Gateway-Authorization': `Bearer ${miniMaxApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'MiniMax-M2.5',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个文档信息提取专家。请从提供的文档内容中提取关键实体信息(如姓名、日期、金额、项目名称、地址、关键指标等)。输出格式必须为 JSON 数组,包含 entity_type, entity_value, confidence 三个字段。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `文档内容如下:\n\n${extractedText.slice(0, 15000)}` // Limit text for token budget
|
||||
}
|
||||
],
|
||||
response_format: {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: 'extracted_entities',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
entities: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
entity_type: { type: 'string' },
|
||||
entity_value: { type: 'string' },
|
||||
confidence: { type: 'number' }
|
||||
},
|
||||
required: ['entity_type', 'entity_value']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const miniMaxData = await miniMaxResponse.json();
|
||||
if (!miniMaxResponse.ok) {
|
||||
console.error('MiniMax Error:', miniMaxData);
|
||||
throw new Error('MiniMax extraction failed');
|
||||
}
|
||||
|
||||
const extractionResult = JSON.parse(miniMaxData.choices[0].message.content);
|
||||
const entities = extractionResult.entities || [];
|
||||
|
||||
// Save entities
|
||||
if (entities.length > 0) {
|
||||
const entitiesToInsert = entities.map((e: any) => ({
|
||||
document_id: documentId,
|
||||
entity_type: e.entity_type,
|
||||
entity_value: e.entity_value,
|
||||
confidence: e.confidence || 1.0
|
||||
}));
|
||||
|
||||
await supabase.from('extracted_entities').insert(entitiesToInsert);
|
||||
}
|
||||
|
||||
// Update document
|
||||
await supabase.from('documents').update({
|
||||
content_text: extractedText,
|
||||
status: 'completed'
|
||||
}).eq('id', documentId);
|
||||
|
||||
return new Response(JSON.stringify({ success: true, entities }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing document:', error);
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
});
|
||||
141
frontend/supabase/migrations/00001_init_database_v1.sql
Normal file
141
frontend/supabase/migrations/00001_init_database_v1.sql
Normal file
@@ -0,0 +1,141 @@
|
||||
-- Create user_role enum
|
||||
CREATE TYPE public.user_role AS ENUM ('user', 'admin');
|
||||
|
||||
-- Create profiles table
|
||||
CREATE TABLE IF NOT EXISTS public.profiles (
|
||||
id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
email text,
|
||||
phone text,
|
||||
role public.user_role DEFAULT 'user'::public.user_role,
|
||||
created_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
-- Sync auth.users to profiles trigger
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
user_count int;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO user_count FROM profiles;
|
||||
INSERT INTO public.profiles (id, email, phone, role)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
NEW.email,
|
||||
NEW.phone,
|
||||
CASE WHEN user_count = 0 THEN 'admin'::public.user_role ELSE 'user'::public.user_role END
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS on_auth_user_confirmed ON auth.users;
|
||||
CREATE TRIGGER on_auth_user_confirmed
|
||||
AFTER UPDATE ON auth.users
|
||||
FOR EACH ROW
|
||||
WHEN (OLD.confirmed_at IS NULL AND NEW.confirmed_at IS NOT NULL)
|
||||
EXECUTE FUNCTION handle_new_user();
|
||||
|
||||
-- Create documents table
|
||||
CREATE TABLE IF NOT EXISTS public.documents (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_id uuid REFERENCES public.profiles(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
type text NOT NULL,
|
||||
storage_path text NOT NULL,
|
||||
content_text text,
|
||||
metadata jsonb DEFAULT '{}'::jsonb,
|
||||
status text DEFAULT 'pending',
|
||||
created_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create extracted_entities table
|
||||
CREATE TABLE IF NOT EXISTS public.extracted_entities (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
document_id uuid REFERENCES public.documents(id) ON DELETE CASCADE,
|
||||
entity_type text NOT NULL,
|
||||
entity_value text NOT NULL,
|
||||
confidence float DEFAULT 1.0,
|
||||
created_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create templates table
|
||||
CREATE TABLE IF NOT EXISTS public.templates (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_id uuid REFERENCES public.profiles(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
type text NOT NULL,
|
||||
storage_path text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
-- Create fill_tasks table
|
||||
CREATE TABLE IF NOT EXISTS public.fill_tasks (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_id uuid REFERENCES public.profiles(id) ON DELETE CASCADE,
|
||||
template_id uuid REFERENCES public.templates(id) ON DELETE CASCADE,
|
||||
document_ids uuid[] NOT NULL,
|
||||
status text DEFAULT 'pending',
|
||||
result_path text,
|
||||
error_message text,
|
||||
created_at timestamp with time zone DEFAULT now()
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.extracted_entities ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.templates ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.fill_tasks ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Helper function is_admin
|
||||
CREATE OR REPLACE FUNCTION is_admin(uid uuid)
|
||||
RETURNS boolean LANGUAGE sql SECURITY DEFINER AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM profiles p
|
||||
WHERE p.id = uid AND p.role = 'admin'::user_role
|
||||
);
|
||||
$$;
|
||||
|
||||
-- Policies
|
||||
-- Profiles: Admin full access, users view/update own (except role)
|
||||
CREATE POLICY "Admins have full access to profiles" ON profiles
|
||||
FOR ALL TO authenticated USING (is_admin(auth.uid()));
|
||||
CREATE POLICY "Users can view their own profile" ON profiles
|
||||
FOR SELECT TO authenticated USING (auth.uid() = id);
|
||||
CREATE POLICY "Users can update their own profile" ON profiles
|
||||
FOR UPDATE TO authenticated USING (auth.uid() = id)
|
||||
WITH CHECK (role IS NOT DISTINCT FROM (SELECT role FROM profiles WHERE id = auth.uid()));
|
||||
|
||||
-- Documents: owner full access, admin view
|
||||
CREATE POLICY "Users can manage own documents" ON documents
|
||||
FOR ALL TO authenticated USING (auth.uid() = owner_id);
|
||||
CREATE POLICY "Admins can view all documents" ON documents
|
||||
FOR SELECT TO authenticated USING (is_admin(auth.uid()));
|
||||
|
||||
-- Extracted Entities: owner (via document) full access
|
||||
CREATE POLICY "Users can manage entities of own docs" ON extracted_entities
|
||||
FOR ALL TO authenticated USING (
|
||||
EXISTS (SELECT 1 FROM documents d WHERE d.id = document_id AND d.owner_id = auth.uid())
|
||||
);
|
||||
|
||||
-- Templates: owner full access
|
||||
CREATE POLICY "Users can manage own templates" ON templates
|
||||
FOR ALL TO authenticated USING (auth.uid() = owner_id);
|
||||
|
||||
-- Fill Tasks: owner full access
|
||||
CREATE POLICY "Users can manage own tasks" ON fill_tasks
|
||||
FOR ALL TO authenticated USING (auth.uid() = owner_id);
|
||||
|
||||
-- Create storage buckets
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('document_storage', 'document_storage', false) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Storage policies
|
||||
CREATE POLICY "Users can upload docs" ON storage.objects
|
||||
FOR INSERT TO authenticated WITH CHECK (bucket_id = 'document_storage');
|
||||
CREATE POLICY "Users can view own docs" ON storage.objects
|
||||
FOR SELECT TO authenticated USING (bucket_id = 'document_storage' AND (auth.uid()::text = (storage.foldername(name))[1]));
|
||||
CREATE POLICY "Users can delete own docs" ON storage.objects
|
||||
FOR DELETE TO authenticated USING (bucket_id = 'document_storage' AND (auth.uid()::text = (storage.foldername(name))[1]));
|
||||
Reference in New Issue
Block a user