Compare commits

...

7 Commits

Author SHA1 Message Date
4ec6a4e493 Feat: Remove the code that outputs jsonschema from the webhook.#10427 (#12297)
### What problem does this PR solve?

Feat: Remove the code that outputs jsonschema from the webhook.#10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-12-29 17:46:05 +08:00
2d5ad42128 docs: add optional proxy arguments for Docker build instructions (#12272)
### What problem does this PR solve?

Adds instructions for passing optional HTTP/HTTPS proxy arguments when
building the Docker image.

This helps users behind a proxy to successfully build the RAGFlow Docker
image without modifying the Dockerfile itself.

### Type of change

- [x] Documentation Update
2025-12-29 17:43:55 +08:00
dccda35f65 Fix: S3 parameter error (#12290)
### What problem does this PR solve?

Fix: S3 parameter error

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-12-29 17:38:01 +08:00
d142b9095e Fix: pick message to delete (#12295)
### What problem does this PR solve?

Pick unforgotten message when not found forgotten message to delete.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-12-29 17:10:46 +08:00
c2c079886f Revert "Feat: github connector" (#12296)
Reverts infiniflow/ragflow#12292
2025-12-29 17:06:40 +08:00
c3ae1aaecd Feat: Gitlab connector (#12248)
### What problem does this PR solve?

Feat: Gitlab connector
Fix: submit button in darkmode

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
2025-12-29 17:05:20 +08:00
f099bc1236 Feat: github connector (#12292)
### What problem does this PR solve?

Feat: github connector

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-12-29 16:57:20 +08:00
27 changed files with 677 additions and 154 deletions

View File

@ -303,6 +303,15 @@ cd ragflow/
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
Or if you are behind a proxy, you can pass proxy arguments:
```bash
docker build --platform linux/amd64 \
--build-arg http_proxy=http://YOUR_PROXY:PORT \
--build-arg https_proxy=http://YOUR_PROXY:PORT \
-f Dockerfile -t infiniflow/ragflow:nightly .
```
## 🔨 Launch service from source for development
1. Install `uv` and `pre-commit`, or skip this step if they are already installed:

View File

@ -277,6 +277,15 @@ cd ragflow/
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
Jika berada di belakang proxy, Anda dapat melewatkan argumen proxy:
```bash
docker build --platform linux/amd64 \
--build-arg http_proxy=http://YOUR_PROXY:PORT \
--build-arg https_proxy=http://YOUR_PROXY:PORT \
-f Dockerfile -t infiniflow/ragflow:nightly .
```
## 🔨 Menjalankan Aplikasi dari untuk Pengembangan
1. Instal `uv` dan `pre-commit`, atau lewati langkah ini jika sudah terinstal:

View File

@ -277,6 +277,15 @@ cd ragflow/
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
プロキシ環境下にいる場合は、プロキシ引数を指定できます:
```bash
docker build --platform linux/amd64 \
--build-arg http_proxy=http://YOUR_PROXY:PORT \
--build-arg https_proxy=http://YOUR_PROXY:PORT \
-f Dockerfile -t infiniflow/ragflow:nightly .
```
## 🔨 ソースコードからサービスを起動する方法
1. `uv` と `pre-commit` をインストールする。すでにインストールされている場合は、このステップをスキップしてください:

View File

@ -271,6 +271,15 @@ cd ragflow/
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
프록시 환경인 경우, 프록시 인수를 전달할 수 있습니다:
```bash
docker build --platform linux/amd64 \
--build-arg http_proxy=http://YOUR_PROXY:PORT \
--build-arg https_proxy=http://YOUR_PROXY:PORT \
-f Dockerfile -t infiniflow/ragflow:nightly .
```
## 🔨 소스 코드로 서비스를 시작합니다.
1. `uv` 와 `pre-commit` 을 설치하거나, 이미 설치된 경우 이 단계를 건너뜁니다:

View File

@ -294,6 +294,15 @@ cd ragflow/
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
Se você estiver atrás de um proxy, pode passar argumentos de proxy:
```bash
docker build --platform linux/amd64 \
--build-arg http_proxy=http://YOUR_PROXY:PORT \
--build-arg https_proxy=http://YOUR_PROXY:PORT \
-f Dockerfile -t infiniflow/ragflow:nightly .
```
## 🔨 Lançar o serviço a partir do código-fonte para desenvolvimento
1. Instale o `uv` e o `pre-commit`, ou pule esta etapa se eles já estiverem instalados:

View File

@ -303,6 +303,15 @@ cd ragflow/
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
若您位於代理環境,可傳遞代理參數:
```bash
docker build --platform linux/amd64 \
--build-arg http_proxy=http://YOUR_PROXY:PORT \
--build-arg https_proxy=http://YOUR_PROXY:PORT \
-f Dockerfile -t infiniflow/ragflow:nightly .
```
## 🔨 以原始碼啟動服務
1. 安裝 `uv` 和 `pre-commit`。如已安裝,可跳過此步驟:

View File

@ -302,6 +302,15 @@ cd ragflow/
docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly .
```
如果您处在代理环境下,可以传递代理参数:
```bash
docker build --platform linux/amd64 \
--build-arg http_proxy=http://YOUR_PROXY:PORT \
--build-arg https_proxy=http://YOUR_PROXY:PORT \
-f Dockerfile -t infiniflow/ragflow:nightly .
```
## 🔨 以源代码启动服务
1. 安装 `uv` 和 `pre-commit`。如已经安装,可跳过本步骤:

View File

@ -130,7 +130,7 @@ class FileSource(StrEnum):
GOOGLE_CLOUD_STORAGE = "google_cloud_storage"
AIRTABLE = "airtable"
ASANA = "asana"
GITLAB = "gitlab"
class PipelineTaskType(StrEnum):
PARSE = "Parse"

View File

@ -55,6 +55,8 @@ class DocumentSource(str, Enum):
BOX = "box"
AIRTABLE = "airtable"
ASANA = "asana"
GITHUB = "github"
GITLAB = "gitlab"
class FileOrigin(str, Enum):
"""File origins"""

View File

@ -0,0 +1,340 @@
import fnmatch
import itertools
from collections import deque
from collections.abc import Iterable
from collections.abc import Iterator
from datetime import datetime
from datetime import timezone
from typing import Any
from typing import TypeVar
import gitlab
from gitlab.v4.objects import Project
from common.data_source.config import DocumentSource, INDEX_BATCH_SIZE
from common.data_source.exceptions import ConnectorMissingCredentialError
from common.data_source.exceptions import ConnectorValidationError
from common.data_source.exceptions import CredentialExpiredError
from common.data_source.exceptions import InsufficientPermissionsError
from common.data_source.exceptions import UnexpectedValidationError
from common.data_source.interfaces import GenerateDocumentsOutput
from common.data_source.interfaces import LoadConnector
from common.data_source.interfaces import PollConnector
from common.data_source.interfaces import SecondsSinceUnixEpoch
from common.data_source.models import BasicExpertInfo
from common.data_source.models import Document
from common.data_source.utils import get_file_ext
T = TypeVar("T")
# List of directories/Files to exclude
exclude_patterns = [
"logs",
".github/",
".gitlab/",
".pre-commit-config.yaml",
]
def _batch_gitlab_objects(git_objs: Iterable[T], batch_size: int) -> Iterator[list[T]]:
it = iter(git_objs)
while True:
batch = list(itertools.islice(it, batch_size))
if not batch:
break
yield batch
def get_author(author: Any) -> BasicExpertInfo:
return BasicExpertInfo(
display_name=author.get("name"),
)
def _convert_merge_request_to_document(mr: Any) -> Document:
mr_text = mr.description or ""
doc = Document(
id=mr.web_url,
blob=mr_text,
source=DocumentSource.GITLAB,
semantic_identifier=mr.title,
extension=".md",
# updated_at is UTC time but is timezone unaware, explicitly add UTC
# as there is logic in indexing to prevent wrong timestamped docs
# due to local time discrepancies with UTC
doc_updated_at=mr.updated_at.replace(tzinfo=timezone.utc),
size_bytes=len(mr_text.encode("utf-8")),
primary_owners=[get_author(mr.author)],
metadata={"state": mr.state, "type": "MergeRequest", "web_url": mr.web_url},
)
return doc
def _convert_issue_to_document(issue: Any) -> Document:
issue_text = issue.description or ""
doc = Document(
id=issue.web_url,
blob=issue_text,
source=DocumentSource.GITLAB,
semantic_identifier=issue.title,
extension=".md",
# updated_at is UTC time but is timezone unaware, explicitly add UTC
# as there is logic in indexing to prevent wrong timestamped docs
# due to local time discrepancies with UTC
doc_updated_at=issue.updated_at.replace(tzinfo=timezone.utc),
size_bytes=len(issue_text.encode("utf-8")),
primary_owners=[get_author(issue.author)],
metadata={
"state": issue.state,
"type": issue.type if issue.type else "Issue",
"web_url": issue.web_url,
},
)
return doc
def _convert_code_to_document(
project: Project, file: Any, url: str, projectName: str, projectOwner: str
) -> Document:
# Dynamically get the default branch from the project object
default_branch = project.default_branch
# Fetch the file content using the correct branch
file_content_obj = project.files.get(
file_path=file["path"], ref=default_branch # Use the default branch
)
# BoxConnector uses raw bytes for blob. Keep the same here.
file_content_bytes = file_content_obj.decode()
file_url = f"{url}/{projectOwner}/{projectName}/-/blob/{default_branch}/{file['path']}"
# Try to use the last commit timestamp for incremental sync.
# Falls back to "now" if the commit lookup fails.
last_commit_at = None
try:
# Query commit history for this file on the default branch.
commits = project.commits.list(
ref_name=default_branch,
path=file["path"],
per_page=1,
)
if commits:
# committed_date is ISO string like "2024-01-01T00:00:00.000+00:00"
committed_date = commits[0].committed_date
if isinstance(committed_date, str):
last_commit_at = datetime.strptime(
committed_date, "%Y-%m-%dT%H:%M:%S.%f%z"
).astimezone(timezone.utc)
elif isinstance(committed_date, datetime):
last_commit_at = committed_date.astimezone(timezone.utc)
except Exception:
last_commit_at = None
# Create and return a Document object
doc = Document(
# Use a stable ID so reruns don't create duplicates.
id=file_url,
blob=file_content_bytes,
source=DocumentSource.GITLAB,
semantic_identifier=file.get("name"),
extension=get_file_ext(file.get("name")),
doc_updated_at=last_commit_at or datetime.now(tz=timezone.utc),
size_bytes=len(file_content_bytes) if file_content_bytes is not None else 0,
primary_owners=[], # Add owners if needed
metadata={
"type": "CodeFile",
"path": file.get("path"),
"ref": default_branch,
"project": f"{projectOwner}/{projectName}",
"web_url": file_url,
},
)
return doc
def _should_exclude(path: str) -> bool:
"""Check if a path matches any of the exclude patterns."""
return any(fnmatch.fnmatch(path, pattern) for pattern in exclude_patterns)
class GitlabConnector(LoadConnector, PollConnector):
def __init__(
self,
project_owner: str,
project_name: str,
batch_size: int = INDEX_BATCH_SIZE,
state_filter: str = "all",
include_mrs: bool = True,
include_issues: bool = True,
include_code_files: bool = False,
) -> None:
self.project_owner = project_owner
self.project_name = project_name
self.batch_size = batch_size
self.state_filter = state_filter
self.include_mrs = include_mrs
self.include_issues = include_issues
self.include_code_files = include_code_files
self.gitlab_client: gitlab.Gitlab | None = None
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
self.gitlab_client = gitlab.Gitlab(
credentials["gitlab_url"], private_token=credentials["gitlab_access_token"]
)
return None
def validate_connector_settings(self) -> None:
if self.gitlab_client is None:
raise ConnectorMissingCredentialError("GitLab")
try:
self.gitlab_client.auth()
self.gitlab_client.projects.get(
f"{self.project_owner}/{self.project_name}",
lazy=True,
)
except gitlab.exceptions.GitlabAuthenticationError as e:
raise CredentialExpiredError(
"Invalid or expired GitLab credentials."
) from e
except gitlab.exceptions.GitlabAuthorizationError as e:
raise InsufficientPermissionsError(
"Insufficient permissions to access GitLab resources."
) from e
except gitlab.exceptions.GitlabGetError as e:
raise ConnectorValidationError(
"GitLab project not found or not accessible."
) from e
except Exception as e:
raise UnexpectedValidationError(
f"Unexpected error while validating GitLab settings: {e}"
) from e
def _fetch_from_gitlab(
self, start: datetime | None = None, end: datetime | None = None
) -> GenerateDocumentsOutput:
if self.gitlab_client is None:
raise ConnectorMissingCredentialError("Gitlab")
project: Project = self.gitlab_client.projects.get(
f"{self.project_owner}/{self.project_name}"
)
start_utc = start.astimezone(timezone.utc) if start else None
end_utc = end.astimezone(timezone.utc) if end else None
# Fetch code files
if self.include_code_files:
# Fetching using BFS as project.report_tree with recursion causing slow load
queue = deque([""]) # Start with the root directory
while queue:
current_path = queue.popleft()
files = project.repository_tree(path=current_path, all=True)
for file_batch in _batch_gitlab_objects(files, self.batch_size):
code_doc_batch: list[Document] = []
for file in file_batch:
if _should_exclude(file["path"]):
continue
if file["type"] == "blob":
doc = _convert_code_to_document(
project,
file,
self.gitlab_client.url,
self.project_name,
self.project_owner,
)
# Apply incremental window filtering for code files too.
if start_utc is not None and doc.doc_updated_at <= start_utc:
continue
if end_utc is not None and doc.doc_updated_at > end_utc:
continue
code_doc_batch.append(doc)
elif file["type"] == "tree":
queue.append(file["path"])
if code_doc_batch:
yield code_doc_batch
if self.include_mrs:
merge_requests = project.mergerequests.list(
state=self.state_filter,
order_by="updated_at",
sort="desc",
iterator=True,
)
for mr_batch in _batch_gitlab_objects(merge_requests, self.batch_size):
mr_doc_batch: list[Document] = []
for mr in mr_batch:
mr.updated_at = datetime.strptime(
mr.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z"
)
if start_utc is not None and mr.updated_at <= start_utc:
yield mr_doc_batch
return
if end_utc is not None and mr.updated_at > end_utc:
continue
mr_doc_batch.append(_convert_merge_request_to_document(mr))
yield mr_doc_batch
if self.include_issues:
issues = project.issues.list(state=self.state_filter, iterator=True)
for issue_batch in _batch_gitlab_objects(issues, self.batch_size):
issue_doc_batch: list[Document] = []
for issue in issue_batch:
issue.updated_at = datetime.strptime(
issue.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z"
)
# Avoid re-syncing the last-seen item.
if start_utc is not None and issue.updated_at <= start_utc:
yield issue_doc_batch
return
if end_utc is not None and issue.updated_at > end_utc:
continue
issue_doc_batch.append(_convert_issue_to_document(issue))
yield issue_doc_batch
def load_from_state(self) -> GenerateDocumentsOutput:
return self._fetch_from_gitlab()
def poll_source(
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
) -> GenerateDocumentsOutput:
start_datetime = datetime.fromtimestamp(start, tz=timezone.utc)
end_datetime = datetime.fromtimestamp(end, tz=timezone.utc)
return self._fetch_from_gitlab(start_datetime, end_datetime)
if __name__ == "__main__":
import os
connector = GitlabConnector(
# gitlab_url="https://gitlab.com/api/v4",
project_owner=os.environ["PROJECT_OWNER"],
project_name=os.environ["PROJECT_NAME"],
batch_size=INDEX_BATCH_SIZE,
state_filter="all",
include_mrs=True,
include_issues=True,
include_code_files=True,
)
connector.load_credentials(
{
"gitlab_access_token": os.environ["GITLAB_ACCESS_TOKEN"],
"gitlab_url": os.environ["GITLAB_URL"],
}
)
document_batches = connector.load_from_state()
for f in document_batches:
print("Batch:", f)
print("Finished loading from state.")

View File

@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
from enum import IntFlag, auto
from types import TracebackType
from typing import Any, Dict, Generator, TypeVar, Generic, Callable, TypeAlias
from collections.abc import Iterator
from anthropic import BaseModel
from common.data_source.models import (
@ -16,6 +16,7 @@ from common.data_source.models import (
SecondsSinceUnixEpoch, GenerateSlimDocumentOutput
)
GenerateDocumentsOutput = Iterator[list[Document]]
class LoadConnector(ABC):
"""Load connector interface"""

View File

@ -194,19 +194,18 @@ class MessageService:
select_fields = ["message_id", "content", "content_embed"]
_index_name = index_name(uid)
res = settings.msgStoreConn.get_forgotten_messages(select_fields, _index_name, memory_id)
if not res:
return []
message_list = settings.msgStoreConn.get_fields(res, select_fields)
current_size = 0
ids_to_remove = []
for message in message_list.values():
if current_size < size_to_delete:
current_size += cls.calculate_message_size(message)
ids_to_remove.append(message["message_id"])
else:
if res:
message_list = settings.msgStoreConn.get_fields(res, select_fields)
for message in message_list.values():
if current_size < size_to_delete:
current_size += cls.calculate_message_size(message)
ids_to_remove.append(message["message_id"])
else:
return ids_to_remove, current_size
if current_size >= size_to_delete:
return ids_to_remove, current_size
if current_size >= size_to_delete:
return ids_to_remove, current_size
order_by = OrderByExpr()
order_by.asc("valid_at")

View File

@ -150,6 +150,7 @@ dependencies = [
# "jinja2>=3.1.0",
"pyairtable>=3.3.0",
"asana>=5.2.2",
"python-gitlab>=7.0.0",
]
[dependency-groups]

View File

@ -38,12 +38,24 @@ from api.db.services.connector_service import ConnectorService, SyncLogsService
from api.db.services.knowledgebase_service import KnowledgebaseService
from common import settings
from common.config_utils import show_configs
from common.data_source import BlobStorageConnector, NotionConnector, DiscordConnector, GoogleDriveConnector, MoodleConnector, JiraConnector, DropboxConnector, WebDAVConnector, AirtableConnector, AsanaConnector
from common.data_source import (
BlobStorageConnector,
NotionConnector,
DiscordConnector,
GoogleDriveConnector,
MoodleConnector,
JiraConnector,
DropboxConnector,
WebDAVConnector,
AirtableConnector,
AsanaConnector,
)
from common.constants import FileSource, TaskStatus
from common.data_source.config import INDEX_BATCH_SIZE
from common.data_source.confluence_connector import ConfluenceConnector
from common.data_source.gmail_connector import GmailConnector
from common.data_source.box_connector import BoxConnector
from common.data_source.gitlab_connector import GitlabConnector
from common.data_source.interfaces import CheckpointOutputWrapper
from common.log_utils import init_root_logger
from common.signal_utils import start_tracemalloc_and_snapshot, stop_tracemalloc
@ -843,6 +855,47 @@ class Asana(SyncBase):
return document_generator
class Gitlab(SyncBase):
SOURCE_NAME: str = FileSource.GITLAB
async def _generate(self, task: dict):
"""
Sync files from GitLab attachments.
"""
self.connector = GitlabConnector(
project_owner= self.conf.get("project_owner"),
project_name= self.conf.get("project_name"),
include_mrs = self.conf.get("include_mrs", False),
include_issues = self.conf.get("include_issues", False),
include_code_files= self.conf.get("include_code_files", False),
)
self.connector.load_credentials(
{
"gitlab_access_token": self.conf.get("credentials", {}).get("gitlab_access_token"),
"gitlab_url": self.conf.get("credentials", {}).get("gitlab_url"),
}
)
if task["reindex"] == "1" or not task["poll_range_start"]:
document_generator = self.connector.load_from_state()
begin_info = "totally"
else:
poll_start = task["poll_range_start"]
if poll_start is None:
document_generator = self.connector.load_from_state()
begin_info = "totally"
else:
document_generator = self.connector.poll_source(
poll_start.timestamp(),
datetime.now(timezone.utc).timestamp()
)
begin_info = "from {}".format(poll_start)
logging.info("Connect to Gitlab: ({}) {}".format(self.conf["project_name"], begin_info))
return document_generator
func_factory = {
FileSource.S3: S3,
FileSource.R2: R2,
@ -862,7 +915,8 @@ func_factory = {
FileSource.WEBDAV: WebDAV,
FileSource.BOX: BOX,
FileSource.AIRTABLE: Airtable,
FileSource.ASANA: Asana
FileSource.GITLAB: Gitlab,
FileSource.ASANA: Asana,
}

15
uv.lock generated
View File

@ -5871,6 +5871,19 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" },
]
[[package]]
name = "python-gitlab"
version = "7.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "requests" },
{ name = "requests-toolbelt" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/c4/0b613303b4f0fcda69b3d2e03d0a1fb1b6b079a7c7832e03a8d92461e9fe/python_gitlab-7.0.0.tar.gz", hash = "sha256:e4d934430f64efc09e6208b782c61cc0a3389527765e03ffbef17f4323dce441", size = 400568, upload-time = "2025-10-29T15:06:02.069Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/9e/811edc46a15f8deb828cba7ef8aab3451dc11ca72d033f3df72a5af865d9/python_gitlab-7.0.0-py3-none-any.whl", hash = "sha256:712a6c8c5e79e7e66f6dabb25d8fe7831a6b238d4a5132f8231df6b3b890ceff", size = 144415, upload-time = "2025-10-29T15:06:00.232Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
@ -6178,6 +6191,7 @@ dependencies = [
{ name = "pypdf2" },
{ name = "python-calamine" },
{ name = "python-docx" },
{ name = "python-gitlab" },
{ name = "python-pptx" },
{ name = "pywencai" },
{ name = "qianfan" },
@ -6308,6 +6322,7 @@ requires-dist = [
{ name = "pypdf2", specifier = ">=3.0.1,<4.0.0" },
{ name = "python-calamine", specifier = ">=0.4.0" },
{ name = "python-docx", specifier = ">=1.1.2,<2.0.0" },
{ name = "python-gitlab", specifier = ">=7.0.0" },
{ name = "python-pptx", specifier = ">=1.0.2,<2.0.0" },
{ name = "pywencai", specifier = ">=0.13.1,<1.0.0" },
{ name = "qianfan", specifier = "==0.4.6" },

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_gitlab</title><polygon points="16 28.896 16 28.896 21.156 13.029 10.844 13.029 16 28.896" style="fill:#e24329"/><polygon points="16 28.896 10.844 13.029 3.619 13.029 16 28.896" style="fill:#fc6d26"/><path d="M3.619,13.029h0L2.052,17.851a1.067,1.067,0,0,0,.388,1.193L16,28.9,3.619,13.029Z" style="fill:#fca326"/><path d="M3.619,13.029h7.225L7.739,3.473a.534.534,0,0,0-1.015,0L3.619,13.029Z" style="fill:#e24329"/><polygon points="16 28.896 21.156 13.029 28.381 13.029 16 28.896" style="fill:#fc6d26"/><path d="M28.381,13.029h0l1.567,4.822a1.067,1.067,0,0,1-.388,1.193L16,28.9,28.381,13.029Z" style="fill:#fca326"/><path d="M28.381,13.029H21.156l3.105-9.557a.534.534,0,0,1,1.015,0l3.105,9.557Z" style="fill:#e24329"/></svg>

After

Width:  |  Height:  |  Size: 946 B

View File

@ -1,6 +1,7 @@
import { zodResolver } from '@hookform/resolvers/zod';
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
@ -44,6 +45,11 @@ const getNestedValue = (obj: any, path: string) => {
}, obj);
};
/**
* Properties of this field will be treated as static attributes and will be filtered out during form submission.
*/
export const FilterFormField = 'RAG_DY_STATIC';
// Field type enumeration
export enum FormFieldType {
Text = 'text',
@ -660,7 +666,6 @@ const DynamicForm = {
useMemo(() => {
setFields(originFields);
}, [originFields]);
const schema = useMemo(() => generateSchema(fields), [fields]);
const defaultValues = useMemo(() => {
const value = {
@ -729,7 +734,6 @@ const DynamicForm = {
...fieldErrors,
} as any;
console.log('combinedErrors', combinedErrors);
for (const key in combinedErrors) {
if (Array.isArray(combinedErrors[key])) {
combinedErrors[key] = combinedErrors[key][0];
@ -777,11 +781,61 @@ const DynamicForm = {
};
}, [fields, form]);
const filterActiveValues = useCallback(
(allValues: any) => {
const filteredValues: any = {};
fields.forEach((field) => {
if (
!field.shouldRender ||
(field.shouldRender(allValues) &&
field.name?.indexOf(FilterFormField) < 0)
) {
const keys = field.name.split('.');
let current = allValues;
let exists = true;
for (const key of keys) {
if (current && current[key] !== undefined) {
current = current[key];
} else {
exists = false;
break;
}
}
if (exists) {
let target = filteredValues;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!target[key]) {
target[key] = {};
}
target = target[key];
}
target[keys[keys.length - 1]] = getNestedValue(
allValues,
field.name,
);
}
}
});
return filteredValues;
},
[fields],
);
// Expose form methods via ref
useImperativeHandle(
ref,
() => ({
submit: form.handleSubmit(onSubmit),
submit: () => {
form.handleSubmit((values) => {
const filteredValues = filterActiveValues(values);
onSubmit(filteredValues);
})();
},
getValues: form.getValues,
reset: (values?: T) => {
if (values) {
@ -824,9 +878,9 @@ const DynamicForm = {
// }, 0);
},
}),
[form],
[form, onSubmit, filterActiveValues],
);
(form as any).filterActiveValues = filterActiveValues;
useEffect(() => {
if (formDefaultValues && Object.keys(formDefaultValues).length > 0) {
form.reset({
@ -848,7 +902,10 @@ const DynamicForm = {
className={`space-y-6 ${className}`}
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit(onSubmit)(e);
form.handleSubmit((values) => {
const filteredValues = filterActiveValues(values);
onSubmit(filteredValues);
})(e);
}}
>
<>
@ -897,10 +954,23 @@ const DynamicForm = {
try {
let beValid = await form.formControl.trigger();
console.log('form valid', beValid, form, form.formControl);
if (beValid) {
// if (beValid) {
// form.handleSubmit(async (values) => {
// console.log('form values', values);
// submitFunc?.(values);
// })();
// }
if (beValid && submitFunc) {
form.handleSubmit(async (values) => {
console.log('form values', values);
submitFunc?.(values);
const filteredValues = (form as any).filterActiveValues
? (form as any).filterActiveValues(values)
: values;
console.log(
'filtered form values in saving button',
filteredValues,
);
submitFunc(filteredValues);
})();
}
} catch (e) {

View File

@ -929,6 +929,8 @@ Beispiel: Virtual Hosted Style`,
'Verbinden Sie Ihr Gmail über OAuth, um E-Mails zu synchronisieren.',
webdavDescription:
'Verbinden Sie sich mit WebDAV-Servern, um Dateien zu synchronisieren.',
gitlabDescription:
'Verbinden Sie GitLab, um Repositories, Issues, Merge Requests und zugehörige Dokumentation zu synchronisieren.',
webdavRemotePathTip:
'Optional: Geben Sie einen Ordnerpfad auf dem WebDAV-Server an (z.B. /Dokumente). Lassen Sie das Feld leer, um vom Stammverzeichnis aus zu synchronisieren.',
google_driveTokenTip:

View File

@ -933,6 +933,8 @@ Example: Virtual Hosted Style`,
boxDescription: 'Connect your Box drive to sync files and folders.',
airtableDescription:
'Connect to Airtable and synchronize files from a specified table within a designated workspace.',
gitlabDescription:
'Connect GitLab to sync repositories, issues, merge requests, and related documentation.',
asanaDescription:
'Connect to Asana and synchronize files from a specified workspace.',
dropboxAccessTokenTip:

View File

@ -749,6 +749,8 @@ export default {
'Подключите ваш диск Box для синхронизации файлов и папок.',
airtableDescription:
'Подключите Airtable и синхронизируйте файлы из указанной таблицы в заданном рабочем пространстве.',
gitlabDescription:
'Подключите GitLab для синхронизации репозиториев, задач, merge requests и связанной документации.',
asanaDescription:
'Подключите Asana и синхронизируйте файлы из рабочего пространства.',
google_driveDescription:

View File

@ -547,6 +547,8 @@ export default {
avatar: '头像',
avatarTip: '這會在你的個人主頁展示',
profileDescription: '在此更新您的照片和個人詳細信息。',
gitlabDescription:
'連接 GitLab同步儲存庫、Issue、合併請求MR及相關文件內容。',
bedrockCredentialsHint:
'提示Access Key / Secret Key 可留空,以啟用 AWS IAM 自動驗證。',
awsAuthModeAccessKeySecret: 'Access Key 和 Secret',

View File

@ -862,6 +862,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
dropboxDescription: '连接 Dropbox同步指定账号下的文件与文件夹。',
boxDescription: '连接你的 Box 云盘以同步文件和文件夹。',
airtableDescription: '连接 Airtable同步指定工作区下指定表格中的文件。',
gitlabDescription:
'连接 GitLab同步仓库、Issue、合并请求MR及相关文档内容。',
asanaDescription: '连接 Asana同步工作区中的文件。',
r2Description: '连接你的 Cloudflare R2 存储桶以导入和同步文件。',
dropboxAccessTokenTip:

View File

@ -24,8 +24,6 @@ export const FormSchema = z.object(VariableAssignerSchema);
export type VariableAssignerFormSchemaType = z.infer<typeof FormSchema>;
// const outputList = buildOutputList(initialVariableAssignerValues.outputs);
function VariableAssignerForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialDataOperationsValues, node);
@ -41,10 +39,7 @@ function VariableAssignerForm({ node }: INextOperatorForm) {
<Form {...form}>
<FormWrapper>
<DynamicVariables name="variables" label="Variables"></DynamicVariables>
{/* <Output list={outputList} isFormRequired></Output> */}
</FormWrapper>
{/* <DevTool control={form.control} placement="top-left" /> */}
{/* set up the dev tool */}
</Form>
);
}

View File

@ -2,9 +2,7 @@ import { getStructuredDatatype } from '@/utils/canvas-util';
import { get, isPlainObject } from 'lodash';
import { ReactNode, useCallback } from 'react';
import {
AgentDialogueMode,
AgentStructuredOutputField,
BeginId,
JsonSchemaDataType,
Operator,
} from '../constant';
@ -18,94 +16,36 @@ function getNodeId(value: string) {
}
export function useShowSecondaryMenu() {
const { getOperatorTypeFromId, getNode } = useGraphStore((state) => state);
const { getOperatorTypeFromId } = useGraphStore((state) => state);
const showSecondaryMenu = useCallback(
(value: string, outputLabel: string) => {
const nodeId = getNodeId(value);
const operatorType = getOperatorTypeFromId(nodeId);
// For Agent nodes, show secondary menu for 'structured' field
if (
operatorType === Operator.Agent &&
return (
getOperatorTypeFromId(nodeId) === Operator.Agent &&
outputLabel === AgentStructuredOutputField
) {
return true;
}
// For Begin nodes in webhook mode, show secondary menu for schema properties (body, headers, query, etc.)
if (operatorType === Operator.Begin) {
const node = getNode(nodeId);
const mode = get(node, 'data.form.mode');
if (mode === AgentDialogueMode.Webhook) {
// Check if this output field is from the schema
const outputs = get(node, 'data.form.outputs', {});
const outputField = outputs[outputLabel];
// Show secondary menu if the field is an object or has properties
return (
outputField &&
(outputField.type === 'object' ||
(outputField.properties &&
Object.keys(outputField.properties).length > 0))
);
}
}
return false;
);
},
[getOperatorTypeFromId, getNode],
[getOperatorTypeFromId],
);
return showSecondaryMenu;
}
function useGetBeginOutputsOrSchema() {
const { getNode } = useGraphStore((state) => state);
const getBeginOutputs = useCallback(() => {
const node = getNode(BeginId);
const outputs = get(node, 'data.form.outputs', {});
return outputs;
}, [getNode]);
const getBeginSchema = useCallback(() => {
const node = getNode(BeginId);
const outputs = get(node, 'data.form.schema', {});
return outputs;
}, [getNode]);
return { getBeginOutputs, getBeginSchema };
}
export function useGetStructuredOutputByValue() {
const { getNode, getOperatorTypeFromId } = useGraphStore((state) => state);
const { getBeginOutputs } = useGetBeginOutputsOrSchema();
const { getNode } = useGraphStore((state) => state);
const getStructuredOutput = useCallback(
(value: string) => {
const nodeId = getNodeId(value);
const node = getNode(nodeId);
const operatorType = getOperatorTypeFromId(nodeId);
const fields = splitValue(value);
const outputLabel = fields.at(1);
let structuredOutput;
if (operatorType === Operator.Agent) {
structuredOutput = get(
node,
`data.form.outputs.${AgentStructuredOutputField}`,
);
} else if (operatorType === Operator.Begin) {
// For Begin nodes in webhook mode, get the specific schema property
const outputs = getBeginOutputs();
if (outputLabel) {
structuredOutput = outputs[outputLabel];
}
}
const node = getNode(getNodeId(value));
const structuredOutput = get(
node,
`data.form.outputs.${AgentStructuredOutputField}`,
);
return structuredOutput;
},
[getBeginOutputs, getNode, getOperatorTypeFromId],
[getNode],
);
return getStructuredOutput;
@ -126,14 +66,13 @@ export function useFindAgentStructuredOutputLabel() {
icon?: ReactNode;
}>,
) => {
// agent structured output
const fields = splitValue(value);
const operatorType = getOperatorTypeFromId(fields.at(0));
// Handle Agent structured fields
if (
operatorType === Operator.Agent &&
getOperatorTypeFromId(fields.at(0)) === Operator.Agent &&
fields.at(1)?.startsWith(AgentStructuredOutputField)
) {
// is agent structured output
const agentOption = options.find((x) => value.includes(x.value));
const jsonSchemaFields = fields
.at(1)
@ -145,19 +84,6 @@ export function useFindAgentStructuredOutputLabel() {
value: value,
};
}
// Handle Begin webhook fields
if (operatorType === Operator.Begin && fields.at(1)) {
const fieldOption = options
.filter((x) => x.parentLabel === BeginId)
.find((x) => value.startsWith(x.value));
return {
...fieldOption,
label: fields.at(1),
value: value,
};
}
},
[getOperatorTypeFromId],
);
@ -168,7 +94,6 @@ export function useFindAgentStructuredOutputLabel() {
export function useFindAgentStructuredOutputTypeByValue() {
const { getOperatorTypeFromId } = useGraphStore((state) => state);
const filterStructuredOutput = useGetStructuredOutputByValue();
const { getBeginSchema } = useGetBeginOutputsOrSchema();
const findTypeByValue = useCallback(
(
@ -211,12 +136,10 @@ export function useFindAgentStructuredOutputTypeByValue() {
}
const fields = splitValue(value);
const nodeId = fields.at(0);
const operatorType = getOperatorTypeFromId(nodeId);
const jsonSchema = filterStructuredOutput(value);
// Handle Agent structured fields
if (
operatorType === Operator.Agent &&
getOperatorTypeFromId(nodeId) === Operator.Agent &&
fields.at(1)?.startsWith(AgentStructuredOutputField)
) {
const jsonSchemaFields = fields
@ -228,32 +151,13 @@ export function useFindAgentStructuredOutputTypeByValue() {
return type;
}
}
// Handle Begin webhook fields (body, headers, query, etc.)
if (operatorType === Operator.Begin) {
const outputLabel = fields.at(1);
const schema = getBeginSchema();
if (outputLabel && schema) {
const jsonSchemaFields = fields.at(1);
if (jsonSchemaFields) {
const type = findTypeByValue(schema, jsonSchemaFields);
return type;
}
}
}
},
[
filterStructuredOutput,
findTypeByValue,
getBeginSchema,
getOperatorTypeFromId,
],
[filterStructuredOutput, findTypeByValue, getOperatorTypeFromId],
);
return findAgentStructuredOutputTypeByValue;
}
// TODO: Consider merging with useFindAgentStructuredOutputLabel
export function useFindAgentStructuredOutputLabelByValue() {
const { getNode } = useGraphStore((state) => state);

View File

@ -318,8 +318,7 @@ export function useFilterQueryVariableOptionsByTypes({
isAgentStructured(
y.value,
y.value.slice(-AgentStructuredOutputField.length),
) ||
y.value.startsWith(BeginId), // begin node outputs
),
),
};
})

View File

@ -25,6 +25,7 @@ export enum DataSourceKey {
OCI_STORAGE = 'oci_storage',
GOOGLE_CLOUD_STORAGE = 'google_cloud_storage',
AIRTABLE = 'airtable',
GITLAB = 'gitlab',
ASANA = 'asana',
// SHAREPOINT = 'sharepoint',
// SLACK = 'slack',
@ -110,6 +111,11 @@ export const generateDataSourceInfo = (t: TFunction) => {
description: t(`setting.${DataSourceKey.AIRTABLE}Description`),
icon: <SvgIcon name={'data-source/airtable'} width={38} />,
},
[DataSourceKey.GITLAB]: {
name: 'GitLab',
description: t(`setting.${DataSourceKey.GITLAB}Description`),
icon: <SvgIcon name={'data-source/gitlab'} width={38} />,
},
[DataSourceKey.ASANA]: {
name: 'Asana',
description: t(`setting.${DataSourceKey.ASANA}Description`),
@ -658,6 +664,54 @@ export const DataSourceFormFields = {
required: true,
},
],
[DataSourceKey.GITLAB]: [
{
label: 'Project Owner',
name: 'config.project_owner',
type: FormFieldType.Text,
required: true,
},
{
label: 'Project Name',
name: 'config.project_name',
type: FormFieldType.Text,
required: true,
},
{
label: 'GitLab Personal Access Token',
name: 'config.credentials.gitlab_access_token',
type: FormFieldType.Password,
required: true,
},
{
label: 'GitLab URL',
name: 'config.gitlab_url',
type: FormFieldType.Text,
required: true,
placeholder: 'https://gitlab.com',
},
{
label: 'include Merge Requests',
name: 'config.include_mrs',
type: FormFieldType.Checkbox,
required: false,
defaultValue: true,
},
{
label: 'include Issues',
name: 'config.include_issues',
type: FormFieldType.Checkbox,
required: false,
defaultValue: true,
},
{
label: 'include Code Files',
name: 'config.include_code_files',
type: FormFieldType.Checkbox,
required: false,
defaultValue: true,
},
],
[DataSourceKey.ASANA]: [
{
label: 'API Token',
@ -693,7 +747,6 @@ export const DataSourceFormDefaultValues = {
config: {
bucket_name: '',
bucket_type: 's3',
authMode: 'access_key',
prefix: '',
credentials: {
aws_access_key_id: '',
@ -883,6 +936,21 @@ export const DataSourceFormDefaultValues = {
},
},
},
[DataSourceKey.GITLAB]: {
name: '',
source: DataSourceKey.GITLAB,
config: {
project_owner: '',
project_name: '',
gitlab_url: 'https://gitlab.com',
include_mrs: true,
include_issues: true,
include_code_files: true,
credentials: {
gitlab_access_token: '',
},
},
},
[DataSourceKey.ASANA]: {
name: '',
source: DataSourceKey.ASANA,

View File

@ -1,4 +1,4 @@
import { FormFieldType } from '@/components/dynamic-form';
import { FilterFormField, FormFieldType } from '@/components/dynamic-form';
import { TFunction } from 'i18next';
import { BedrockRegionList } from '../../setting-model/constant';
@ -50,7 +50,7 @@ export const S3Constant = (t: TFunction) => [
},
{
label: 'Authentication',
name: 'config.authMode',
name: 'config.credentials.authentication_method',
type: FormFieldType.Segmented,
options: [
{ label: 'Access Key', value: 'access_key' },
@ -67,7 +67,7 @@ export const S3Constant = (t: TFunction) => [
label: 'AWS Access Key ID',
type: FormFieldType.Text,
customValidate: (val: string, formValues: any) => {
const authMode = formValues?.config?.authMode;
const authMode = formValues?.config?.credentials?.authentication_method;
const bucketType = formValues?.config?.bucket_type;
console.log('authMode', authMode, val);
if (
@ -79,7 +79,7 @@ export const S3Constant = (t: TFunction) => [
return true;
},
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const authMode = formValues?.config?.credentials?.authentication_method;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'access_key' || bucketType === 's3_compatible';
},
@ -89,7 +89,7 @@ export const S3Constant = (t: TFunction) => [
label: 'AWS Secret Access Key',
type: FormFieldType.Password,
customValidate: (val: string, formValues: any) => {
const authMode = formValues?.config?.authMode;
const authMode = formValues?.config?.credentials?.authentication_method;
const bucketType = formValues?.config?.bucket_type;
if (authMode === 'access_key' || bucketType === 's3_compatible') {
return Boolean(val) || '"AWS Secret Access Key" is required';
@ -97,7 +97,7 @@ export const S3Constant = (t: TFunction) => [
return true;
},
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const authMode = formValues?.config?.credentials?.authentication_method;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'access_key' || bucketType === 's3_compatible';
},
@ -109,7 +109,7 @@ export const S3Constant = (t: TFunction) => [
type: FormFieldType.Text,
placeholder: 'arn:aws:iam::123456789012:role/YourRole',
customValidate: (val: string, formValues: any) => {
const authMode = formValues?.config?.authMode;
const authMode = formValues?.config?.credentials?.authentication_method;
const bucketType = formValues?.config?.bucket_type;
if (authMode === 'iam_role' || bucketType === 's3') {
return Boolean(val) || '"AWS Secret Access Key" is required';
@ -117,17 +117,17 @@ export const S3Constant = (t: TFunction) => [
return true;
},
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const authMode = formValues?.config?.credentials?.authentication_method;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'iam_role' && bucketType === 's3';
},
},
{
name: 'static.tip',
name: FilterFormField + '.tip',
label: ' ',
type: FormFieldType.Custom,
shouldRender: (formValues: any) => {
const authMode = formValues?.config?.authMode;
const authMode = formValues?.config?.credentials?.authentication_method;
const bucketType = formValues?.config?.bucket_type;
return authMode === 'assume_role' && bucketType === 's3';
},