From 4a91ca53492edef2431b94b716d419bd5d4896d4 Mon Sep 17 00:00:00 2001 From: Haruko386 Date: Wed, 20 May 2026 19:21:57 +0800 Subject: [PATCH] Go: implement provider: MinerU_Local (#15051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What problem does this PR solve? 1. Add model types when add model --- ``` RAGFlow(user)> add model 'pipeline' to provider 'mineru_local' instance 'test' with tokens 131072 doc_parse; SUCCESS ``` 2. implement provider: MinerU_Local --- **Verified from CLI** ``` RAGFlow(user)> parse with 'pipeline@test@mineru_local' file './internal/test.pdf' +--------------------------------------+ | task_id | +--------------------------------------+ | c7260e31-b6e2-4b36-955d-e9c60510c669 | +--------------------------------------+ RAGFlow(user)> show 'test@mineru_local' task 'c7260e31-b6e2-4b36-955d-e9c60510c669' +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ | content | index | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ | # Repurposing Diffusion-Based Image Generators for Monocular Depth Estimation Bingxin Ke Anton Obukhov Shengyu Huang Nando Metzger Rodrigo Caye Daudt Konrad Schindler Photogrammetry and Remote Sensing, ETH Zurich ยจ ![](images/ae256101419715b544d13722... | 1 | +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------+ ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- conf/models/mineru_local.json | 8 + internal/cli/lexer.go | 2 + internal/cli/types.go | 1 + internal/cli/user_parser.go | 3 + internal/entity/models/factory.go | 2 + internal/entity/models/mineru_local.go | 267 +++++++++++++++++++++++++ 6 files changed, 283 insertions(+) create mode 100644 conf/models/mineru_local.json create mode 100644 internal/entity/models/mineru_local.go diff --git a/conf/models/mineru_local.json b/conf/models/mineru_local.json new file mode 100644 index 0000000000..54bd46e391 --- /dev/null +++ b/conf/models/mineru_local.json @@ -0,0 +1,8 @@ +{ + "name": "mineru_local", + "url_suffix": { + "doc_parse": "file_parse", + "task": "tasks" + }, + "class": "local" +} \ No newline at end of file diff --git a/internal/cli/lexer.go b/internal/cli/lexer.go index 6df63fde0c..b3bd1c8b32 100644 --- a/internal/cli/lexer.go +++ b/internal/cli/lexer.go @@ -375,6 +375,8 @@ func (l *Lexer) lookupIdent(ident string) Token { return Token{Type: TokenDimension, Value: ident} case "OCR": return Token{Type: TokenOCR, Value: ident} + case "DOC_PARSE": + return Token{Type: TokenDocParse, Value: ident} case "ASYNC": return Token{Type: TokenAsync, Value: ident} case "SYNC": diff --git a/internal/cli/types.go b/internal/cli/types.go index e5bf55d9fc..3f3ef27425 100644 --- a/internal/cli/types.go +++ b/internal/cli/types.go @@ -102,6 +102,7 @@ const ( TokenASR TokenTTS TokenOCR + TokenDocParse TokenEmbed TokenText TokenQuery diff --git a/internal/cli/user_parser.go b/internal/cli/user_parser.go index 36b7a35297..64357abeea 100644 --- a/internal/cli/user_parser.go +++ b/internal/cli/user_parser.go @@ -862,6 +862,9 @@ func (p *Parser) parseAddModel() (*Command, error) { case TokenOCR: p.nextToken() modelTypes = append(modelTypes, "ocr") + case TokenDocParse: + p.nextToken() + modelTypes = append(modelTypes, "doc_parse") case TokenTTS: p.nextToken() modelTypes = append(modelTypes, "tts") diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go index a0d7dd1afe..819878ed62 100644 --- a/internal/entity/models/factory.go +++ b/internal/entity/models/factory.go @@ -113,6 +113,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string return NewJieKouAIModel(baseURL, urlSuffix), nil case "302.ai": return NewAI302Model(baseURL, urlSuffix), nil + case "mineru_local": + return NewMinerLocalUModel(baseURL, urlSuffix), nil default: return NewDummyModel(baseURL, urlSuffix), nil } diff --git a/internal/entity/models/mineru_local.go b/internal/entity/models/mineru_local.go new file mode 100644 index 0000000000..177ce77530 --- /dev/null +++ b/internal/entity/models/mineru_local.go @@ -0,0 +1,267 @@ +package models + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "time" +) + +type MinerULocalModel struct { + BaseURL map[string]string + URLSuffix URLSuffix + httpClient *http.Client +} + +func NewMinerLocalUModel(baseURL map[string]string, urlSuffix URLSuffix) *MinerULocalModel { + return &MinerULocalModel{ + BaseURL: baseURL, + URLSuffix: urlSuffix, + httpClient: &http.Client{ + Timeout: time.Second * 120, + Transport: &http.Transport{ + MaxIdleConns: 10, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: time.Second * 90, + DisableCompression: false, + }, + }, + } +} + +func (m *MinerULocalModel) NewInstance(baseURL map[string]string) ModelDriver { + return &MinerULocalModel{ + BaseURL: baseURL, + URLSuffix: m.URLSuffix, + httpClient: &http.Client{ + Timeout: time.Second * 120, + Transport: &http.Transport{ + MaxIdleConns: 10, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: time.Second * 90, + DisableCompression: false, + }, + }, + } +} + +func (m *MinerULocalModel) Name() string { + return "mineru_local" +} + +func (m *MinerULocalModel) ChatWithMessages(modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig) (*ChatResponse, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) ChatStreamlyWithSender(modelName string, messages []Message, apiConfig *APIConfig, modelConfig *ChatConfig, sender func(*string, *string) error) error { + return fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) Embed(modelName *string, texts []string, apiConfig *APIConfig, embeddingConfig *EmbeddingConfig) ([]EmbeddingData, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) Rerank(modelName *string, query string, documents []string, apiConfig *APIConfig, rerankConfig *RerankConfig) (*RerankResponse, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) TranscribeAudio(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig) (*ASRResponse, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) TranscribeAudioWithSender(modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, sender func(*string, *string) error) error { + return fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) AudioSpeech(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig) (*TTSResponse, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) AudioSpeechWithSender(modelName *string, audioContent *string, apiConfig *APIConfig, ttsConfig *TTSConfig, sender func(*string, *string) error) error { + return fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) OCRFile(modelName *string, content []byte, url *string, apiConfig *APIConfig, ocrConfig *OCRConfig) (*OCRFileResponse, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) ListModels(apiConfig *APIConfig) ([]string, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) Balance(apiConfig *APIConfig) (map[string]interface{}, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) CheckConnection(apiConfig *APIConfig) error { + return fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) ParseFile(modelName *string, content []byte, documentURL *string, apiConfig *APIConfig, parseFileConfig *ParseFileConfig) (*ParseFileResponse, error) { + if len(content) == 0 { + return nil, fmt.Errorf("local MinerU API requires file content byte array, but content is empty") + } + + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + apiURL := fmt.Sprintf("%s/%s", m.BaseURL[region], m.URLSuffix.DocumentParse) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + // Get file + part, err := writer.CreateFormFile("files", "upload_document.pdf") + if err != nil { + return nil, fmt.Errorf("failed to create multipart file field: %w", err) + } + if _, err = part.Write(content); err != nil { + return nil, fmt.Errorf("failed to write file content: %w", err) + } + + if modelName != nil && *modelName != "" { + _ = writer.WriteField("backend", *modelName) + } else { + _ = writer.WriteField("backend", "pipeline") + } + + if err = writer.Close(); err != nil { + return nil, fmt.Errorf("failed to close multipart writer: %w", err) + } + + req, err := http.NewRequest("POST", apiURL, &body) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } + + resp, err := m.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != 202 { + return nil, fmt.Errorf("local MinerU API failed with status %d: %s (URL: %s)", resp.StatusCode, string(respBody), apiURL) + } + + var result map[string]interface{} + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("failed to parse response JSON: %w, body: %s", err, string(respBody)) + } + // Get task ID + var taskID string + if dataMap, ok := result["data"].(map[string]interface{}); ok { + if tid, ok := dataMap["task_id"].(string); ok { + taskID = tid + } + } else if tid, ok := result["task_id"].(string); ok { + taskID = tid + } + + if taskID == "" { + return nil, fmt.Errorf("failed to extract task_id from local MinerU response: %s", string(respBody)) + } + + return &ParseFileResponse{ + TaskID: taskID, + }, nil +} + +func (m *MinerULocalModel) ListTasks(apiConfig *APIConfig) ([]ListTaskStatus, error) { + return nil, fmt.Errorf("%s no such method", m.Name()) +} + +func (m *MinerULocalModel) ShowTask(taskID string, apiConfig *APIConfig) (*TaskResponse, error) { + if taskID == "" { + return nil, fmt.Errorf("taskID is empty") + } + + var region = "default" + if apiConfig != nil && apiConfig.Region != nil && *apiConfig.Region != "" { + region = *apiConfig.Region + } + + url := fmt.Sprintf("%s/%s/%s/result", m.BaseURL[region], m.URLSuffix.Task, taskID) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create status request: %w", err) + } + + if apiConfig != nil && apiConfig.ApiKey != nil && *apiConfig.ApiKey != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiConfig.ApiKey)) + } + + resp, err := m.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send status request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read status response: %w", err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != 202 { + return nil, fmt.Errorf("MinerU local status API failed with status %d: %s", resp.StatusCode, string(body)) + } + + // parse JSON + var result map[string]interface{} + + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + content := "" + + // results + results, ok := result["results"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing results field") + } + + // Get markdown + for _, fileObj := range results { + + fileMap, ok := fileObj.(map[string]interface{}) + if !ok { + continue + } + + md, ok := fileMap["md_content"].(string) + if ok { + content = md + break + } + } + + if content == "" { + return nil, fmt.Errorf("md_content not found") + } + + return &TaskResponse{ + Segments: []TaskSegment{ + { + Index: 1, + Content: content, + }, + }, + }, nil +}